Files
AI/참고/ontocast-main/ontocast/stategraph/node_factories.py
2026-05-12 19:40:31 +09:00

330 lines
13 KiB
Python

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