참고소스 수정본
This commit is contained in:
13
참고/ontocast-main/ontocast/stategraph/__init__.py
Normal file
13
참고/ontocast-main/ontocast/stategraph/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""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 .create import create_agent_graph
|
||||
|
||||
__all__ = [
|
||||
"create_agent_graph",
|
||||
]
|
||||
248
참고/ontocast-main/ontocast/stategraph/atomic.py
Normal file
248
참고/ontocast-main/ontocast/stategraph/atomic.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Reusable per-unit render/critic retry loops.
|
||||
|
||||
These loops are designed for map/reduce execution where each content unit
|
||||
is processed independently. They deep-copy the incoming unit state, then run
|
||||
render -> critic until success or retry exhaustion.
|
||||
|
||||
Minimal tools contract:
|
||||
- A ``ToolBox`` instance is still expected by type, but the loop itself only
|
||||
relies on downstream agent calls that resolve an LLM via
|
||||
``tools.get_llm_tool(state.budget_tracker)``.
|
||||
- No triple-store, chunker, converter, or aggregator capabilities are required
|
||||
for these atomic loops.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ontocast.agent.criticise_facts import criticise_facts
|
||||
from ontocast.agent.criticise_ontology import criticise_ontology
|
||||
from ontocast.agent.external_evidence import (
|
||||
fetch_external_evidence_for_node,
|
||||
plan_external_evidence_for_node,
|
||||
)
|
||||
from ontocast.agent.render_facts import render_facts
|
||||
from ontocast.agent.render_ontology import render_ontology
|
||||
from ontocast.onto.enum import Status, WorkflowNode
|
||||
from ontocast.onto.model import ExternalEvidenceCacheEntry, ExternalEvidenceRequest
|
||||
from ontocast.onto.unit_states import UnitFactsState, UnitOntologyState
|
||||
from ontocast.tool.atomic import AtomicToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_max_visits_limit(state_visits: int, override: int | None) -> int:
|
||||
"""Return a safe visit limit while respecting explicit overrides."""
|
||||
visits = state_visits if override is None else override
|
||||
return max(1, visits)
|
||||
|
||||
|
||||
def _reset_node_evidence_context(
|
||||
state: UnitFactsState | UnitOntologyState, node: WorkflowNode
|
||||
) -> None:
|
||||
"""Start node execution in no-search mode with empty evidence context."""
|
||||
state.set_external_evidence_request(node, ExternalEvidenceRequest())
|
||||
state.set_external_evidence_cache_entry(node, ExternalEvidenceCacheEntry())
|
||||
state.load_external_evidence_for_node(node)
|
||||
|
||||
|
||||
async def facts_loop(
|
||||
state: UnitFactsState, tools: AtomicToolBox, max_visits_per_node: int | None = None
|
||||
) -> UnitFactsState:
|
||||
"""Run facts render/critic loop for one content unit.
|
||||
|
||||
Ontology is selected once per document in the main workflow; ontology_snapshot
|
||||
is always provided by the caller.
|
||||
"""
|
||||
unit_state = state.model_copy(deep=True)
|
||||
max_visits = _resolve_max_visits_limit(
|
||||
unit_state.max_visits_per_node, max_visits_per_node
|
||||
)
|
||||
unit_state.max_visits_per_node = max_visits
|
||||
|
||||
for render_attempt in range(1, max_visits + 1):
|
||||
unit_state.node_visits[WorkflowNode.TEXT_TO_FACTS] += 1
|
||||
_reset_node_evidence_context(unit_state, WorkflowNode.TEXT_TO_FACTS)
|
||||
unit_state = await render_facts(unit_state, tools)
|
||||
if unit_state.status != Status.SUCCESS:
|
||||
render_request = unit_state.get_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_FACTS
|
||||
)
|
||||
if render_request.initiate_search:
|
||||
unit_state = await plan_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.TEXT_TO_FACTS
|
||||
)
|
||||
unit_state = await fetch_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.TEXT_TO_FACTS
|
||||
)
|
||||
unit_state = await render_facts(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit facts render recovered with search at attempt %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
# Continue to critic tier below.
|
||||
else:
|
||||
logger.info(
|
||||
"Unit facts render failed at attempt %s/%s (with search)",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
logger.info(
|
||||
"Unit facts render failed at attempt %s/%s (no search request)",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
continue
|
||||
|
||||
for critic_attempt in range(1, max_visits + 1):
|
||||
unit_state.node_visits[WorkflowNode.CRITICISE_FACTS] += 1
|
||||
_reset_node_evidence_context(unit_state, WorkflowNode.CRITICISE_FACTS)
|
||||
unit_state = await criticise_facts(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit facts loop converged at render %s/%s critic %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
return unit_state
|
||||
|
||||
critic_request = unit_state.get_external_evidence_request(
|
||||
WorkflowNode.CRITICISE_FACTS
|
||||
)
|
||||
if not critic_request.initiate_search:
|
||||
logger.info(
|
||||
"Unit facts critic failed at render %s/%s critic %s/%s without search request",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
break
|
||||
|
||||
unit_state = await plan_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.CRITICISE_FACTS
|
||||
)
|
||||
unit_state = await fetch_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.CRITICISE_FACTS
|
||||
)
|
||||
unit_state = await criticise_facts(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit facts loop converged with critic search at render %s/%s critic %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
return unit_state
|
||||
|
||||
continue
|
||||
|
||||
logger.info("Unit facts loop exhausted retries")
|
||||
return unit_state
|
||||
|
||||
|
||||
async def ontology_loop(
|
||||
state: UnitOntologyState,
|
||||
tools: AtomicToolBox,
|
||||
max_visits_per_node: int | None = None,
|
||||
) -> UnitOntologyState:
|
||||
"""Run ontology render/critic loop for one content unit.
|
||||
|
||||
Ontology is selected once per document in the main workflow; ontology_snapshot
|
||||
is always provided by the caller (may be null for fresh-ontology builds).
|
||||
"""
|
||||
unit_state = state.model_copy(deep=True)
|
||||
max_visits = _resolve_max_visits_limit(
|
||||
unit_state.max_visits_per_node, max_visits_per_node
|
||||
)
|
||||
unit_state.max_visits_per_node = max_visits
|
||||
|
||||
for render_attempt in range(1, max_visits + 1):
|
||||
unit_state.node_visits[WorkflowNode.TEXT_TO_ONTOLOGY] += 1
|
||||
_reset_node_evidence_context(unit_state, WorkflowNode.TEXT_TO_ONTOLOGY)
|
||||
unit_state = await render_ontology(unit_state, tools)
|
||||
if unit_state.status != Status.SUCCESS:
|
||||
render_request = unit_state.get_external_evidence_request(
|
||||
WorkflowNode.TEXT_TO_ONTOLOGY
|
||||
)
|
||||
if render_request.initiate_search:
|
||||
unit_state = await plan_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.TEXT_TO_ONTOLOGY
|
||||
)
|
||||
unit_state = await fetch_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.TEXT_TO_ONTOLOGY
|
||||
)
|
||||
unit_state = await render_ontology(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit ontology render recovered with search at attempt %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Unit ontology render failed at attempt %s/%s (with search)",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
logger.info(
|
||||
"Unit ontology render failed at attempt %s/%s (no search request)",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
)
|
||||
continue
|
||||
|
||||
for critic_attempt in range(1, max_visits + 1):
|
||||
unit_state.node_visits[WorkflowNode.CRITICISE_ONTOLOGY] += 1
|
||||
_reset_node_evidence_context(unit_state, WorkflowNode.CRITICISE_ONTOLOGY)
|
||||
unit_state = await criticise_ontology(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit ontology loop converged at render %s/%s critic %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
return unit_state
|
||||
|
||||
critic_request = unit_state.get_external_evidence_request(
|
||||
WorkflowNode.CRITICISE_ONTOLOGY
|
||||
)
|
||||
if not critic_request.initiate_search:
|
||||
logger.info(
|
||||
"Unit ontology critic failed at render %s/%s critic %s/%s without search request",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
break
|
||||
|
||||
unit_state = await plan_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.CRITICISE_ONTOLOGY
|
||||
)
|
||||
unit_state = await fetch_external_evidence_for_node(
|
||||
unit_state, tools, WorkflowNode.CRITICISE_ONTOLOGY
|
||||
)
|
||||
unit_state = await criticise_ontology(unit_state, tools)
|
||||
if unit_state.status == Status.SUCCESS:
|
||||
logger.info(
|
||||
"Unit ontology loop converged with critic search at render %s/%s critic %s/%s",
|
||||
render_attempt,
|
||||
max_visits,
|
||||
critic_attempt,
|
||||
max_visits,
|
||||
)
|
||||
return unit_state
|
||||
|
||||
logger.info("Unit ontology loop exhausted retries")
|
||||
return unit_state
|
||||
95
참고/ontocast-main/ontocast/stategraph/create.py
Normal file
95
참고/ontocast-main/ontocast/stategraph/create.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from functools import partial
|
||||
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
|
||||
from ontocast.agent import chunk_text, convert_document, select_ontology
|
||||
from ontocast.agent.serialize import serialize
|
||||
from ontocast.onto.enum import WorkflowNode
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.stategraph.node_factories import (
|
||||
make_bootstrap_ontology_node,
|
||||
make_consolidate_ontology_node,
|
||||
make_merge_facts_node,
|
||||
make_normalize_ontology_node,
|
||||
make_render_facts_node,
|
||||
make_render_ontology_node,
|
||||
)
|
||||
from ontocast.stategraph.routing import (
|
||||
route_after_ontology_consolidation,
|
||||
route_after_ontology_selection,
|
||||
)
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
|
||||
def create_agent_graph(tools: ToolBox) -> CompiledStateGraph:
|
||||
"""Create the parallel map/reduce agent graph.
|
||||
|
||||
Flow: CONVERT -> CHUNK -> (conditional)
|
||||
- ontology null: SELECT_ONTOLOGY -> (ontology or facts map)
|
||||
- ontology set: PARALLEL_ONTOLOGY_MAP or PARALLEL_FACTS_MAP
|
||||
- render_ontology: PARALLEL_ONTOLOGY_MAP -> REDUCE_ONTOLOGY ->
|
||||
[PARALLEL_FACTS_MAP -> REDUCE_FACTS]? -> SERIALIZE
|
||||
- render_facts only: PARALLEL_FACTS_MAP -> REDUCE_FACTS -> SERIALIZE
|
||||
|
||||
One ontology is selected per document in the main workflow (SELECT_ONTOLOGY).
|
||||
"""
|
||||
workflow = StateGraph(AgentState)
|
||||
|
||||
convert_document_node = partial(convert_document, tools=tools)
|
||||
chunk_text_node = partial(chunk_text, tools=tools)
|
||||
select_ontology_node = partial(select_ontology, tools=tools)
|
||||
serialize_node = partial(serialize, tools=tools)
|
||||
|
||||
bootstrap_ontology_node = make_bootstrap_ontology_node(tools)
|
||||
render_ontology_node = make_render_ontology_node(tools)
|
||||
normalize_ontology_node = make_normalize_ontology_node(tools)
|
||||
consolidate_ontology_node = make_consolidate_ontology_node(tools)
|
||||
render_facts_node = make_render_facts_node(tools)
|
||||
merge_facts_node = make_merge_facts_node(tools)
|
||||
|
||||
workflow.add_node(WorkflowNode.CONVERT_TO_MD, convert_document_node)
|
||||
workflow.add_node(WorkflowNode.CHUNK, chunk_text_node)
|
||||
workflow.add_node(WorkflowNode.SELECT_ONTOLOGY, select_ontology_node)
|
||||
workflow.add_node(WorkflowNode.BOOTSTRAP_ONTOLOGY, bootstrap_ontology_node)
|
||||
workflow.add_node(WorkflowNode.RENDER_ONTOLOGY_UPDATE, render_ontology_node)
|
||||
workflow.add_node(WorkflowNode.NORMALIZE_ONTOLOGY_UPDATES, normalize_ontology_node)
|
||||
workflow.add_node(WorkflowNode.CONSOLIDATE_ONTOLOGY, consolidate_ontology_node)
|
||||
workflow.add_node(WorkflowNode.RENDER_FACTS, render_facts_node)
|
||||
workflow.add_node(WorkflowNode.MERGE_FACTS, merge_facts_node)
|
||||
workflow.add_node(WorkflowNode.SERIALIZE, serialize_node)
|
||||
workflow.add_edge(WorkflowNode.CHUNK, WorkflowNode.SELECT_ONTOLOGY)
|
||||
workflow.add_conditional_edges(
|
||||
WorkflowNode.SELECT_ONTOLOGY,
|
||||
route_after_ontology_selection,
|
||||
{
|
||||
WorkflowNode.BOOTSTRAP_ONTOLOGY: WorkflowNode.BOOTSTRAP_ONTOLOGY,
|
||||
WorkflowNode.RENDER_ONTOLOGY_UPDATE: WorkflowNode.RENDER_ONTOLOGY_UPDATE,
|
||||
WorkflowNode.RENDER_FACTS: WorkflowNode.RENDER_FACTS,
|
||||
},
|
||||
)
|
||||
workflow.add_edge(
|
||||
WorkflowNode.BOOTSTRAP_ONTOLOGY, WorkflowNode.RENDER_ONTOLOGY_UPDATE
|
||||
)
|
||||
workflow.add_edge(START, WorkflowNode.CONVERT_TO_MD)
|
||||
workflow.add_edge(WorkflowNode.CONVERT_TO_MD, WorkflowNode.CHUNK)
|
||||
workflow.add_edge(
|
||||
WorkflowNode.RENDER_ONTOLOGY_UPDATE, WorkflowNode.NORMALIZE_ONTOLOGY_UPDATES
|
||||
)
|
||||
workflow.add_edge(
|
||||
WorkflowNode.NORMALIZE_ONTOLOGY_UPDATES, WorkflowNode.CONSOLIDATE_ONTOLOGY
|
||||
)
|
||||
workflow.add_conditional_edges(
|
||||
WorkflowNode.CONSOLIDATE_ONTOLOGY,
|
||||
route_after_ontology_consolidation,
|
||||
{
|
||||
WorkflowNode.RENDER_FACTS: WorkflowNode.RENDER_FACTS,
|
||||
WorkflowNode.SERIALIZE: WorkflowNode.SERIALIZE,
|
||||
},
|
||||
)
|
||||
workflow.add_edge(WorkflowNode.RENDER_FACTS, WorkflowNode.MERGE_FACTS)
|
||||
workflow.add_edge(WorkflowNode.MERGE_FACTS, WorkflowNode.SERIALIZE)
|
||||
workflow.add_edge(WorkflowNode.SERIALIZE, END)
|
||||
|
||||
return workflow.compile()
|
||||
57
참고/ontocast-main/ontocast/stategraph/helpers.py
Normal file
57
참고/ontocast-main/ontocast/stategraph/helpers.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import logging
|
||||
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.onto.unit_states import UnitOntologyState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_ontology_delta_graph(result: UnitOntologyState) -> RDFGraph:
|
||||
"""Build a delta graph from a unit ontology result.
|
||||
|
||||
If update operations exist, only inserted triples are aggregated.
|
||||
Otherwise, the current ontology snapshot is used as the delta.
|
||||
"""
|
||||
if result.all_updates:
|
||||
delta_graph = RDFGraph()
|
||||
for graph_update in result.all_updates:
|
||||
insert_graph = graph_update.extract_insert_graph()
|
||||
for triple in insert_graph:
|
||||
delta_graph.add(triple)
|
||||
for prefix, namespace_uri in insert_graph.namespaces():
|
||||
if prefix:
|
||||
delta_graph.bind(prefix, namespace_uri)
|
||||
return delta_graph
|
||||
|
||||
return result.current_ontology.graph.copy()
|
||||
|
||||
|
||||
def build_document_excerpt(state: AgentState) -> str:
|
||||
"""Create a representative excerpt from sampled source units."""
|
||||
excerpt_parts: list[str] = []
|
||||
|
||||
if state.content_units:
|
||||
unit_count = len(state.content_units)
|
||||
if unit_count == 1:
|
||||
sample_indices = [0]
|
||||
elif unit_count == 2:
|
||||
sample_indices = [0, 1]
|
||||
else:
|
||||
sample_indices = [0, 1, unit_count // 2, unit_count - 1]
|
||||
|
||||
visited_indices: set[int] = set()
|
||||
for index in sample_indices:
|
||||
if index in visited_indices or index < 0 or index >= unit_count:
|
||||
continue
|
||||
visited_indices.add(index)
|
||||
unit_text = state.content_units[index].text.strip()
|
||||
if not unit_text:
|
||||
continue
|
||||
excerpt_parts.append(unit_text)
|
||||
|
||||
if excerpt_parts:
|
||||
return "\n\n[...]\n\n".join(excerpt_parts)
|
||||
if state.input_text:
|
||||
return state.input_text
|
||||
return ""
|
||||
329
참고/ontocast-main/ontocast/stategraph/node_factories.py
Normal file
329
참고/ontocast-main/ontocast/stategraph/node_factories.py
Normal file
@@ -0,0 +1,329 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from rdflib import DCTERMS, URIRef
|
||||
|
||||
from ontocast.agent.normalize_ontology import normalize_ontology_units
|
||||
from ontocast.agent.render_ontology import render_ontology_update
|
||||
from ontocast.onto.content_unit import ContentUnit, OutputType, SourceUnit
|
||||
from ontocast.onto.enum import Status
|
||||
from ontocast.onto.ontology import Ontology
|
||||
from ontocast.onto.rdfgraph import RDFGraph
|
||||
from ontocast.onto.state import AgentState
|
||||
from ontocast.onto.unit_states import UnitFactsState, UnitOntologyState
|
||||
from ontocast.stategraph.atomic import facts_loop, ontology_loop
|
||||
from ontocast.stategraph.helpers import (
|
||||
build_document_excerpt,
|
||||
build_ontology_delta_graph,
|
||||
)
|
||||
from ontocast.toolbox import ToolBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_bootstrap_ontology_node(tools: ToolBox):
|
||||
atomic_tools = tools.get_atomic_tools()
|
||||
|
||||
async def bootstrap_ontology(state: AgentState) -> AgentState:
|
||||
"""Create one seed ontology for null-selection flow."""
|
||||
if not state.render_ontology or not state.current_ontology.is_null():
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
if not state.content_units:
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
excerpt = build_document_excerpt(state).strip()
|
||||
if not excerpt:
|
||||
logger.warning(
|
||||
"Skipping ontology bootstrap: no usable excerpt was produced from content units."
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
bootstrap_unit = SourceUnit(
|
||||
text=excerpt,
|
||||
index=0,
|
||||
doc_iri=URIRef(state.doc_iri),
|
||||
type=OutputType.ONTOLOGIES,
|
||||
)
|
||||
bootstrap_state = UnitOntologyState(
|
||||
content_unit=bootstrap_unit,
|
||||
ontology_snapshot=Ontology(),
|
||||
ontology_user_instruction=state.ontology_user_instruction,
|
||||
budget_tracker=state.budget_tracker,
|
||||
max_visits_per_node=tools.config.server.max_visits_per_node,
|
||||
current_domain=state.current_domain,
|
||||
ontology_max_triples=tools.config.server.ontology_max_triples,
|
||||
)
|
||||
result = await ontology_loop(bootstrap_state, atomic_tools)
|
||||
if result.status == Status.SUCCESS and not result.current_ontology.is_null():
|
||||
state.current_ontology = result.current_ontology
|
||||
logger.info(
|
||||
f"Bootstrapped ontology anchor: {state.current_ontology.iri} "
|
||||
f"({len(state.current_ontology.graph)} triples)"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Ontology bootstrap did not yield a usable seed ontology; "
|
||||
"continuing with fallback normalization behavior."
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return bootstrap_ontology
|
||||
|
||||
|
||||
def make_render_ontology_node(tools: ToolBox):
|
||||
atomic_tools = tools.get_atomic_tools()
|
||||
|
||||
async def render_ontology_updates(state: AgentState) -> AgentState:
|
||||
if not state.content_units:
|
||||
state.ontology_units = []
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
worker_limit = max(1, tools.config.server.parallel_workers)
|
||||
semaphore = asyncio.Semaphore(worker_limit)
|
||||
|
||||
async def process_unit(unit_index: int) -> tuple[int, UnitOntologyState]:
|
||||
async with semaphore:
|
||||
base_state = state.model_copy(deep=True)
|
||||
ontology_state = UnitOntologyState(
|
||||
content_unit=state.content_units[unit_index],
|
||||
ontology_snapshot=state.current_ontology,
|
||||
ontology_user_instruction=state.ontology_user_instruction,
|
||||
budget_tracker=base_state.budget_tracker,
|
||||
max_visits_per_node=tools.config.server.max_visits_per_node,
|
||||
current_domain=state.current_domain,
|
||||
ontology_max_triples=tools.config.server.ontology_max_triples,
|
||||
)
|
||||
result = await ontology_loop(ontology_state, atomic_tools)
|
||||
return unit_index, result
|
||||
|
||||
tasks = [process_unit(i) for i, _ in enumerate(state.content_units)]
|
||||
raw_results = await asyncio.gather(*tasks)
|
||||
ordered_results = sorted(raw_results, key=lambda item: item[0])
|
||||
|
||||
ontology_units: list[ContentUnit] = []
|
||||
failed_without_output_count = 0
|
||||
salvaged_failed_count = 0
|
||||
for _, result in ordered_results:
|
||||
has_output = bool(result.all_updates) or (
|
||||
result.current_ontology.hash != result.ontology_snapshot.hash
|
||||
)
|
||||
if not has_output:
|
||||
failed_without_output_count += 1
|
||||
continue
|
||||
|
||||
content_unit = result.content_unit
|
||||
delta_graph = build_ontology_delta_graph(result)
|
||||
ontology_units.append(
|
||||
ContentUnit(
|
||||
text=content_unit.text,
|
||||
index=content_unit.index,
|
||||
doc_iri=content_unit.doc_iri,
|
||||
graph=delta_graph,
|
||||
type=OutputType.ONTOLOGIES,
|
||||
)
|
||||
)
|
||||
if result.status != Status.SUCCESS:
|
||||
salvaged_failed_count += 1
|
||||
|
||||
if failed_without_output_count:
|
||||
logger.warning(
|
||||
"Parallel ontology map failed without usable output for "
|
||||
f"{failed_without_output_count}/{len(state.content_units)} unit(s)"
|
||||
)
|
||||
if salvaged_failed_count:
|
||||
logger.warning(
|
||||
"Parallel ontology map salvaged output from non-converged loop(s): "
|
||||
f"{salvaged_failed_count}/{len(state.content_units)} unit(s)"
|
||||
)
|
||||
|
||||
state.ontology_units = ontology_units
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return render_ontology_updates
|
||||
|
||||
|
||||
def make_normalize_ontology_node(tools: ToolBox):
|
||||
def normalize_ontology_updates(state: AgentState) -> AgentState:
|
||||
if not state.ontology_units:
|
||||
state.ontology_provenance_artifact = RDFGraph()
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
ontology, applied_updates, provenance_artifact = normalize_ontology_units(
|
||||
units=state.ontology_units,
|
||||
tools=tools,
|
||||
base_ontology=state.current_ontology
|
||||
if not state.current_ontology.is_null()
|
||||
else None,
|
||||
require_base=True,
|
||||
)
|
||||
state.current_ontology = ontology
|
||||
state.ontology_updates_applied = applied_updates
|
||||
state.ontology_provenance_artifact = provenance_artifact
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return normalize_ontology_updates
|
||||
|
||||
|
||||
def make_consolidate_ontology_node(tools: ToolBox):
|
||||
atomic_tools = tools.get_atomic_tools()
|
||||
|
||||
async def consolidate_ontology(state: AgentState) -> AgentState:
|
||||
"""Optional post-normalization ontology consolidation pass."""
|
||||
if not tools.config.server.enable_ontology_consolidation:
|
||||
logger.info(
|
||||
"Skipping ontology consolidation: enable_ontology_consolidation is false"
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
if not state.render_ontology or state.current_ontology.is_null():
|
||||
logger.info(
|
||||
"Skipping ontology consolidation: no rendered ontology snapshot available"
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
excerpt = build_document_excerpt(state).strip()
|
||||
if not excerpt:
|
||||
logger.info(
|
||||
"Skipping ontology consolidation: no usable document excerpt was produced"
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
consolidation_unit = SourceUnit(
|
||||
text=excerpt,
|
||||
index=0,
|
||||
doc_iri=state.doc_iri,
|
||||
type=OutputType.ONTOLOGIES,
|
||||
)
|
||||
consolidation_instruction = (
|
||||
"Consolidation pass: keep ontology IRI, ontology_id, and prefix unchanged. "
|
||||
"Harmonize duplicated or semantically overlapping classes/properties, "
|
||||
"normalize naming consistency, and improve hierarchy coherence."
|
||||
)
|
||||
ontology_user_instruction = (
|
||||
f"{state.ontology_user_instruction}\n\n{consolidation_instruction}".strip()
|
||||
)
|
||||
consolidation_state = UnitOntologyState(
|
||||
content_unit=consolidation_unit,
|
||||
ontology_snapshot=state.current_ontology,
|
||||
ontology_user_instruction=ontology_user_instruction,
|
||||
budget_tracker=state.budget_tracker,
|
||||
max_visits_per_node=1,
|
||||
current_domain=state.current_domain,
|
||||
ontology_max_triples=tools.config.server.ontology_max_triples,
|
||||
)
|
||||
result = await render_ontology_update(consolidation_state, atomic_tools)
|
||||
if result.status == Status.SUCCESS and not result.current_ontology.is_null():
|
||||
state.current_ontology = result.current_ontology
|
||||
state.ontology_updates_applied.extend(result.ontology_updates_applied)
|
||||
logger.info(
|
||||
f"Ontology consolidation applied {len(result.ontology_updates_applied)} "
|
||||
"update operation(s)."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Ontology consolidation was enabled but no update was applied."
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return consolidate_ontology
|
||||
|
||||
|
||||
def make_render_facts_node(tools: ToolBox):
|
||||
atomic_tools = tools.get_atomic_tools()
|
||||
|
||||
async def render_facts(state: AgentState) -> AgentState:
|
||||
if not state.content_units:
|
||||
state.parallel_facts_units = []
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
worker_limit = max(1, tools.config.server.parallel_workers)
|
||||
semaphore = asyncio.Semaphore(worker_limit)
|
||||
|
||||
async def process_unit(unit_index: int) -> tuple[int, UnitFactsState]:
|
||||
async with semaphore:
|
||||
base_state = state.model_copy(deep=True)
|
||||
facts_state = UnitFactsState(
|
||||
content_unit=state.content_units[unit_index],
|
||||
ontology_snapshot=state.current_ontology,
|
||||
facts_user_instruction=state.facts_user_instruction,
|
||||
budget_tracker=base_state.budget_tracker,
|
||||
max_visits_per_node=tools.config.server.max_visits_per_node,
|
||||
)
|
||||
result = await facts_loop(facts_state, atomic_tools)
|
||||
return unit_index, result
|
||||
|
||||
tasks = [process_unit(i) for i, _ in enumerate(state.content_units)]
|
||||
raw_results = await asyncio.gather(*tasks)
|
||||
ordered_results = sorted(raw_results, key=lambda item: item[0])
|
||||
|
||||
facts_units: list[ContentUnit] = []
|
||||
failed_without_output_count = 0
|
||||
salvaged_failed_count = 0
|
||||
for _, result in ordered_results:
|
||||
has_output = len(result.content_unit.graph) > 0
|
||||
if not has_output:
|
||||
failed_without_output_count += 1
|
||||
continue
|
||||
|
||||
facts_units.append(result.content_unit)
|
||||
if result.status != Status.SUCCESS:
|
||||
salvaged_failed_count += 1
|
||||
|
||||
if failed_without_output_count:
|
||||
logger.warning(
|
||||
"Parallel facts map failed without usable output for "
|
||||
f"{failed_without_output_count}/{len(state.content_units)} unit(s)"
|
||||
)
|
||||
if salvaged_failed_count:
|
||||
logger.warning(
|
||||
"Parallel facts map salvaged output from non-converged loop(s): "
|
||||
f"{salvaged_failed_count}/{len(state.content_units)} unit(s)"
|
||||
)
|
||||
|
||||
state.parallel_facts_units = facts_units
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return render_facts
|
||||
|
||||
|
||||
def make_merge_facts_node(tools: ToolBox):
|
||||
def merge_facts(state: AgentState) -> AgentState:
|
||||
if not state.parallel_facts_units:
|
||||
state.aggregated_facts = RDFGraph()
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
for unit in state.parallel_facts_units:
|
||||
unit.sanitize()
|
||||
state.aggregated_facts = tools.aggregator.aggregate_graphs(
|
||||
units=state.parallel_facts_units,
|
||||
ontology_graph=state.current_ontology.graph
|
||||
if not state.current_ontology.is_null()
|
||||
else None,
|
||||
)
|
||||
if len(state.aggregated_facts) == 0:
|
||||
logger.warning(
|
||||
"Facts aggregation produced an empty graph from "
|
||||
f"{len(state.parallel_facts_units)} successful unit(s)."
|
||||
)
|
||||
if state.source_url and state.doc_namespace:
|
||||
state.aggregated_facts.add(
|
||||
(URIRef(state.doc_namespace), DCTERMS.source, URIRef(state.source_url))
|
||||
)
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
return merge_facts
|
||||
18
참고/ontocast-main/ontocast/stategraph/routing.py
Normal file
18
참고/ontocast-main/ontocast/stategraph/routing.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from ontocast.onto.enum import WorkflowNode
|
||||
from ontocast.onto.state import AgentState
|
||||
|
||||
|
||||
def route_after_ontology_selection(state: AgentState) -> str:
|
||||
"""Route after ontology selection."""
|
||||
if not state.render_ontology:
|
||||
return WorkflowNode.RENDER_FACTS
|
||||
if state.current_ontology.is_null():
|
||||
return WorkflowNode.BOOTSTRAP_ONTOLOGY
|
||||
return WorkflowNode.RENDER_ONTOLOGY_UPDATE
|
||||
|
||||
|
||||
def route_after_ontology_consolidation(state: AgentState) -> str:
|
||||
"""Route after ontology stage: facts map if needed, else serialize."""
|
||||
if state.render_facts:
|
||||
return WorkflowNode.RENDER_FACTS
|
||||
return WorkflowNode.SERIALIZE
|
||||
75
참고/ontocast-main/ontocast/stategraph/util.py
Normal file
75
참고/ontocast-main/ontocast/stategraph/util.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from ontocast.onto.enum import Status, WorkflowNode
|
||||
from ontocast.onto.state import AgentState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def count_visits_conditional_success(
|
||||
state: AgentState, current_node: WorkflowNode
|
||||
) -> AgentState:
|
||||
"""Track node visits and handle success/failure conditions.
|
||||
|
||||
This function increments the visit counter for a node and manages the state
|
||||
based on success/failure conditions and maximum visit limits.
|
||||
|
||||
Args:
|
||||
state: The current agent state.
|
||||
current_node: The node being visited.
|
||||
|
||||
Returns:
|
||||
AgentState: Updated agent state after processing visit conditions.
|
||||
"""
|
||||
state.node_visits[current_node] += 1
|
||||
if state.status == Status.SUCCESS:
|
||||
logger.info(f"For {current_node}: status is SUCCESS, proceeding to next node")
|
||||
state.clear_failure()
|
||||
elif state.node_visits[current_node] >= state.max_visits:
|
||||
logger.info(f"For {current_node}: maximum visits exceeded")
|
||||
# Don't set failure stage since we're continuing with SUCCESS status
|
||||
# Just log the reason and continue
|
||||
state.failure_reason = f"Maximum visits exceeded for {current_node}"
|
||||
state.status = Status.SUCCESS
|
||||
return state
|
||||
|
||||
|
||||
def wrap_with(func, node_name, post_func) -> tuple[WorkflowNode, Callable]:
|
||||
"""Add a visit counter to a function.
|
||||
|
||||
This function wraps a given function with logging and post-processing
|
||||
functionality, typically used for workflow node execution.
|
||||
|
||||
Args:
|
||||
func: The function to wrap (can be sync or async).
|
||||
node_name: The name of the node.
|
||||
post_func: Function to execute after the main function.
|
||||
|
||||
Returns:
|
||||
tuple[WorkflowNode, Callable]: A tuple containing the node name and
|
||||
the wrapped function.
|
||||
"""
|
||||
# Check if the function is async
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(state: AgentState):
|
||||
logger.info(f"Starting to execute {node_name}")
|
||||
state = await func(state)
|
||||
state = post_func(state, node_name)
|
||||
return state
|
||||
|
||||
return node_name, async_wrapper
|
||||
else:
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(state: AgentState):
|
||||
logger.info(f"Starting to execute {node_name}")
|
||||
state = func(state)
|
||||
state = post_func(state, node_name)
|
||||
return state
|
||||
|
||||
return node_name, sync_wrapper
|
||||
Reference in New Issue
Block a user