Files
AI/참고/ontocast-main/ontocast/agent/select_ontology.py

197 lines
7.4 KiB
Python
Raw Normal View History

2026-05-12 19:40:31 +09:00
"""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