참고소스 수정본
This commit is contained in:
31
참고/ontocast-main/ontocast/agent/__init__.py
Normal file
31
참고/ontocast-main/ontocast/agent/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Agent module for OntoCast.
|
||||
|
||||
This module provides a collection of agents that handle various aspects of ontology
|
||||
processing, including document conversion, text chunking, fact aggregation, and
|
||||
ontology management. Each agent is designed to perform a specific task in the
|
||||
ontology processing pipeline.
|
||||
"""
|
||||
|
||||
from .chunk_text import chunk_text
|
||||
from .convert_document import convert_document
|
||||
from .criticise_facts import criticise_facts
|
||||
from .criticise_ontology import criticise_ontology
|
||||
from .render_facts import render_facts, render_facts_fresh
|
||||
from .render_ontology import render_ontology, render_ontology_fresh
|
||||
from .select_ontology import select_ontology
|
||||
from .serialize import serialize
|
||||
from .sublimate_ontology import sublimate_ontology
|
||||
|
||||
__all__ = [
|
||||
"chunk_text",
|
||||
"convert_document",
|
||||
"criticise_facts",
|
||||
"criticise_ontology",
|
||||
"render_facts",
|
||||
"render_ontology",
|
||||
"select_ontology",
|
||||
"serialize",
|
||||
"sublimate_ontology",
|
||||
"render_ontology_fresh",
|
||||
"render_facts_fresh",
|
||||
]
|
||||
61
참고/ontocast-main/ontocast/agent/chunk_text.py
Normal file
61
참고/ontocast-main/ontocast/agent/chunk_text.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Text chunking agent for OntoCast.
|
||||
|
||||
This module provides functionality for splitting text into manageable chunks
|
||||
that can be processed independently, ensuring optimal processing of large
|
||||
documents.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ontocast.onto.content_unit import ContentUnit
|
||||
from ontocast.onto.enum import Status
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def chunk_text(state: AgentState, tools: ToolBox) -> AgentState:
|
||||
"""Split text into manageable chunks.
|
||||
|
||||
This function takes the converted document text and splits it into smaller,
|
||||
manageable chunks that can be processed independently.
|
||||
|
||||
Args:
|
||||
state: The current agent state containing the text to chunk.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
AgentState: Updated state with text chunks.
|
||||
"""
|
||||
logger.info("Chunking the text")
|
||||
if state.input_text is not None:
|
||||
chunks_txt: list[str] = tools.chunker(state.input_text)
|
||||
logger.info(
|
||||
f"Created {len(chunks_txt)} chunks for processing: {[len(c) for c in chunks_txt]}"
|
||||
)
|
||||
|
||||
if state.max_chunks is not None:
|
||||
logger.info(f"Selecting {state.max_chunks} chunks")
|
||||
|
||||
chunks_txt = chunks_txt[: state.max_chunks]
|
||||
|
||||
for i, chunk_txt in enumerate(chunks_txt):
|
||||
state.content_units.append(
|
||||
ContentUnit(
|
||||
text=chunk_txt,
|
||||
index=i,
|
||||
doc_iri=state.doc_iri,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Created "
|
||||
f"{len(state.content_units)} content units for processing: "
|
||||
f"{[len(c) for c in state.content_units]}"
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
else:
|
||||
state.status = Status.FAILED
|
||||
|
||||
return state
|
||||
151
참고/ontocast-main/ontocast/agent/common.py
Normal file
151
참고/ontocast-main/ontocast/agent/common.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from langchain_core.output_parsers import BaseOutputParser
|
||||
from langchain_core.prompts import BasePromptTemplate
|
||||
|
||||
from ontocast.onto.enum import WorkflowNode
|
||||
from ontocast.onto.model import Suggestions
|
||||
from ontocast.prompt.common import (
|
||||
suggestion_concrete_template,
|
||||
suggestion_general_template,
|
||||
)
|
||||
from ontocast.prompt.render_facts import (
|
||||
improvement_instruction_template as facts_template,
|
||||
)
|
||||
from ontocast.prompt.render_ontology import (
|
||||
improvement_instruction_template as ontology_template,
|
||||
)
|
||||
from ontocast.tool import LLMTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def render_suggestions_prompt(suggestions: Suggestions, stage: WorkflowNode) -> str:
|
||||
"""Generate prompt templates from the suggestions.
|
||||
|
||||
Returns:
|
||||
Combined string with general and concrete templates.
|
||||
Returns empty string if both fields are empty.
|
||||
"""
|
||||
|
||||
# Generate general template if systemic_critique_summary is not empty
|
||||
general_template = ""
|
||||
if suggestions.systemic_critique_summary.strip():
|
||||
general_template = suggestion_general_template.format(
|
||||
general_suggestion=suggestions.systemic_critique_summary
|
||||
)
|
||||
|
||||
concrete_template = ""
|
||||
if suggestions.actionable_fixes:
|
||||
# Generate concrete template if actionable_fixes is not empty
|
||||
concrete_template = suggestion_concrete_template.format(
|
||||
suggestion_str=suggestions.to_markdown()
|
||||
)
|
||||
|
||||
if stage == WorkflowNode.TEXT_TO_FACTS:
|
||||
template = facts_template
|
||||
elif stage == WorkflowNode.TEXT_TO_ONTOLOGY:
|
||||
template = ontology_template
|
||||
else:
|
||||
raise ValueError(f"Stage {stage} not supported")
|
||||
if general_template or concrete_template:
|
||||
final_prompt = template.format(
|
||||
suggestions_instruction=f"\n\n{general_template}\n\n{concrete_template}"
|
||||
)
|
||||
else:
|
||||
final_prompt = ""
|
||||
return final_prompt
|
||||
|
||||
|
||||
async def call_llm_with_retry(
|
||||
llm_tool: LLMTool,
|
||||
prompt: BasePromptTemplate,
|
||||
parser: BaseOutputParser[T],
|
||||
prompt_kwargs: dict[str, Any],
|
||||
max_retries: int = 3,
|
||||
retry_error_feedback: bool = True,
|
||||
) -> T:
|
||||
"""Call LLM and parse response with automatic retry on parsing failures.
|
||||
|
||||
This utility function implements a common pattern across agent functions:
|
||||
1. Call LLM with a prompt
|
||||
2. Parse the response
|
||||
3. Retry if parsing fails (up to max_retries times)
|
||||
|
||||
On retry, if retry_error_feedback is True, the error message from the previous
|
||||
attempt is included in the prompt to help the LLM correct its output format.
|
||||
|
||||
Args:
|
||||
llm_tool: The LLM tool instance to use for generation.
|
||||
prompt: The prompt template to format and send to the LLM.
|
||||
parser: The output parser to parse the LLM response.
|
||||
prompt_kwargs: Keyword arguments to pass to prompt.format_prompt().
|
||||
max_retries: Maximum number of retry attempts (default: 3).
|
||||
retry_error_feedback: Whether to include error feedback in retry prompts (default: True).
|
||||
|
||||
Returns:
|
||||
The parsed output of type T.
|
||||
|
||||
Raises:
|
||||
Exception: If parsing fails after all retry attempts, raises the last parsing error.
|
||||
"""
|
||||
last_error: Exception | None = None
|
||||
last_sanitized_content: str | None = None
|
||||
original_format_instructions = prompt_kwargs.get("format_instructions", "")
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# Create a copy of prompt_kwargs for this attempt
|
||||
attempt_kwargs = prompt_kwargs.copy()
|
||||
|
||||
# On retry, add error feedback to help LLM correct format
|
||||
if attempt > 0 and retry_error_feedback and last_error is not None:
|
||||
# Use sanitized content in error feedback for consistency
|
||||
feedback_content = (
|
||||
last_sanitized_content if last_sanitized_content else ""
|
||||
)
|
||||
error_feedback = (
|
||||
f"\n\nIMPORTANT: The previous attempt failed to parse the response. "
|
||||
f"Error: {str(last_error)}\n"
|
||||
f"Previous response (for reference):\n{feedback_content}\n\n"
|
||||
f"Please ensure your response strictly follows the format instructions "
|
||||
f"and does not contain any control characters or invalid syntax."
|
||||
)
|
||||
# Add error feedback to format_instructions if present
|
||||
if "format_instructions" in attempt_kwargs:
|
||||
attempt_kwargs["format_instructions"] = (
|
||||
original_format_instructions + error_feedback
|
||||
)
|
||||
else:
|
||||
# If no format_instructions, add as a new field
|
||||
attempt_kwargs["parsing_error_feedback"] = error_feedback
|
||||
|
||||
# Call LLM
|
||||
response = await llm_tool(prompt.format_prompt(**attempt_kwargs))
|
||||
content_to_parse = response.content
|
||||
|
||||
parsed = parser.parse(content_to_parse)
|
||||
logger.debug(
|
||||
f"Successfully parsed LLM response on attempt {attempt + 1}/{max_retries}"
|
||||
)
|
||||
return parsed
|
||||
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
logger.warning(
|
||||
f"Failed to parse LLM response on attempt {attempt + 1}/{max_retries}: {str(e)}"
|
||||
)
|
||||
|
||||
# If this was the last attempt, raise the error
|
||||
if attempt == max_retries - 1:
|
||||
logger.error(
|
||||
f"Failed to parse LLM response after {max_retries} attempts. "
|
||||
f"Last error: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
# This should never be reached, but type checker needs it
|
||||
raise RuntimeError("Unexpected error in call_llm_with_retry")
|
||||
82
참고/ontocast-main/ontocast/agent/convert_document.py
Normal file
82
참고/ontocast-main/ontocast/agent/convert_document.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Document conversion agent for OntoCast.
|
||||
|
||||
This module provides functionality for converting various document formats into
|
||||
structured data that can be processed by the OntoCast system.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from ontocast.onto.enum import Status
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def convert_document(state: AgentState, tools: ToolBox) -> AgentState:
|
||||
"""Convert a document into structured data.
|
||||
|
||||
This function takes a document and converts it into a structured format that
|
||||
can be processed by the OntoCast system.
|
||||
|
||||
Args:
|
||||
state: The current agent state containing the document to convert.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
AgentState: Updated state with converted document data.
|
||||
"""
|
||||
logger.debug("Converting documents. NB: processing only one file")
|
||||
|
||||
state.status = Status.SUCCESS
|
||||
files = state.files
|
||||
for filename, file_content in files.items():
|
||||
file_extension = pathlib.Path(filename).suffix.lower()
|
||||
|
||||
# if file_content is None:
|
||||
# try:
|
||||
# with open(filename, "rb") as f:
|
||||
# file_content = f.read()
|
||||
# if file_extension == ".json":
|
||||
# file_content = json.loads(file_content)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Failed to load file {filename}: {str(e)}")
|
||||
# state.status = Status.FAILED
|
||||
# return state
|
||||
logger.debug(f"file ext: {file_extension}, {filename}")
|
||||
if file_extension in tools.converter.supported_extensions:
|
||||
logger.debug("will apply convert :")
|
||||
result = tools.converter(file_content)
|
||||
elif file_extension == ".json":
|
||||
result = json.loads(file_content.decode("utf-8"))
|
||||
|
||||
# Extract user instructions from JSON if present
|
||||
ontology_user_instruction = result.get("ontology_user_instruction", "")
|
||||
facts_user_instruction = result.get("facts_user_instruction", "")
|
||||
|
||||
# Update state with user instructions
|
||||
if ontology_user_instruction:
|
||||
state.ontology_user_instruction = ontology_user_instruction
|
||||
logger.debug(
|
||||
f"Set ontology user instruction: {ontology_user_instruction}"
|
||||
)
|
||||
if facts_user_instruction:
|
||||
state.facts_user_instruction = facts_user_instruction
|
||||
logger.debug(f"Set facts user instruction: {facts_user_instruction}")
|
||||
|
||||
# Extract source URL from JSON if present (for provenance tracking)
|
||||
source_url = result.get("url", None)
|
||||
if source_url:
|
||||
state.source_url = source_url
|
||||
logger.debug(f"Extracted source URL from JSON: {source_url}")
|
||||
|
||||
elif file_extension == ".txt":
|
||||
result = {"text": json.loads(file_content.decode("utf-8"))}
|
||||
else:
|
||||
state.status = Status.FAILED
|
||||
return state
|
||||
|
||||
state.set_text(result["text"])
|
||||
return state
|
||||
134
참고/ontocast-main/ontocast/agent/criticise_facts.py
Normal file
134
참고/ontocast-main/ontocast/agent/criticise_facts.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Enhanced fact criticism agent with memory and SPARQL operations.
|
||||
|
||||
This module provides enhanced functionality for analyzing and validating facts
|
||||
with SPARQL operation support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry
|
||||
from ontocast.onto.enum import FailureStage, Status, WorkflowNode
|
||||
from ontocast.onto.model import FactsCritiqueReport, Suggestions
|
||||
from ontocast.onto.unit_states import UnitFactsState
|
||||
from ontocast.prompt.common import (
|
||||
facts_template,
|
||||
ontology_template,
|
||||
text_template,
|
||||
user_template,
|
||||
)
|
||||
from ontocast.prompt.criticise_facts import (
|
||||
evaluation_instruction,
|
||||
preamble,
|
||||
template_prompt,
|
||||
)
|
||||
from ontocast.tool.atomic import AtomicToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def criticise_facts(
|
||||
state: UnitFactsState, tools: AtomicToolBox
|
||||
) -> UnitFactsState:
|
||||
"""Enhanced criticize facts with SPARQL operations.
|
||||
|
||||
This function performs a critical analysis of the facts in the current content unit,
|
||||
with SPARQL operation support.
|
||||
|
||||
Args:
|
||||
state: The current unit facts state containing the chunk to analyze.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
UnitFactsState: Updated state with analysis results.
|
||||
"""
|
||||
if not state.content_unit:
|
||||
logger.warning("No current content unit to analyze")
|
||||
return state
|
||||
|
||||
progress_info = state.get_content_unit_progress_string()
|
||||
logger.info(
|
||||
f"Facts critic for {progress_info}: visit {state.node_visits[WorkflowNode.CRITICISE_FACTS]}/{state.max_visits_per_node}"
|
||||
)
|
||||
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
parser = PydanticOutputParser(pydantic_object=FactsCritiqueReport)
|
||||
|
||||
ontology_ttl = state.ontology_snapshot.graph.serialize(format="turtle")
|
||||
|
||||
ontology_chapter = ontology_template.format(
|
||||
ontology_ttl=ontology_ttl,
|
||||
)
|
||||
|
||||
facts_ttl = state.content_unit.graph.serialize(format="turtle")
|
||||
|
||||
facts_chapter = facts_template.format(
|
||||
facts_ttl=facts_ttl,
|
||||
)
|
||||
|
||||
text_chapter = text_template.format(text=state.content_unit.text)
|
||||
|
||||
user_instruction = (
|
||||
user_template.format(user_instruction=state.facts_user_instruction)
|
||||
if state.facts_user_instruction
|
||||
else ""
|
||||
)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"preamble",
|
||||
"evaluation_instruction",
|
||||
"user_instruction",
|
||||
"ontology_chapter",
|
||||
"facts_chapter",
|
||||
"text_chapter",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
|
||||
prompt_data = {
|
||||
"preamble": preamble,
|
||||
"evaluation_instruction": evaluation_instruction,
|
||||
"user_instruction": user_instruction,
|
||||
"ontology_chapter": ontology_chapter,
|
||||
"facts_chapter": facts_chapter,
|
||||
"text_chapter": text_chapter,
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
}
|
||||
|
||||
try:
|
||||
critique: FactsCritiqueReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs=prompt_data,
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.CRITICISE_FACTS, critique.external_evidence_request
|
||||
)
|
||||
logger.debug(
|
||||
f"Parsed critique report - success: {critique.success}, "
|
||||
f"score: {critique.score}"
|
||||
)
|
||||
|
||||
if critique.success or critique.score > 90:
|
||||
state.status = Status.SUCCESS
|
||||
state.set_node_status(WorkflowNode.CRITICISE_FACTS, Status.SUCCESS)
|
||||
logger.info("Facts critique passed")
|
||||
else:
|
||||
state.status = Status.FAILED
|
||||
state.set_node_status(WorkflowNode.CRITICISE_FACTS, Status.FAILED)
|
||||
state.failure_stage = FailureStage.FACTS_CRITIQUE
|
||||
state.suggestions = Suggestions.from_critique_report(critique)
|
||||
state.failure_reason = "Facts Critic suggests improvements"
|
||||
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to criticize facts: {str(e)}")
|
||||
state.set_failure(FailureStage.FACTS_CRITIQUE, str(e))
|
||||
state.set_node_status(WorkflowNode.CRITICISE_FACTS, Status.FAILED)
|
||||
return state
|
||||
132
참고/ontocast-main/ontocast/agent/criticise_ontology.py
Normal file
132
참고/ontocast-main/ontocast/agent/criticise_ontology.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Enhanced ontology criticism agent with SPARQL operations.
|
||||
|
||||
This module provides enhanced functionality for analyzing and validating ontologies of previous critiques and SPARQL operation support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry
|
||||
from ontocast.onto.enum import FailureStage, Status, WorkflowNode
|
||||
from ontocast.onto.model import OntologyCritiqueReport, Suggestions
|
||||
from ontocast.onto.unit_states import UnitOntologyState
|
||||
from ontocast.prompt.common import ontology_template, text_template
|
||||
from ontocast.prompt.common import system_preamble_ontology as system_preamble
|
||||
from ontocast.prompt.criticise_ontology import (
|
||||
intro_instruction,
|
||||
ontology_criteria,
|
||||
template_prompt,
|
||||
)
|
||||
from ontocast.tool import LLMTool
|
||||
from ontocast.tool.atomic import AtomicToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def criticise_ontology(
|
||||
state: UnitOntologyState, tools: AtomicToolBox
|
||||
) -> UnitOntologyState:
|
||||
"""Enhanced ontology criticism with SPARQL operations.
|
||||
|
||||
This function performs a critical analysis of the ontology in the current
|
||||
state, with SPARQL operation support.
|
||||
|
||||
Args:
|
||||
state: The current unit ontology state containing the ontology to analyze.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
UnitOntologyState: Updated state with analysis results.
|
||||
"""
|
||||
|
||||
progress_info = state.get_content_unit_progress_string()
|
||||
logger.info(
|
||||
f"Ontology Critic for {progress_info}: visit {state.node_visits[WorkflowNode.CRITICISE_ONTOLOGY]}/{state.max_visits_per_node}"
|
||||
)
|
||||
|
||||
if state.content_unit is None:
|
||||
state.status = Status.FAILED
|
||||
return state
|
||||
|
||||
current = state.current_ontology or state.ontology_snapshot
|
||||
if current.is_null():
|
||||
raise ValueError(
|
||||
f"Null ontology cannot be criticised: {current.iri} is not a valid ontology"
|
||||
)
|
||||
|
||||
parser = PydanticOutputParser(pydantic_object=OntologyCritiqueReport)
|
||||
llm_tool: LLMTool = await tools.get_llm_tool(state.budget_tracker)
|
||||
|
||||
ontology_ttl = current.graph.serialize(format="turtle")
|
||||
|
||||
ontology_chapter = ontology_template.format(
|
||||
ontology_ttl=ontology_ttl,
|
||||
)
|
||||
|
||||
text_chapter = text_template.format(text=state.content_unit.text)
|
||||
|
||||
user_instruction = state.ontology_user_instruction
|
||||
external_evidence = state.external_evidence_text
|
||||
if external_evidence:
|
||||
state.mark_external_evidence_used(WorkflowNode.CRITICISE_ONTOLOGY)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"preamble",
|
||||
"intro_instruction",
|
||||
"ontology_criteria",
|
||||
"user_instruction",
|
||||
"ontology_chapter",
|
||||
"text_chapter",
|
||||
"external_evidence",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
critique: OntologyCritiqueReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"preamble": system_preamble,
|
||||
"intro_instruction": intro_instruction,
|
||||
"ontology_criteria": ontology_criteria,
|
||||
"text_chapter": text_chapter,
|
||||
"user_instruction": user_instruction,
|
||||
"ontology_chapter": ontology_chapter,
|
||||
"external_evidence": external_evidence,
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
},
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.CRITICISE_ONTOLOGY, critique.external_evidence_request
|
||||
)
|
||||
logger.info(
|
||||
f"Parsed critique report - success: {critique.success}, "
|
||||
f"score: {critique.score}, n fixes: {len(critique.actionable_ontology_fixes)}."
|
||||
)
|
||||
|
||||
if critique.success or critique.score > 90:
|
||||
state.status = Status.SUCCESS
|
||||
state.set_node_status(WorkflowNode.CRITICISE_ONTOLOGY, Status.SUCCESS)
|
||||
logger.info("Ontology critique passed")
|
||||
else:
|
||||
state.status = Status.FAILED
|
||||
state.failure_stage = FailureStage.ONTOLOGY_CRITIQUE
|
||||
state.set_node_status(WorkflowNode.CRITICISE_ONTOLOGY, Status.FAILED)
|
||||
state.suggestions = Suggestions.from_critique_report(critique)
|
||||
state.failure_reason = "Ontology Critic suggests improvements"
|
||||
logger.info(
|
||||
f"Ontology critique failed: {critique.systemic_critique_summary}"
|
||||
)
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to critique ontology: {str(e)}")
|
||||
state.set_failure(FailureStage.ONTOLOGY_CRITIQUE, str(e))
|
||||
state.set_node_status(WorkflowNode.CRITICISE_ONTOLOGY, Status.FAILED)
|
||||
return state
|
||||
448
참고/ontocast-main/ontocast/agent/external_evidence.py
Normal file
448
참고/ontocast-main/ontocast/agent/external_evidence.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""Helpers for optional web-grounded prompts with explicit plan/fetch steps."""
|
||||
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry
|
||||
from ontocast.onto.enum import WorkflowNode
|
||||
from ontocast.onto.model import (
|
||||
ExternalEvidenceCacheEntry,
|
||||
ExternalEvidenceHit,
|
||||
ExternalEvidencePlan,
|
||||
ExternalEvidenceRequest,
|
||||
)
|
||||
from ontocast.onto.unit_states import UnitFactsState, UnitOntologyState
|
||||
from ontocast.tool.atomic import AtomicToolBox, SearchHit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
UnitStateT = TypeVar("UnitStateT", UnitFactsState, UnitOntologyState)
|
||||
|
||||
_planner_template = """
|
||||
You are planning optional web-search grounding for a knowledge-graph workflow.
|
||||
Decide conservatively whether external web evidence is necessary.
|
||||
|
||||
Target workflow node:
|
||||
{target_node}
|
||||
|
||||
Source text:
|
||||
{content_text}
|
||||
|
||||
User instruction:
|
||||
{user_instruction}
|
||||
|
||||
Node request rationale:
|
||||
{search_rationale}
|
||||
|
||||
Node query hints:
|
||||
{query_hints}
|
||||
|
||||
Rules:
|
||||
1. Prefer NOT searching unless there is genuine ambiguity, domain-standard uncertainty,
|
||||
or term disambiguation need.
|
||||
2. If searching, propose short, focused queries, not broad summaries of the entire text.
|
||||
3. Never propose more than {max_queries} queries.
|
||||
4. If no search is needed, set should_search=false, intent=\"none\", and queries=[].
|
||||
|
||||
{format_instructions}
|
||||
"""
|
||||
|
||||
|
||||
def _get_int(tools: AtomicToolBox, key: str, default: int) -> int:
|
||||
value = getattr(tools, key, default)
|
||||
return int(value) if isinstance(value, int | float) else default
|
||||
|
||||
|
||||
def _get_float(tools: AtomicToolBox, key: str, default: float) -> float:
|
||||
value = getattr(tools, key, default)
|
||||
return float(value) if isinstance(value, int | float) else default
|
||||
|
||||
|
||||
def _get_bool(tools: AtomicToolBox, key: str, default: bool) -> bool:
|
||||
value = getattr(tools, key, default)
|
||||
return bool(value) if isinstance(value, bool) else default
|
||||
|
||||
|
||||
def _get_set(tools: AtomicToolBox, key: str) -> set[str]:
|
||||
value = getattr(tools, key, set())
|
||||
if isinstance(value, set):
|
||||
return {str(entry).strip().lower() for entry in value if str(entry).strip()}
|
||||
return set()
|
||||
|
||||
|
||||
def _web_grounding_enabled_for_node(
|
||||
tools: AtomicToolBox, target_node: WorkflowNode
|
||||
) -> bool:
|
||||
checker = getattr(tools, "web_grounding_enabled_for_node", None)
|
||||
if checker is None or not callable(checker):
|
||||
return False
|
||||
return bool(checker(target_node))
|
||||
|
||||
|
||||
def build_evidence_query(
|
||||
content_text: str, user_instruction: str, max_chars: int = 220
|
||||
) -> str:
|
||||
"""Backward-compatible fallback query from content and user guidance."""
|
||||
source = user_instruction.strip() if user_instruction.strip() else content_text
|
||||
query = " ".join(source.split())
|
||||
return query[:max_chars].strip()
|
||||
|
||||
|
||||
def _resolve_user_instruction(state: UnitFactsState | UnitOntologyState) -> str:
|
||||
if isinstance(state, UnitOntologyState):
|
||||
return state.ontology_user_instruction
|
||||
return state.facts_user_instruction
|
||||
|
||||
|
||||
def _resolve_content_text(state: UnitFactsState | UnitOntologyState) -> str:
|
||||
return state.content_unit.text
|
||||
|
||||
|
||||
def _resolve_search_request(
|
||||
state: UnitFactsState | UnitOntologyState, target_node: WorkflowNode
|
||||
) -> ExternalEvidenceRequest:
|
||||
return state.get_external_evidence_request(target_node)
|
||||
|
||||
|
||||
def _normalize_query(query: str) -> str:
|
||||
return " ".join(query.split()).strip()
|
||||
|
||||
|
||||
def _extract_domain(url: str) -> str:
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc.lower()
|
||||
if domain.startswith("www."):
|
||||
return domain[4:]
|
||||
return domain
|
||||
|
||||
|
||||
def _domain_matches(domain: str, patterns: set[str]) -> bool:
|
||||
for pattern in patterns:
|
||||
if domain == pattern or domain.endswith(f".{pattern}"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_external_evidence_plan(
|
||||
plan: ExternalEvidencePlan, tools: AtomicToolBox
|
||||
) -> ExternalEvidencePlan:
|
||||
"""Apply deterministic guardrails to planner output."""
|
||||
deduped_queries: list[str] = []
|
||||
seen_queries: set[str] = set()
|
||||
min_chars = max(3, _get_int(tools, "web_search_planner_min_query_chars", 12))
|
||||
for raw_query in plan.queries:
|
||||
query = _normalize_query(raw_query)
|
||||
if len(query) < min_chars:
|
||||
continue
|
||||
alpha_chars = sum(1 for char in query if char.isalpha())
|
||||
if alpha_chars < max(4, min_chars // 2):
|
||||
continue
|
||||
lowered = query.lower()
|
||||
if lowered in seen_queries:
|
||||
continue
|
||||
deduped_queries.append(query)
|
||||
seen_queries.add(lowered)
|
||||
|
||||
max_queries = max(1, _get_int(tools, "web_search_planner_max_queries", 3))
|
||||
min_confidence = _get_float(tools, "web_search_planner_min_confidence", 0.35)
|
||||
deduped_queries = deduped_queries[:max_queries]
|
||||
should_search = (
|
||||
plan.should_search
|
||||
and plan.intent != "none"
|
||||
and plan.confidence >= min_confidence
|
||||
and len(deduped_queries) > 0
|
||||
)
|
||||
return ExternalEvidencePlan(
|
||||
should_search=should_search,
|
||||
rationale=plan.rationale,
|
||||
intent=plan.intent if should_search else "none",
|
||||
confidence=plan.confidence,
|
||||
queries=deduped_queries if should_search else [],
|
||||
)
|
||||
|
||||
|
||||
def normalize_search_hits(
|
||||
hits: list[SearchHit], tools: AtomicToolBox
|
||||
) -> list[ExternalEvidenceHit]:
|
||||
"""Filter and normalize search hits with deterministic quality checks."""
|
||||
normalized_hits: list[ExternalEvidenceHit] = []
|
||||
seen_urls: set[str] = set()
|
||||
allowed_domains = _get_set(tools, "web_search_allowed_domains")
|
||||
blocked_domains = _get_set(tools, "web_search_blocked_domains")
|
||||
min_snippet_chars = max(0, _get_int(tools, "web_search_min_snippet_chars", 40))
|
||||
|
||||
for hit in hits:
|
||||
url = hit.url.strip()
|
||||
if not url or url in seen_urls:
|
||||
continue
|
||||
domain = _extract_domain(url)
|
||||
if not domain:
|
||||
continue
|
||||
if blocked_domains and _domain_matches(domain, blocked_domains):
|
||||
continue
|
||||
if allowed_domains and not _domain_matches(domain, allowed_domains):
|
||||
continue
|
||||
|
||||
snippet = " ".join(hit.snippet.split()).strip()
|
||||
if len(snippet) < min_snippet_chars:
|
||||
continue
|
||||
|
||||
seen_urls.add(url)
|
||||
normalized_hits.append(
|
||||
ExternalEvidenceHit(
|
||||
title=hit.title.strip() or url,
|
||||
url=url,
|
||||
snippet=snippet,
|
||||
domain=domain,
|
||||
)
|
||||
)
|
||||
|
||||
return normalized_hits
|
||||
|
||||
|
||||
async def plan_external_evidence_for_node(
|
||||
state: UnitStateT, tools: AtomicToolBox, target_node: WorkflowNode
|
||||
) -> UnitStateT:
|
||||
"""Plan evidence retrieval for a workflow node using LLM + guardrails."""
|
||||
state.node_visits[WorkflowNode.PLAN_EXTERNAL_EVIDENCE] += 1
|
||||
|
||||
if not _web_grounding_enabled_for_node(tools, target_node):
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node, ExternalEvidenceCacheEntry()
|
||||
)
|
||||
state.external_evidence_hits = []
|
||||
state.external_evidence_text = ""
|
||||
state.external_evidence_source_count = 0
|
||||
state.external_evidence_domains = []
|
||||
return state
|
||||
|
||||
request = _resolve_search_request(state, target_node)
|
||||
if not request.initiate_search:
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node, ExternalEvidenceCacheEntry()
|
||||
)
|
||||
state.external_evidence_hits = []
|
||||
state.external_evidence_text = ""
|
||||
state.external_evidence_source_count = 0
|
||||
state.external_evidence_domains = []
|
||||
state.external_evidence_planned_at_node = target_node
|
||||
return state
|
||||
|
||||
cached_entry = state.get_external_evidence_cache_entry(target_node)
|
||||
if (
|
||||
_get_bool(tools, "web_search_reuse_evidence_across_attempt", True)
|
||||
and cached_entry.text
|
||||
and cached_entry.plan.should_search
|
||||
):
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
user_instruction = _resolve_user_instruction(state)
|
||||
content_text = _resolve_content_text(state)
|
||||
|
||||
if not _get_bool(tools, "web_search_planner_enabled", True):
|
||||
fallback_query = build_evidence_query(
|
||||
content_text=content_text, user_instruction=user_instruction
|
||||
)
|
||||
fallback_plan = ExternalEvidencePlan(
|
||||
should_search=bool(fallback_query) or bool(request.query_hints),
|
||||
rationale="Planner disabled; fallback query from content/instruction.",
|
||||
intent="background",
|
||||
confidence=1.0,
|
||||
queries=[
|
||||
*([fallback_query] if fallback_query else []),
|
||||
*request.query_hints,
|
||||
],
|
||||
)
|
||||
sanitized_plan = sanitize_external_evidence_plan(fallback_plan, tools)
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node,
|
||||
ExternalEvidenceCacheEntry(
|
||||
plan=sanitized_plan,
|
||||
hits=[],
|
||||
text="",
|
||||
source_count=0,
|
||||
domains=[],
|
||||
),
|
||||
)
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
parser = PydanticOutputParser(pydantic_object=ExternalEvidencePlan)
|
||||
prompt = PromptTemplate(
|
||||
template=_planner_template,
|
||||
input_variables=[
|
||||
"target_node",
|
||||
"content_text",
|
||||
"user_instruction",
|
||||
"max_queries",
|
||||
"search_rationale",
|
||||
"query_hints",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
try:
|
||||
planned: ExternalEvidencePlan = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"target_node": target_node.value,
|
||||
"content_text": content_text,
|
||||
"user_instruction": user_instruction,
|
||||
"max_queries": str(
|
||||
max(1, _get_int(tools, "web_search_planner_max_queries", 3))
|
||||
),
|
||||
"search_rationale": request.rationale or "none",
|
||||
"query_hints": (
|
||||
"\n".join(f"- {hint}" for hint in request.query_hints)
|
||||
if request.query_hints
|
||||
else "none"
|
||||
),
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
},
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
"Evidence planner failed for %s; skipping external evidence (%s).",
|
||||
target_node.value,
|
||||
str(error),
|
||||
)
|
||||
planned = ExternalEvidencePlan(
|
||||
should_search=False,
|
||||
rationale="Planner failure fallback: skip search.",
|
||||
intent="none",
|
||||
confidence=0.0,
|
||||
queries=[],
|
||||
)
|
||||
|
||||
merged_plan = ExternalEvidencePlan(
|
||||
should_search=planned.should_search or bool(request.query_hints),
|
||||
rationale=planned.rationale or request.rationale,
|
||||
intent=planned.intent,
|
||||
confidence=max(planned.confidence, request.confidence),
|
||||
queries=[*planned.queries, *request.query_hints],
|
||||
)
|
||||
sanitized_plan = sanitize_external_evidence_plan(merged_plan, tools)
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node,
|
||||
ExternalEvidenceCacheEntry(
|
||||
plan=sanitized_plan,
|
||||
hits=[],
|
||||
text="",
|
||||
source_count=0,
|
||||
domains=[],
|
||||
),
|
||||
)
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
|
||||
async def fetch_external_evidence_for_node(
|
||||
state: UnitStateT, tools: AtomicToolBox, target_node: WorkflowNode
|
||||
) -> UnitStateT:
|
||||
"""Fetch and render evidence for a previously planned workflow node."""
|
||||
state.node_visits[WorkflowNode.FETCH_EXTERNAL_EVIDENCE] += 1
|
||||
|
||||
if not _web_grounding_enabled_for_node(tools, target_node):
|
||||
state.external_evidence_hits = []
|
||||
state.external_evidence_text = ""
|
||||
state.external_evidence_source_count = 0
|
||||
state.external_evidence_domains = []
|
||||
return state
|
||||
|
||||
request = _resolve_search_request(state, target_node)
|
||||
if not request.initiate_search:
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node, ExternalEvidenceCacheEntry()
|
||||
)
|
||||
state.external_evidence_hits = []
|
||||
state.external_evidence_text = ""
|
||||
state.external_evidence_source_count = 0
|
||||
state.external_evidence_domains = []
|
||||
state.external_evidence_planned_at_node = target_node
|
||||
return state
|
||||
|
||||
cache_entry = state.get_external_evidence_cache_entry(target_node)
|
||||
plan = cache_entry.plan
|
||||
if (
|
||||
_get_bool(tools, "web_search_reuse_evidence_across_attempt", True)
|
||||
and cache_entry.text
|
||||
and plan.should_search
|
||||
):
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
if not plan.should_search or not plan.queries:
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node, ExternalEvidenceCacheEntry()
|
||||
)
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
combined_hits: list[SearchHit] = []
|
||||
for query in plan.queries:
|
||||
search_hits = await tools.search(query)
|
||||
combined_hits.extend(search_hits)
|
||||
|
||||
normalized_hits = normalize_search_hits(combined_hits, tools)
|
||||
evidence_text = render_external_evidence(
|
||||
hits=normalized_hits,
|
||||
max_snippet_chars=max(40, _get_int(tools, "web_search_max_snippet_chars", 400)),
|
||||
max_total_chars=max(200, _get_int(tools, "web_search_max_total_chars", 1800)),
|
||||
)
|
||||
state.set_external_evidence_cache_entry(
|
||||
target_node,
|
||||
ExternalEvidenceCacheEntry(
|
||||
plan=plan,
|
||||
hits=normalized_hits,
|
||||
text=evidence_text,
|
||||
source_count=len(normalized_hits),
|
||||
domains=sorted({hit.domain for hit in normalized_hits}),
|
||||
),
|
||||
)
|
||||
state.load_external_evidence_for_node(target_node)
|
||||
return state
|
||||
|
||||
|
||||
def render_external_evidence(
|
||||
hits: list[ExternalEvidenceHit],
|
||||
max_snippet_chars: int,
|
||||
max_total_chars: int,
|
||||
) -> str:
|
||||
"""Render bounded external evidence as a prompt chapter."""
|
||||
if not hits:
|
||||
return ""
|
||||
|
||||
rendered_lines: list[str] = []
|
||||
remaining_chars = max_total_chars
|
||||
for index, hit in enumerate(hits, start=1):
|
||||
clean_snippet = " ".join(hit.snippet.split())
|
||||
if len(clean_snippet) > max_snippet_chars:
|
||||
clean_snippet = f"{clean_snippet[: max_snippet_chars - 3]}..."
|
||||
|
||||
line = f"{index}. {hit.title} | {hit.url}\n {clean_snippet}"
|
||||
if len(line) > remaining_chars:
|
||||
if remaining_chars < 80:
|
||||
break
|
||||
truncated = line[: remaining_chars - 3].rstrip()
|
||||
line = f"{truncated}..."
|
||||
rendered_lines.append(line)
|
||||
break
|
||||
|
||||
rendered_lines.append(line)
|
||||
remaining_chars -= len(line)
|
||||
|
||||
if not rendered_lines:
|
||||
return ""
|
||||
|
||||
return (
|
||||
"### EXTERNAL EVIDENCE (WEB SEARCH)\n"
|
||||
"Use these sources to clarify uncertain terms or standards only.\n"
|
||||
"When evidence conflicts, prioritize the source text and ontology context.\n\n"
|
||||
f"{chr(10).join(rendered_lines)}"
|
||||
)
|
||||
187
참고/ontocast-main/ontocast/agent/normalize_ontology.py
Normal file
187
참고/ontocast-main/ontocast/agent/normalize_ontology.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Reducers for parallel map/reduce workflow outputs."""
|
||||
|
||||
import logging
|
||||
|
||||
from rdflib import OWL, RDF, BNode, Node, URIRef
|
||||
|
||||
from ontocast.onto.constants import PROV, RDF_REIFIES, SCHEMA
|
||||
from ontocast.onto.content_unit import ContentUnit
|
||||
from ontocast.onto.ontology import Ontology
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.sparql_models import GraphUpdate, TripleOp
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def split_ontology_and_provenance_graph(
|
||||
graph: RDFGraph,
|
||||
) -> tuple[RDFGraph, RDFGraph]:
|
||||
"""Split normalized ontology graph into clean ontology + provenance artifact.
|
||||
|
||||
Provenance/reification and normalization-time alignment artifacts are moved
|
||||
to a side graph so downstream consolidation works with a clean ontology graph.
|
||||
"""
|
||||
clean_graph = RDFGraph()
|
||||
provenance_graph = RDFGraph()
|
||||
|
||||
for prefix, namespace in graph.namespaces():
|
||||
if prefix:
|
||||
clean_graph.bind(prefix, namespace)
|
||||
provenance_graph.bind(prefix, namespace)
|
||||
|
||||
reifier_nodes: set[BNode] = {
|
||||
subject
|
||||
for subject, _, _ in graph.triples((None, RDF_REIFIES, None))
|
||||
if isinstance(subject, BNode)
|
||||
}
|
||||
chunk_nodes: set[Node] = set()
|
||||
|
||||
def is_schema_chunk_metadata(predicate: Node) -> bool:
|
||||
predicate_str = str(predicate)
|
||||
return predicate_str in {
|
||||
str(SCHEMA.identifier),
|
||||
str(SCHEMA.position),
|
||||
"http://schema.org/identifier",
|
||||
"http://schema.org/position",
|
||||
}
|
||||
|
||||
for subject, predicate, obj in graph:
|
||||
if is_schema_chunk_metadata(predicate) or predicate == PROV.generatedAtTime:
|
||||
chunk_nodes.add(subject)
|
||||
if predicate == RDF.type and str(obj) in {
|
||||
str(PROV.Entity),
|
||||
str(SCHEMA.text),
|
||||
"http://schema.org/text",
|
||||
}:
|
||||
chunk_nodes.add(subject)
|
||||
|
||||
def is_provenance_or_alignment_triple(
|
||||
subject: Node, predicate: Node, obj: Node
|
||||
) -> bool:
|
||||
if predicate == RDF_REIFIES:
|
||||
return True
|
||||
if predicate == PROV.wasDerivedFrom:
|
||||
# Keep ontology lineage hashes in the clean ontology graph.
|
||||
if isinstance(obj, URIRef) and str(obj).startswith("urn:hash:"):
|
||||
return False
|
||||
return True
|
||||
if predicate == PROV.generatedAtTime or is_schema_chunk_metadata(predicate):
|
||||
return True
|
||||
if predicate == OWL.sameAs:
|
||||
return True
|
||||
if subject in reifier_nodes or obj in reifier_nodes:
|
||||
return True
|
||||
if subject in chunk_nodes or obj in chunk_nodes:
|
||||
return True
|
||||
if predicate == RDF.type and str(obj) in {
|
||||
str(PROV.Entity),
|
||||
str(SCHEMA.text),
|
||||
"http://schema.org/text",
|
||||
}:
|
||||
return True
|
||||
return False
|
||||
|
||||
for triple in graph:
|
||||
if is_provenance_or_alignment_triple(*triple):
|
||||
provenance_graph.add(triple)
|
||||
else:
|
||||
clean_graph.add(triple)
|
||||
|
||||
return clean_graph, provenance_graph
|
||||
|
||||
|
||||
def normalize_ontology_units(
|
||||
units: list[ContentUnit],
|
||||
tools: ToolBox,
|
||||
base_ontology: Ontology | None = None,
|
||||
require_base: bool = False,
|
||||
) -> tuple[Ontology, list[GraphUpdate], RDFGraph]:
|
||||
"""Merge ontology unit deltas as TripleOps, then apply to base ontology.
|
||||
|
||||
Units contain ontology delta graphs (insert triples only). To preserve the
|
||||
exact unit output shape (and avoid ontology/facts aggregation rewrites), we
|
||||
convert each unit graph into an ``insert`` TripleOp and apply them as one
|
||||
GraphUpdate.
|
||||
|
||||
Args:
|
||||
units: ContentUnits with type=ONTOLOGIES and delta graph from each unit.
|
||||
tools: ToolBox instance.
|
||||
base_ontology: Optional ontology to use as base; merged delta is applied to it.
|
||||
require_base: Whether map/reduce caller expects a base ontology.
|
||||
|
||||
Returns:
|
||||
Tuple of (
|
||||
ontology with cleaned graph,
|
||||
list of applied GraphUpdates for versioning,
|
||||
provenance artifact graph stripped from ontology output,
|
||||
).
|
||||
"""
|
||||
if not units:
|
||||
if base_ontology is not None:
|
||||
return base_ontology, [], RDFGraph()
|
||||
return Ontology(graph=RDFGraph()), [], RDFGraph()
|
||||
|
||||
for unit in units:
|
||||
unit.sanitize()
|
||||
_ = tools
|
||||
|
||||
if require_base and (base_ontology is None or base_ontology.is_null()):
|
||||
logger.warning(
|
||||
"normalize_ontology_units expected a base ontology but none was available; "
|
||||
"continuing with merged aggregated ontology output."
|
||||
)
|
||||
|
||||
merged_update = GraphUpdate(
|
||||
triple_operations=[
|
||||
TripleOp(type="insert", graph=unit.graph)
|
||||
for unit in units
|
||||
if len(unit.graph) > 0
|
||||
]
|
||||
)
|
||||
if not merged_update.triple_operations:
|
||||
merged_update = None
|
||||
|
||||
if base_ontology is not None and not base_ontology.is_null():
|
||||
base_graph = base_ontology.graph
|
||||
if merged_update is not None:
|
||||
updated_graph, _ = AgentState.render_updated_graph(
|
||||
base_graph, [merged_update], max_triples=None
|
||||
)
|
||||
graph_changed = set(updated_graph) != set(base_graph)
|
||||
if graph_changed:
|
||||
result = base_ontology.derive_updated_version(updated_graph)
|
||||
else:
|
||||
result = base_ontology.model_copy(deep=True)
|
||||
result.graph = updated_graph
|
||||
else:
|
||||
result = base_ontology.model_copy(deep=True)
|
||||
result.sync_properties_to_graph()
|
||||
cleaned_graph, provenance_graph = split_ontology_and_provenance_graph(
|
||||
result.graph
|
||||
)
|
||||
result.graph = cleaned_graph
|
||||
result.sync_properties_to_graph()
|
||||
applied = [merged_update] if merged_update else []
|
||||
return result, applied, provenance_graph
|
||||
|
||||
aggregated_delta = RDFGraph()
|
||||
for unit in units:
|
||||
for triple in unit.graph:
|
||||
aggregated_delta.add(triple)
|
||||
for prefix, namespace in unit.graph.namespaces():
|
||||
if prefix:
|
||||
aggregated_delta.bind(prefix, namespace)
|
||||
|
||||
cleaned_graph, provenance_graph = split_ontology_and_provenance_graph(
|
||||
aggregated_delta
|
||||
)
|
||||
result = Ontology(
|
||||
graph=cleaned_graph,
|
||||
ontology_id=base_ontology.ontology_id if base_ontology else None,
|
||||
title=base_ontology.title if base_ontology else None,
|
||||
description=base_ontology.description if base_ontology else None,
|
||||
)
|
||||
applied = [merged_update] if merged_update else []
|
||||
return result, applied, provenance_graph
|
||||
294
참고/ontocast-main/ontocast/agent/render_facts.py
Normal file
294
참고/ontocast-main/ontocast/agent/render_facts.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""Fact rendering agent for OntoCast.
|
||||
|
||||
This module provides functionality for rendering facts from RDF graphs into
|
||||
human-readable formats, making the extracted knowledge more accessible and
|
||||
understandable.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry, render_suggestions_prompt
|
||||
from ontocast.onto.constants import DEFAULT_IRI
|
||||
from ontocast.onto.enum import FailureStage, Status, WorkflowNode
|
||||
from ontocast.onto.model import FactsRenderReport, GraphUpdateRenderReport
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.unit_states import UnitFactsState
|
||||
from ontocast.prompt.common import (
|
||||
facts_template,
|
||||
ontology_template,
|
||||
output_instruction_empty,
|
||||
output_instruction_sparql,
|
||||
text_template,
|
||||
user_template,
|
||||
)
|
||||
from ontocast.prompt.render_facts import (
|
||||
facts_instruction_template,
|
||||
preamble,
|
||||
template_prompt,
|
||||
)
|
||||
from ontocast.tool.atomic import AtomicToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _extract_known_prefixes(state: UnitFactsState) -> dict[str, str]:
|
||||
"""Extract ontology prefixes used to patch missing declarations in LLM TTL output."""
|
||||
known_prefixes: dict[str, str] = {}
|
||||
|
||||
if state.ontology_snapshot and state.ontology_snapshot.graph:
|
||||
for prefix, namespace_uri in state.ontology_snapshot.graph.namespaces():
|
||||
if prefix: # Skip empty prefixes
|
||||
known_prefixes[prefix] = str(namespace_uri)
|
||||
|
||||
# Also add the ontology prefix explicitly if available.
|
||||
if state.ontology_snapshot.prefix and state.ontology_snapshot.namespace:
|
||||
known_prefixes[state.ontology_snapshot.prefix] = (
|
||||
state.ontology_snapshot.namespace
|
||||
)
|
||||
|
||||
return known_prefixes
|
||||
|
||||
|
||||
async def render_facts(state: UnitFactsState, tools: AtomicToolBox) -> UnitFactsState:
|
||||
"""Structured hybrid facts renderer with Turtle/SPARQL decision logic.
|
||||
|
||||
This function decides between generating bare Turtle for fresh facts
|
||||
and SPARQL operations for updates based on whether facts exist.
|
||||
|
||||
Args:
|
||||
state: The current unit facts state
|
||||
tools: The toolbox containing necessary tools
|
||||
|
||||
Returns:
|
||||
UnitFactsState: Updated state with rendered facts
|
||||
"""
|
||||
|
||||
is_fresh_facts_graph = len(state.content_unit.graph) == 0
|
||||
|
||||
progress_info = state.get_content_unit_progress_string()
|
||||
logger.info(f"Render facts for {progress_info}")
|
||||
|
||||
if is_fresh_facts_graph:
|
||||
logger.info("Generating fresh facts as Turtle")
|
||||
return await render_facts_fresh(state, tools)
|
||||
else:
|
||||
logger.info("Generating facts update")
|
||||
return await render_facts_update(state, tools)
|
||||
|
||||
|
||||
def _prepare_prompt_data(state: UnitFactsState) -> dict[str, str]:
|
||||
"""Prepare common prompt data for both fresh and update rendering.
|
||||
|
||||
Args:
|
||||
state: The current unit facts state
|
||||
|
||||
Returns:
|
||||
Dictionary containing formatted prompt components
|
||||
"""
|
||||
ontology_chapter = ontology_template.format(
|
||||
ontology_ttl=state.ontology_snapshot.graph.serialize(format="turtle")
|
||||
)
|
||||
|
||||
facts_instruction_str = facts_instruction_template.format(
|
||||
ontology_namespace=state.ontology_snapshot.namespace,
|
||||
ontology_prefix=state.ontology_snapshot.prefix,
|
||||
facts_namespace=DEFAULT_IRI,
|
||||
)
|
||||
|
||||
text_chapter = text_template.format(text=state.content_unit.text)
|
||||
|
||||
fact_chapter = ""
|
||||
|
||||
user_instruction = (
|
||||
user_template.format(user_instruction=state.facts_user_instruction)
|
||||
if state.facts_user_instruction
|
||||
else ""
|
||||
)
|
||||
|
||||
return {
|
||||
"ontology_chapter": ontology_chapter,
|
||||
"user_instruction": user_instruction,
|
||||
"facts_instruction": facts_instruction_str,
|
||||
"text_chapter": text_chapter,
|
||||
"fact_chapter": fact_chapter,
|
||||
}
|
||||
|
||||
|
||||
def _create_prompt_template() -> PromptTemplate:
|
||||
"""Create the common prompt template used by both rendering functions.
|
||||
|
||||
Returns:
|
||||
Configured PromptTemplate instance
|
||||
"""
|
||||
return PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"preamble",
|
||||
"facts_instruction",
|
||||
"user_instruction",
|
||||
"ontology_chapter",
|
||||
"text_chapter",
|
||||
"improvement_instruction",
|
||||
"output_instruction",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _handle_rendering_error(
|
||||
state: UnitFactsState, error: Exception, stage: FailureStage
|
||||
) -> UnitFactsState:
|
||||
"""Handle rendering errors consistently.
|
||||
|
||||
Args:
|
||||
state: The current agent state
|
||||
error: The exception that occurred
|
||||
stage: The failure stage to set
|
||||
|
||||
Returns:
|
||||
Updated state with failure information
|
||||
"""
|
||||
logger.error(f"Failed to generate triples: {str(error)}")
|
||||
state.set_failure(stage, str(error))
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_FACTS, Status.FAILED)
|
||||
return state
|
||||
|
||||
|
||||
async def render_facts_fresh(
|
||||
state: UnitFactsState, tools: AtomicToolBox
|
||||
) -> UnitFactsState:
|
||||
"""Render fresh facts from the current chunk into Turtle format.
|
||||
|
||||
Args:
|
||||
state: The current unit facts state containing the chunk to render.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
UnitFactsState: Updated state with rendered facts.
|
||||
"""
|
||||
logger.info("Rendering fresh facts")
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
parser = PydanticOutputParser(pydantic_object=FactsRenderReport)
|
||||
|
||||
known_prefixes = _extract_known_prefixes(state)
|
||||
|
||||
prompt_data = _prepare_prompt_data(state)
|
||||
prompt_data_fresh = {
|
||||
"preamble": preamble,
|
||||
"improvement_instruction": "",
|
||||
"output_instruction": output_instruction_empty,
|
||||
}
|
||||
prompt_data.update(prompt_data_fresh)
|
||||
|
||||
prompt = _create_prompt_template()
|
||||
|
||||
try:
|
||||
# Set known prefixes in context before parsing
|
||||
RDFGraph.set_known_prefixes(known_prefixes if known_prefixes else None)
|
||||
|
||||
render_report: FactsRenderReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
**prompt_data,
|
||||
},
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_FACTS, render_report.external_evidence_request
|
||||
)
|
||||
facts_report = render_report.facts_report
|
||||
facts_report.semantic_graph.sanitize_prefixes_namespaces()
|
||||
state.content_unit.graph = facts_report.semantic_graph
|
||||
|
||||
# Track triples in budget tracker (fresh facts)
|
||||
num_triples = len(facts_report.semantic_graph)
|
||||
logger.info(f"Fresh facts generated with {num_triples} triple(s).")
|
||||
state.budget_tracker.add_facts_update(num_operations=1, num_triples=num_triples)
|
||||
|
||||
state.clear_failure()
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_FACTS, Status.SUCCESS)
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
return _handle_rendering_error(state, e, FailureStage.GENERATE_TTL_FOR_FACTS)
|
||||
finally:
|
||||
# Clear the context after parsing
|
||||
RDFGraph.set_known_prefixes(None)
|
||||
|
||||
|
||||
async def render_facts_update(
|
||||
state: UnitFactsState, tools: AtomicToolBox
|
||||
) -> UnitFactsState:
|
||||
"""Render facts updates using SPARQL operations.
|
||||
|
||||
Args:
|
||||
state: The current unit facts state containing the chunk to render.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
UnitFactsState: Updated state with rendered facts.
|
||||
"""
|
||||
logger.info("Rendering updates for facts")
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
parser = PydanticOutputParser(pydantic_object=GraphUpdateRenderReport)
|
||||
|
||||
prompt_data = _prepare_prompt_data(state)
|
||||
prompt_data_update = {
|
||||
"preamble": preamble,
|
||||
"improvement_instruction": render_suggestions_prompt(
|
||||
state.suggestions, WorkflowNode.TEXT_TO_FACTS
|
||||
),
|
||||
"output_instruction": output_instruction_sparql,
|
||||
"fact_chapter": facts_template.format(
|
||||
facts_ttl=state.content_unit.graph.serialize(format="turtle")
|
||||
),
|
||||
}
|
||||
prompt_data.update(prompt_data_update)
|
||||
prompt = _create_prompt_template()
|
||||
known_prefixes = _extract_known_prefixes(state)
|
||||
|
||||
try:
|
||||
# Set known prefixes in context before parsing
|
||||
RDFGraph.set_known_prefixes(known_prefixes if known_prefixes else None)
|
||||
|
||||
render_report: GraphUpdateRenderReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
**prompt_data,
|
||||
},
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_FACTS, render_report.external_evidence_request
|
||||
)
|
||||
graph_update = render_report.graph_update
|
||||
state.facts_updates.append(graph_update)
|
||||
state.update_facts()
|
||||
|
||||
num_operations, num_triples = graph_update.count_total_triples()
|
||||
logger.info(
|
||||
f"Facts update has {num_operations} operation(s) "
|
||||
f"with {num_triples} total triple(s)."
|
||||
)
|
||||
|
||||
# Track triples in budget tracker
|
||||
state.budget_tracker.add_facts_update(num_operations, num_triples)
|
||||
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_FACTS, Status.SUCCESS)
|
||||
state.clear_failure()
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
return _handle_rendering_error(
|
||||
state, e, FailureStage.GENERATE_SPARQL_UPDATE_FOR_FACTS
|
||||
)
|
||||
finally:
|
||||
# Clear the context after parsing
|
||||
RDFGraph.set_known_prefixes(None)
|
||||
289
참고/ontocast-main/ontocast/agent/render_ontology.py
Normal file
289
참고/ontocast-main/ontocast/agent/render_ontology.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""Ontology triple rendering agent for OntoCast.
|
||||
|
||||
This module provides functionality for rendering RDF triples from ontologies into
|
||||
human-readable formats, making the ontological knowledge more accessible and
|
||||
understandable.
|
||||
The agent decides between generating bare Turtle for fresh ontologies and SPARQL operations for updates.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry, render_suggestions_prompt
|
||||
from ontocast.onto.enum import FailureStage, Status, WorkflowNode
|
||||
from ontocast.onto.model import GraphUpdateRenderReport, OntologyRenderReport
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.unit_states import UnitOntologyState
|
||||
from ontocast.prompt.common import (
|
||||
ontology_template,
|
||||
output_instruction_sparql,
|
||||
output_instruction_ttl,
|
||||
text_template,
|
||||
)
|
||||
from ontocast.prompt.common import system_preamble_ontology as system_preamble
|
||||
from ontocast.prompt.render_ontology import (
|
||||
general_ontology_instruction,
|
||||
intro_instruction_fresh,
|
||||
intro_instruction_update,
|
||||
prefix_instruction,
|
||||
prefix_instruction_fresh,
|
||||
template_prompt,
|
||||
)
|
||||
from ontocast.tool.atomic import AtomicToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _extract_known_prefixes(state: UnitOntologyState) -> dict[str, str]:
|
||||
"""Extract ontology prefixes used to patch missing declarations in LLM TTL output."""
|
||||
current = state.current_ontology or state.ontology_snapshot
|
||||
known_prefixes: dict[str, str] = {}
|
||||
|
||||
if current and current.graph:
|
||||
for prefix, namespace_uri in current.graph.namespaces():
|
||||
if prefix: # Skip empty prefixes
|
||||
known_prefixes[prefix] = str(namespace_uri)
|
||||
|
||||
if current.prefix and current.namespace:
|
||||
known_prefixes[current.prefix] = current.namespace
|
||||
|
||||
return known_prefixes
|
||||
|
||||
|
||||
async def render_ontology(
|
||||
state: UnitOntologyState, tools: AtomicToolBox
|
||||
) -> UnitOntologyState:
|
||||
"""Structured hybrid ontology renderer with Turtle/SPARQL decision logic.
|
||||
|
||||
This function decides between generating bare Turtle for fresh ontologies
|
||||
and SPARQL operations for updates based on whether the ontology exists.
|
||||
|
||||
Args:
|
||||
state: The current unit ontology state
|
||||
tools: The toolbox containing necessary tools
|
||||
|
||||
Returns:
|
||||
UnitOntologyState: Updated state with rendered ontology
|
||||
"""
|
||||
|
||||
progress_info = state.get_content_unit_progress_string()
|
||||
logger.info(
|
||||
f"Ontology Renderer for {progress_info}: visit {state.node_visits[WorkflowNode.TEXT_TO_ONTOLOGY]}/{state.max_visits_per_node}"
|
||||
)
|
||||
current = state.current_ontology or state.ontology_snapshot
|
||||
# Guardrail for map/reduce flow: if a non-null snapshot exists, stay in update mode.
|
||||
has_seed_ontology = not state.ontology_snapshot.is_null()
|
||||
has_no_seed_ontology = current.is_null() and not has_seed_ontology
|
||||
|
||||
if has_no_seed_ontology:
|
||||
return await render_ontology_fresh(state, tools)
|
||||
else:
|
||||
return await render_ontology_update(state, tools)
|
||||
|
||||
|
||||
async def render_ontology_fresh(
|
||||
state: UnitOntologyState, tools: AtomicToolBox
|
||||
) -> UnitOntologyState:
|
||||
"""Render ontology triples into a human-readable format.
|
||||
|
||||
This function takes the triples from the current ontology and renders them
|
||||
into a more accessible format, making the ontological knowledge easier to
|
||||
understand.
|
||||
|
||||
Args:
|
||||
state: The current agent state containing the ontology to render.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
AgentState: Updated state with rendered triples.
|
||||
"""
|
||||
|
||||
parser = PydanticOutputParser(pydantic_object=OntologyRenderReport)
|
||||
logger.info("Rendering fresh ontology")
|
||||
intro_instruction = intro_instruction_fresh.format(
|
||||
current_domain=state.current_domain
|
||||
)
|
||||
output_instruction = output_instruction_ttl
|
||||
ontology_ttl = ""
|
||||
improvement_instruction_str = ""
|
||||
general_ontology_instruction_str = general_ontology_instruction.format(
|
||||
prefix_instruction=prefix_instruction_fresh
|
||||
)
|
||||
|
||||
text_chapter = text_template.format(text=state.content_unit.text)
|
||||
|
||||
external_evidence = state.external_evidence_text
|
||||
if external_evidence:
|
||||
state.mark_external_evidence_used(WorkflowNode.TEXT_TO_ONTOLOGY)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"preamble",
|
||||
"intro_instruction",
|
||||
"ontology_instruction",
|
||||
"output_instruction",
|
||||
"user_instruction",
|
||||
"improvement_instruction",
|
||||
"ontology_ttl",
|
||||
"text",
|
||||
"external_evidence",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
render_report: OntologyRenderReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"preamble": system_preamble,
|
||||
"intro_instruction": intro_instruction,
|
||||
"ontology_instruction": general_ontology_instruction_str,
|
||||
"output_instruction": output_instruction,
|
||||
"ontology_ttl": ontology_ttl,
|
||||
"user_instruction": state.ontology_user_instruction,
|
||||
"improvement_instruction": improvement_instruction_str,
|
||||
"text": text_chapter,
|
||||
"external_evidence": external_evidence,
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
},
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_ONTOLOGY, render_report.external_evidence_request
|
||||
)
|
||||
state.current_ontology = render_report.ontology
|
||||
state.current_ontology.graph.sanitize_prefixes_namespaces()
|
||||
|
||||
num_triples = len(state.current_ontology.graph)
|
||||
logger.info(f"New ontology created with {num_triples} triple(s).")
|
||||
|
||||
# Track triples in budget tracker (fresh ontology)
|
||||
state.budget_tracker.add_ontology_update(
|
||||
num_operations=1, num_triples=num_triples
|
||||
)
|
||||
|
||||
state.clear_failure()
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.SUCCESS)
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate triples: {str(e)}")
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.FAILED)
|
||||
state.set_failure(FailureStage.GENERATE_TTL_FOR_ONTOLOGY, str(e))
|
||||
return state
|
||||
|
||||
|
||||
async def render_ontology_update(
|
||||
state: UnitOntologyState, tools: AtomicToolBox
|
||||
) -> UnitOntologyState:
|
||||
"""Render ontology triples into a human-readable format.
|
||||
|
||||
This function takes the triples from the current ontology and renders them
|
||||
into a more accessible format, making the ontological knowledge easier to
|
||||
understand.
|
||||
|
||||
Args:
|
||||
state: The current unit ontology state containing the ontology to render.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
UnitOntologyState: Updated state with rendered triples.
|
||||
"""
|
||||
|
||||
parser = PydanticOutputParser(pydantic_object=GraphUpdateRenderReport)
|
||||
current = state.current_ontology or state.ontology_snapshot
|
||||
ontology_iri = current.iri
|
||||
ontology_desc = current.describe()
|
||||
intro_instruction = intro_instruction_update.format(
|
||||
ontology_iri=ontology_iri, ontology_desc=ontology_desc
|
||||
)
|
||||
ontology_chapter = ontology_template.format(
|
||||
ontology_ttl=current.graph.serialize(format="turtle")
|
||||
)
|
||||
output_instruction = output_instruction_sparql
|
||||
improvement_instruction_str = render_suggestions_prompt(
|
||||
state.suggestions, WorkflowNode.TEXT_TO_ONTOLOGY
|
||||
)
|
||||
|
||||
general_ontology_instruction_str = general_ontology_instruction.format(
|
||||
prefix_instruction=prefix_instruction.format(ontology_prefix=current.prefix),
|
||||
ontology_prefix=current.prefix,
|
||||
)
|
||||
text_chapter = text_template.format(text=state.content_unit.text)
|
||||
external_evidence = state.external_evidence_text
|
||||
if external_evidence:
|
||||
state.mark_external_evidence_used(WorkflowNode.TEXT_TO_ONTOLOGY)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"preamble",
|
||||
"intro_instruction",
|
||||
"ontology_instruction",
|
||||
"output_instruction",
|
||||
"user_instruction",
|
||||
"improvement_instruction",
|
||||
"ontology_ttl",
|
||||
"text",
|
||||
"external_evidence",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
known_prefixes = _extract_known_prefixes(state)
|
||||
|
||||
try:
|
||||
llm_tool = await tools.get_llm_tool(state.budget_tracker)
|
||||
# Set known prefixes in context before parsing
|
||||
RDFGraph.set_known_prefixes(known_prefixes if known_prefixes else None)
|
||||
|
||||
render_report: GraphUpdateRenderReport = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"preamble": system_preamble,
|
||||
"intro_instruction": intro_instruction,
|
||||
"ontology_instruction": general_ontology_instruction_str,
|
||||
"output_instruction": output_instruction,
|
||||
"improvement_instruction": improvement_instruction_str,
|
||||
"ontology_ttl": ontology_chapter,
|
||||
"user_instruction": state.ontology_user_instruction,
|
||||
"text": text_chapter,
|
||||
"external_evidence": external_evidence,
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
},
|
||||
)
|
||||
state.set_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_ONTOLOGY, render_report.external_evidence_request
|
||||
)
|
||||
graph_update = render_report.graph_update
|
||||
state.ontology_updates.append(graph_update)
|
||||
state.update_ontology()
|
||||
|
||||
num_operations, num_triples = graph_update.count_total_triples()
|
||||
logger.info(
|
||||
f"Ontology update has {num_operations} operation(s) "
|
||||
f"with {num_triples} total triple(s)."
|
||||
)
|
||||
|
||||
# Track triples in budget tracker
|
||||
state.budget_tracker.add_ontology_update(num_operations, num_triples)
|
||||
|
||||
state.clear_failure()
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.SUCCESS)
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate ontology update: {str(e)}")
|
||||
state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.FAILED)
|
||||
state.set_failure(FailureStage.GENERATE_SPARQL_UPDATE_FOR_ONTOLOGY, str(e))
|
||||
return state
|
||||
finally:
|
||||
# Clear the context after parsing
|
||||
RDFGraph.set_known_prefixes(None)
|
||||
196
참고/ontocast-main/ontocast/agent/select_ontology.py
Normal file
196
참고/ontocast-main/ontocast/agent/select_ontology.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Ontology selection agent for OntoCast.
|
||||
|
||||
This module provides functionality for selecting appropriate ontologies based on
|
||||
the content of source text segments, ensuring that the chosen ontology best matches the
|
||||
domain and requirements of the text.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from ontocast.agent.common import call_llm_with_retry
|
||||
from ontocast.onto.enum import Status
|
||||
from ontocast.onto.model import create_ontology_selector_report_model
|
||||
from ontocast.onto.null import NULL_ONTOLOGY
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.prompt.select_ontology import template_prompt
|
||||
from ontocast.tool import OntologyManager
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _create_document_excerpt(state: AgentState, max_length: int = 3000) -> str:
|
||||
"""Create a representative excerpt from the document for ontology selection.
|
||||
|
||||
This function samples text from multiple content units to provide a better
|
||||
representation of the document content than just the first unit.
|
||||
|
||||
Args:
|
||||
state: The current agent state.
|
||||
max_length: Maximum total length of the excerpt.
|
||||
|
||||
Returns:
|
||||
str: A representative excerpt from the document.
|
||||
"""
|
||||
excerpt_parts = []
|
||||
total_length = 0
|
||||
chunk_length = max_length // 3 # Aim for ~3 source chunks, ~1000 chars each
|
||||
|
||||
# Strategy: Sample from first, middle, and last units if available
|
||||
if state.content_units:
|
||||
num_chunks = len(state.content_units)
|
||||
indices_to_sample = []
|
||||
|
||||
if num_chunks == 1:
|
||||
indices_to_sample = [0]
|
||||
elif num_chunks == 2:
|
||||
indices_to_sample = [0, 1]
|
||||
else:
|
||||
# Sample first, middle, and last
|
||||
indices_to_sample = [0, num_chunks // 2, num_chunks - 1]
|
||||
|
||||
for idx in indices_to_sample:
|
||||
if idx < num_chunks and total_length < max_length:
|
||||
chunk_text = state.content_units[idx].text
|
||||
# Take a portion of this source chunk
|
||||
remaining = max_length - total_length
|
||||
sample_length = min(chunk_length, remaining, len(chunk_text))
|
||||
|
||||
if sample_length > 0:
|
||||
if sample_length < len(chunk_text):
|
||||
excerpt_parts.append(chunk_text[:sample_length] + " ...")
|
||||
else:
|
||||
excerpt_parts.append(chunk_text)
|
||||
total_length += sample_length
|
||||
|
||||
if excerpt_parts:
|
||||
return "\n\n[...]\n\n".join(excerpt_parts)
|
||||
|
||||
# Fallback: Use input_text if available
|
||||
if state.input_text:
|
||||
if len(state.input_text) <= max_length:
|
||||
return state.input_text
|
||||
return state.input_text[:max_length] + " ..."
|
||||
|
||||
# Last resort: Use current content unit
|
||||
if state.current_content_unit and state.current_content_unit.text:
|
||||
chunk_text = state.current_content_unit.text
|
||||
if len(chunk_text) <= max_length:
|
||||
return chunk_text
|
||||
return chunk_text[:max_length] + " ..."
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
async def select_ontology(state: AgentState, tools: ToolBox) -> AgentState:
|
||||
"""Select an appropriate ontology for the document.
|
||||
|
||||
This function analyzes the document and selects the most appropriate
|
||||
ontology based on its content and requirements using a numbered list selection.
|
||||
If an ontology is already selected, it skips selection to ensure one ontology
|
||||
per document.
|
||||
|
||||
Args:
|
||||
state: The current agent state containing the document to process.
|
||||
tools: The toolbox instance providing utility functions.
|
||||
|
||||
Returns:
|
||||
AgentState: Updated state with selected ontology.
|
||||
"""
|
||||
# Skip if ontology already selected (for subsequent chunks in the loop)
|
||||
if not state.current_ontology.is_null():
|
||||
logger.debug(
|
||||
f"Ontology already selected: {state.current_ontology.ontology_id}, "
|
||||
"skipping selection to maintain one ontology per document"
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
progress_info = state.get_content_unit_progress_string()
|
||||
logger.info(f"Selecting ontology for document ({progress_info})")
|
||||
llm_tool = tools.llm
|
||||
om_tool: OntologyManager = tools.ontology_manager
|
||||
|
||||
if om_tool.has_ontologies:
|
||||
ontologies = om_tool.ontologies
|
||||
num_ontologies = len(ontologies)
|
||||
|
||||
# Create numbered list of ontologies
|
||||
ontologies_list_lines = []
|
||||
for i, ontology in enumerate(ontologies, start=1):
|
||||
ontologies_list_lines.append(f"{i}. {ontology.describe()}")
|
||||
|
||||
ontologies_list = "\n\n".join(ontologies_list_lines)
|
||||
|
||||
logger.info(f"Presenting {num_ontologies} ontologies for selection")
|
||||
|
||||
# Create a better document excerpt using multiple chunks
|
||||
excerpt = _create_document_excerpt(state, max_length=3000)
|
||||
|
||||
# Create dynamic model with correct constraint
|
||||
ontology_selector_report_model = create_ontology_selector_report_model(
|
||||
num_ontologies
|
||||
)
|
||||
parser = PydanticOutputParser(pydantic_object=ontology_selector_report_model)
|
||||
|
||||
prompt = PromptTemplate(
|
||||
template=template_prompt,
|
||||
input_variables=[
|
||||
"excerpt",
|
||||
"ontologies_list",
|
||||
"num_ontologies",
|
||||
"format_instructions",
|
||||
],
|
||||
)
|
||||
|
||||
selector = await call_llm_with_retry(
|
||||
llm_tool=llm_tool,
|
||||
prompt=prompt,
|
||||
parser=parser,
|
||||
prompt_kwargs={
|
||||
"excerpt": excerpt,
|
||||
"ontologies_list": ontologies_list,
|
||||
"num_ontologies": num_ontologies,
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
},
|
||||
)
|
||||
|
||||
# Map answer_index to ontology
|
||||
# answer_index: 0 -> select None
|
||||
# answer_index: 1 to num_ontologies -> select ontology at (answer_index - 1)
|
||||
state.status = Status.SUCCESS
|
||||
if selector.answer_index == 0:
|
||||
# None selected
|
||||
logger.debug("LLM selected: None (no suitable ontology)")
|
||||
state.current_ontology = NULL_ONTOLOGY
|
||||
elif 1 <= selector.answer_index <= num_ontologies:
|
||||
# Select ontology at index (answer_index - 1) since list is 0-based
|
||||
selected_ontology = ontologies[selector.answer_index - 1]
|
||||
logger.debug(
|
||||
f"LLM selected ontology at index {selector.answer_index}: "
|
||||
f"{selected_ontology.ontology_id} ({selected_ontology.iri})"
|
||||
)
|
||||
state.current_ontology = selected_ontology
|
||||
state.status = Status.SUCCESS
|
||||
else:
|
||||
# This should not happen due to Pydantic validation, but handle gracefully
|
||||
logger.warning(
|
||||
f"Invalid answer_index {selector.answer_index} defaulting to NULL_ONTOLOGY"
|
||||
)
|
||||
state.current_ontology = NULL_ONTOLOGY
|
||||
else:
|
||||
state.current_ontology = NULL_ONTOLOGY
|
||||
|
||||
# Set the initial version if not already set (tracks original version when ontology was selected)
|
||||
if state.current_ontology.initial_version is None:
|
||||
state.current_ontology.initial_version = state.current_ontology.version
|
||||
logger.debug(
|
||||
f"Set initial version for ontology {state.current_ontology.ontology_id}: {state.current_ontology.initial_version}"
|
||||
)
|
||||
|
||||
logger.debug(f"Current ontology set to: {state.current_ontology.ontology_id}")
|
||||
|
||||
return state
|
||||
78
참고/ontocast-main/ontocast/agent/serialize.py
Normal file
78
참고/ontocast-main/ontocast/agent/serialize.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Serialization agent for OntoCast.
|
||||
|
||||
This module provides functionality for serializing the knowledge graph
|
||||
(ontology and facts) to the triple store.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def serialize(state: AgentState, tools: ToolBox) -> AgentState:
|
||||
"""Serialize the knowledge graph to the triple store.
|
||||
|
||||
This function:
|
||||
- Handles version management for updated ontologies
|
||||
- Tracks budget usage
|
||||
- Serializes both ontology and facts to the triple store
|
||||
|
||||
Args:
|
||||
state: Current agent state with ontology and facts
|
||||
tools: ToolBox containing serialization tools
|
||||
|
||||
Returns:
|
||||
Updated agent state after serialization
|
||||
"""
|
||||
# Initialize empty facts graph if not set (for ontology-only render mode)
|
||||
if state.aggregated_facts is None:
|
||||
state.aggregated_facts = RDFGraph()
|
||||
logger.info("No facts to serialize (ontology-only render mode)")
|
||||
|
||||
# Ontology versioning: reduce_ontology sets ontology_updates_applied with the
|
||||
# merged GraphUpdate when aggregating parallel ontology units.
|
||||
if state.ontology_updates_applied:
|
||||
logger.info(
|
||||
f"Ontology was updated during processing ({len(state.ontology_updates_applied)} update operations). "
|
||||
f"Analyzing changes to determine version increment..."
|
||||
)
|
||||
state.current_ontology.mark_as_updated(state.ontology_updates_applied)
|
||||
state.current_ontology.sync_properties_to_graph()
|
||||
elif state.ontology_units:
|
||||
logger.debug("Ontology from EmbeddingBasedAggregator; skipping version bump")
|
||||
else:
|
||||
logger.debug(
|
||||
f"Ontology unchanged during processing (version: {state.current_ontology.version})"
|
||||
)
|
||||
|
||||
# Report LLM budget usage
|
||||
if state.budget_tracker:
|
||||
logger.info(state.budget_tracker.get_summary())
|
||||
|
||||
provenance_graph_uri = f"{str(state.graph_uri).rstrip('/')}/ontology-provenance"
|
||||
if len(state.ontology_provenance_artifact) > 0:
|
||||
logger.info(
|
||||
"Persisting ontology provenance artifact (%d triples) to graph %s",
|
||||
len(state.ontology_provenance_artifact),
|
||||
provenance_graph_uri,
|
||||
)
|
||||
if tools.filesystem_manager is not None:
|
||||
tools.filesystem_manager.serialize(
|
||||
state.ontology_provenance_artifact,
|
||||
graph_uri=provenance_graph_uri,
|
||||
)
|
||||
if (
|
||||
tools.triple_store_manager is not None
|
||||
and tools.triple_store_manager != tools.filesystem_manager
|
||||
):
|
||||
tools.triple_store_manager.serialize(
|
||||
state.ontology_provenance_artifact,
|
||||
graph_uri=provenance_graph_uri,
|
||||
)
|
||||
|
||||
tools.serialize(state)
|
||||
return state
|
||||
153
참고/ontocast-main/ontocast/agent/sublimate_ontology.py
Normal file
153
참고/ontocast-main/ontocast/agent/sublimate_ontology.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Ontology sublimation agent for OntoCast.
|
||||
|
||||
This module provides functionality for refining and enhancing ontologies through
|
||||
a process of sublimation, which involves improving the structure, consistency,
|
||||
and expressiveness of the ontological knowledge.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Iterable, cast
|
||||
|
||||
from rdflib.term import Node
|
||||
|
||||
from ontocast.onto.constants import DEFAULT_IRI
|
||||
from ontocast.onto.enum import FailureStage
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sublimate_ontology(state: AgentState) -> tuple[RDFGraph, RDFGraph]:
|
||||
graph_onto_addendum = RDFGraph()
|
||||
graph_facts_pure = RDFGraph()
|
||||
|
||||
# Copy all prefixes from the original graph to both new graphs
|
||||
for prefix, namespace in state.current_content_unit.graph.namespaces():
|
||||
graph_onto_addendum.bind(prefix, namespace)
|
||||
graph_facts_pure.bind(prefix, namespace)
|
||||
|
||||
query_ontology = f"""
|
||||
PREFIX cd: <{DEFAULT_IRI}>
|
||||
|
||||
SELECT ?s ?p ?o
|
||||
WHERE {{
|
||||
?s ?p ?o .
|
||||
FILTER (
|
||||
!(
|
||||
STRSTARTS(STR(?s), STR(cd:)) ||
|
||||
STRSTARTS(STR(?p), STR(cd:)) ||
|
||||
(isIRI(?o) && STRSTARTS(STR(?o), STR(cd:)))
|
||||
)
|
||||
)
|
||||
}}
|
||||
"""
|
||||
results = cast(
|
||||
Iterable[tuple[Node, Node, Node]],
|
||||
state.current_content_unit.graph.query(query_ontology),
|
||||
)
|
||||
|
||||
# Add filtered triples to the new graph
|
||||
for s, p, o in results:
|
||||
graph_onto_addendum.add((s, p, o))
|
||||
|
||||
query_facts = f"""
|
||||
PREFIX cd: <{DEFAULT_IRI}>
|
||||
|
||||
SELECT ?s ?p ?o
|
||||
WHERE {{
|
||||
?s ?p ?o .
|
||||
FILTER (
|
||||
STRSTARTS(STR(?s), STR(cd:)) ||
|
||||
STRSTARTS(STR(?p), STR(cd:)) ||
|
||||
(isIRI(?o) && STRSTARTS(STR(?o), STR(cd:)))
|
||||
)
|
||||
}}
|
||||
"""
|
||||
|
||||
results = cast(
|
||||
Iterable[tuple[Node, Node, Node]],
|
||||
state.current_content_unit.graph.query(query_facts),
|
||||
)
|
||||
|
||||
# Add filtered triples to the new graph
|
||||
for s, p, o in results:
|
||||
graph_facts_pure.add((s, p, o))
|
||||
|
||||
logger.info(
|
||||
f"Found triples: facts {len(graph_facts_pure)}; ontology {len(graph_onto_addendum)}"
|
||||
)
|
||||
return graph_onto_addendum, graph_facts_pure
|
||||
|
||||
|
||||
def sublimate_ontology(state: AgentState, tools: ToolBox):
|
||||
logger.debug("Starting ontology sublimation")
|
||||
|
||||
if state.current_ontology is None:
|
||||
return state
|
||||
try:
|
||||
state.update_facts()
|
||||
graph_onto_addendum, graph_facts = _sublimate_ontology(state=state)
|
||||
|
||||
# Ensure ontology is not null and ontology_id is set before updating
|
||||
if len(graph_onto_addendum) > 0:
|
||||
logger.info("ontology seeped into facts:")
|
||||
logger.info(f"graph: {graph_onto_addendum.serialize()}")
|
||||
if state.current_ontology.is_null():
|
||||
logger.warning(
|
||||
"Cannot update ontology: null ontology cannot be updated"
|
||||
)
|
||||
elif state.current_ontology.ontology_id:
|
||||
# Check if adding triples would exceed max_triples limit
|
||||
max_triples = state.ontology_max_triples
|
||||
if max_triples is not None:
|
||||
current_size = len(state.current_ontology.graph)
|
||||
addendum_size = len(graph_onto_addendum)
|
||||
if current_size + addendum_size > max_triples:
|
||||
logger.warning(
|
||||
f"Ontology sublimation skipped: would exceed limit "
|
||||
f"({current_size + addendum_size} > {max_triples} triples). "
|
||||
f"Current size: {current_size} triples."
|
||||
)
|
||||
else:
|
||||
# Only update state.current_ontology, not OntologyManager
|
||||
# OntologyManager will be updated in serialize() during final serialization
|
||||
state.current_ontology.graph += graph_onto_addendum
|
||||
logger.debug(
|
||||
f"Updated state.current_ontology with {len(graph_onto_addendum)} triples from sublimation"
|
||||
)
|
||||
else:
|
||||
# No limit set, proceed with update
|
||||
state.current_ontology.graph += graph_onto_addendum
|
||||
logger.debug(
|
||||
f"Updated state.current_ontology with {len(graph_onto_addendum)} triples from sublimation"
|
||||
)
|
||||
else:
|
||||
logger.warning("Cannot update ontology: ontology_id is None")
|
||||
|
||||
# Ensure graph_facts is an RDFGraph instance
|
||||
if not isinstance(graph_facts, RDFGraph):
|
||||
logger.warning("received an rdflib.Graph rather than RDFGraph")
|
||||
new_graph = RDFGraph()
|
||||
graph_facts_rdflib = cast(Iterable[tuple[Node, Node, Node]], graph_facts)
|
||||
for triple in graph_facts_rdflib:
|
||||
new_graph.add(triple)
|
||||
graph_facts_namespaces = cast(
|
||||
Iterable[tuple[str, str]], graph_facts.namespaces()
|
||||
)
|
||||
for prefix, namespace in graph_facts_namespaces:
|
||||
new_graph.bind(prefix, namespace)
|
||||
graph_facts = new_graph
|
||||
|
||||
state.current_content_unit.graph = graph_facts
|
||||
|
||||
state.clear_failure()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in sublimate_ontology: {str(e)}")
|
||||
state.set_failure(
|
||||
FailureStage.SUBLIMATE_ONTOLOGY,
|
||||
str(e),
|
||||
)
|
||||
|
||||
return state
|
||||
Reference in New Issue
Block a user