참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View 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",
]

View 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

View 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()

View 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 ""

View 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

View 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

View 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