Skip to content

ontocast.agent.render_ontology

Ontology triple rendering agent for OntoCast.

Structured hybrid renderer: bare Turtle for fresh ontologies, GraphUpdate patches for complementing an existing snapshot.

render_ontology(state, tools, supplemental_ontologies=None) async

Structured hybrid ontology renderer: fresh Turtle or structured graph updates.

Source code in ontocast/agent/render_ontology.py
async def render_ontology(
    state: UnitOntologyState,
    tools: AtomicToolBox,
    supplemental_ontologies: Sequence[Ontology] | None = None,
) -> UnitOntologyState:
    """Structured hybrid ontology renderer: fresh Turtle or structured graph updates."""

    progress_info = state.get_content_unit_progress_string()
    logger.info(
        f"Ontology Renderer for {progress_info}: visit {state.node_visits[WorkflowNode.TEXT_TO_ONTOLOGY]}/{state.max_visits_per_node}"
    )
    access = ontology_access_for_unit_ontology(state)
    has_seed = access.has_non_empty_seed()
    extras = list(supplemental_ontologies or ())
    if not has_seed:
        return await render_ontology_fresh(state, tools, supplemental_ontologies=extras)
    return await render_ontology_update(state, tools, supplemental_ontologies=extras)

render_ontology_fresh(state, tools, supplemental_ontologies=None) async

Create a brand-new catalog ontology from text (empty seed path).

Source code in ontocast/agent/render_ontology.py
async def render_ontology_fresh(
    state: UnitOntologyState,
    tools: AtomicToolBox,
    supplemental_ontologies: Sequence[Ontology] | None = None,
) -> UnitOntologyState:
    """Create a brand-new catalog ontology from text (empty seed path)."""

    profile = get_graph_format_profile(state.llm_graph_format)
    parser = PydanticOutputParser(pydantic_object=OntologyRenderReport)
    logger.info("Rendering fresh ontology")
    intro_instruction = intro_instruction_fresh.format(
        current_domain=state.current_domain
    )
    output_instruction = profile.render_fresh_output_instruction(target="ontology")
    ontology_ttl = ""
    improvement_instruction_str = ""
    access = ontology_access_for_unit_ontology(state)
    web_search_enabled = tools.web_grounding_enabled_for_node(
        WorkflowNode.TEXT_TO_ONTOLOGY
    )
    (
        general_ontology_instruction_str,
        text_chapter,
        external_evidence,
    ) = _prepare_ontology_common_prompt_layers(
        state,
        access,
        search_guidelines=search_guidelines_for(
            WorkflowNode.TEXT_TO_ONTOLOGY, web_search_enabled
        ),
    )

    prompt = _create_ontology_render_prompt_template()
    known_prefixes = build_llm_prefix_map(
        access.ontology_graph_for_prefixes(),
        supplemental_ontologies or (),
    )

    try:
        RDFGraph.set_known_prefixes(known_prefixes if known_prefixes else None)
        llm_tool = await tools.get_llm_tool(state.budget_tracker)
        render_report: OntologyRenderReport = await call_llm_with_retry(
            llm_tool=llm_tool,
            prompt=prompt,
            parser=parser,
            prompt_kwargs={
                "preamble": system_preamble,
                "intro_instruction": intro_instruction,
                "ontology_instruction": general_ontology_instruction_str,
                "output_instruction": output_instruction,
                "ontology_ttl": ontology_ttl,
                "user_instruction": state.ontology_user_instruction,
                "improvement_instruction": improvement_instruction_str,
                "text": text_chapter,
                "external_evidence": external_evidence,
                "format_instructions": profile.format_instructions(
                    OntologyRenderReport,
                    web_search_enabled=web_search_enabled,
                ),
            },
            llm_graph_format=state.llm_graph_format,
        )
        persist_search_request(
            state,
            WorkflowNode.TEXT_TO_ONTOLOGY,
            render_report.external_evidence_request,
            web_search_enabled,
        )
        ontology = render_report.ontology
        ontology.graph.sanitize_prefixes_namespaces()
        state.fresh_ontology = ontology
        state.working_graph = ontology.graph.copy()
        if ontology.iri:
            state.writable_iris = [ontology.iri]
            state.assembly_anchor_iri = ontology.iri

        num_triples = len(state.working_graph)
        logger.info(f"New ontology created with {num_triples} triple(s).")

        state.budget_tracker.add_ontology_update(
            num_operations=1, num_triples=num_triples
        )

        state.clear_failure()
        state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.SUCCESS)
        return state

    except Exception as e:
        return _handle_ontology_render_error(
            state, e, FailureStage.GENERATE_TTL_FOR_ONTOLOGY
        )
    finally:
        RDFGraph.set_known_prefixes(None)

render_ontology_update(state, tools, supplemental_ontologies=None) async

Complement an existing snapshot via GraphUpdate inserts.

Source code in ontocast/agent/render_ontology.py
async def render_ontology_update(
    state: UnitOntologyState,
    tools: AtomicToolBox,
    supplemental_ontologies: Sequence[Ontology] | None = None,
) -> UnitOntologyState:
    """Complement an existing snapshot via GraphUpdate inserts."""

    state.quarantined_literal_triples = []
    profile = get_graph_format_profile(state.llm_graph_format)
    parser = PydanticOutputParser(pydantic_object=GraphUpdateRenderReport)
    access = ontology_access_for_unit_ontology(state)
    intro_instruction = _build_update_intro(state, access)
    ontology_chapter = profile.format_ontology_chapter(
        access.effective_graph_for_prompt(),
        max_triples=state.ontology_context_max_triples,
    )
    output_instruction = profile.render_update_output_instruction()
    improvement_instruction_str = render_suggestions_prompt(
        state.suggestions, WorkflowNode.TEXT_TO_ONTOLOGY
    )

    web_search_enabled = tools.web_grounding_enabled_for_node(
        WorkflowNode.TEXT_TO_ONTOLOGY
    )
    (
        general_ontology_instruction_str,
        text_chapter,
        external_evidence,
    ) = _prepare_ontology_common_prompt_layers(
        state,
        access,
        search_guidelines=search_guidelines_for(
            WorkflowNode.TEXT_TO_ONTOLOGY, web_search_enabled
        ),
    )

    prompt = _create_ontology_render_prompt_template()
    known_prefixes = build_llm_prefix_map(
        access.ontology_graph_for_prefixes(),
        supplemental_ontologies or (),
    )

    try:
        llm_tool = await tools.get_llm_tool(state.budget_tracker)
        RDFGraph.set_known_prefixes(known_prefixes if known_prefixes else None)

        render_report: GraphUpdateRenderReport = await call_llm_with_retry(
            llm_tool=llm_tool,
            prompt=prompt,
            parser=parser,
            prompt_kwargs={
                "preamble": system_preamble,
                "intro_instruction": intro_instruction,
                "ontology_instruction": general_ontology_instruction_str,
                "output_instruction": output_instruction,
                "improvement_instruction": improvement_instruction_str,
                "ontology_ttl": ontology_chapter,
                "user_instruction": state.ontology_user_instruction,
                "text": text_chapter,
                "external_evidence": external_evidence,
                "format_instructions": profile.format_instructions(
                    GraphUpdateRenderReport,
                    web_search_enabled=web_search_enabled,
                ),
            },
            llm_graph_format=state.llm_graph_format,
        )
        persist_search_request(
            state,
            WorkflowNode.TEXT_TO_ONTOLOGY,
            render_report.external_evidence_request,
            web_search_enabled,
        )
        # No insert_hook: the facts repairs are instance-level (literal retyping,
        # unit-code resolution) and have no ontology counterpart. The ontology
        # side's deterministic validator runs in the loop instead, against the
        # net delta -- see stategraph/atomic.py::_collect_ontology_findings.
        graph_update, rejected = finalize_update_report(render_report)
        state.quarantined_literal_triples = rejected
        log_quarantine("Ontology", rejected)
        state.ontology_updates.append(graph_update)
        applied = state.update_ontology()
        if not applied:
            # The ONTOLOGY_MAX_TRIPLES backstop discarded the whole update, so
            # this billed render changed nothing. Returning SUCCESS with an
            # unchanged graph is not a lie the run can see, and a validator run
            # afterwards would inspect the *previous* graph and report it clean.
            # The status stays SUCCESS -- the pre-update graph is intact and a
            # re-render would hit the same ceiling -- but the discard is counted.
            logger.warning(
                "Ontology update discarded: applying it would exceed "
                "ONTOLOGY_MAX_TRIPLES=%s. The render was billed and had no "
                "effect on the working graph.",
                state.ontology_max_triples,
            )
            state.budget_tracker.incr("ontology/update_rejected_over_budget")
        # Suggestions are consumed by exactly the render they were raised
        # against. Leaving them set carried them into every later render of the
        # unit -- the leak that put two contradictory contracts in one facts
        # prompt (see CHANGELOG [Unreleased]); the ontology path had the same
        # defect and no repair pass to notice it.
        state.suggestions = Suggestions()
        # Findings were consumed by this render; the loop re-collects fresh.
        state.deterministic_findings = []

        num_operations, num_triples = graph_update.count_total_triples()
        logger.info(
            f"Ontology update has {num_operations} operation(s) "
            f"with {num_triples} total triple(s)."
        )

        state.budget_tracker.add_ontology_update(num_operations, num_triples)

        state.clear_failure()
        state.set_node_status(WorkflowNode.TEXT_TO_ONTOLOGY, Status.SUCCESS)
        return state

    except Exception as e:
        return _handle_ontology_render_error(
            state, e, FailureStage.GENERATE_GRAPH_UPDATE_FOR_ONTOLOGY
        )
    finally:
        RDFGraph.set_known_prefixes(None)