참고소스 수정본
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user