import os from pathlib import Path import pytest from rdflib import URIRef from ontocast.agent.criticise_facts import criticise_facts from ontocast.agent.criticise_ontology import criticise_ontology from ontocast.agent.render_facts import render_facts from ontocast.agent.render_ontology import render_ontology from ontocast.config import Config, LLMProvider from ontocast.onto.content_unit import ContentUnit from ontocast.onto.enum import Status from ontocast.onto.ontology import Ontology from ontocast.onto.rdfgraph import RDFGraph from ontocast.onto.unit_states import UnitFactsState, UnitOntologyState from ontocast.toolbox import ToolBox RUN_MANUAL_TESTS = os.getenv("ONTOCAST_RUN_MANUAL_TESTS", "0") == "1" pytestmark = [ pytest.mark.skipif( not RUN_MANUAL_TESTS, reason="Set ONTOCAST_RUN_MANUAL_TESTS=1 to run live manual tests.", ), ] def _require_env(name: str) -> str: value = os.getenv(name) if value is None or value.strip() == "": pytest.fail(f"Missing required environment variable: {name}") return value def _create_tools_from_env() -> ToolBox: _ = _require_env("LLM_PROVIDER") _ = _require_env("LLM_MODEL_NAME") provider = LLMProvider(_require_env("LLM_PROVIDER").lower()) if provider == LLMProvider.OPENAI: _ = _require_env("LLM_API_KEY") elif provider == LLMProvider.OLLAMA: _ = _require_env("LLM_BASE_URL") _ = _require_env("ONTOCAST_WORKING_DIRECTORY") config = Config() config.validate_llm_config() if config.tool_config.path_config.working_directory is None: pytest.fail("ONTOCAST_WORKING_DIRECTORY must be set to run manual agent tests.") config.tool_config.path_config.working_directory = Path( config.tool_config.path_config.working_directory ).expanduser() config.tool_config.path_config.working_directory.mkdir(parents=True, exist_ok=True) if config.tool_config.path_config.ontology_directory is not None: config.tool_config.path_config.ontology_directory = Path( config.tool_config.path_config.ontology_directory ).expanduser() return ToolBox(config) @pytest.fixture(scope="module") def live_tools() -> ToolBox: return _create_tools_from_env() @pytest.fixture def realistic_text() -> str: return ( "ACME Robotics announced that it signed a three-year collaboration with " "North Valley Hospital in Berlin to deploy autonomous delivery carts across " "seven departments. The pilot starts in May 2026 and is co-funded by the " "hospital innovation office and the regional health authority. " "The agreement names Dr. Lena Fischer as clinical lead and ACME CTO " "Rahul Mehta as technical lead. Success metrics include a 20 percent " "reduction in nurse walking distance, fewer late medication rounds, and " "weekly safety audits." ) def _build_seed_ontology() -> Ontology: graph = RDFGraph() graph.parse( data=""" @prefix ex: . @prefix owl: . @prefix rdfs: . ex:health a owl:Ontology ; rdfs:label "Healthcare Collaboration Ontology" . ex:Organization a rdfs:Class . ex:Hospital a rdfs:Class ; rdfs:subClassOf ex:Organization . ex:Company a rdfs:Class ; rdfs:subClassOf ex:Organization . ex:Person a rdfs:Class . ex:collaboratesWith a owl:ObjectProperty . ex:hasLead a owl:ObjectProperty . ex:locatedIn a owl:ObjectProperty . """, format="turtle", ) return Ontology(graph=graph, iri="https://example.com/health") def _build_content_unit(text: str, with_seed_facts: bool = False) -> ContentUnit: unit = ContentUnit( text=text, index=0, doc_iri=URIRef("https://example.com/doc/manual-live"), ) if with_seed_facts: unit.graph.parse( data=""" @prefix ex: . @prefix facts: . facts:acme ex:collaboratesWith facts:north_valley_hospital . """, format="turtle", ) return unit @pytest.mark.anyio async def test_render_facts_live_llm(live_tools: ToolBox, realistic_text: str) -> None: state = UnitFactsState( content_unit=_build_content_unit(realistic_text), ontology_snapshot=_build_seed_ontology(), facts_user_instruction=( "Extract organizations, people, timeline details, and measurable targets." ), ) result = await render_facts(state, live_tools.get_atomic_tools()) assert result.failure_stage is None assert result.status == Status.SUCCESS assert len(result.content_unit.graph) > 0 assert result.budget_tracker.calls_count > 0 @pytest.mark.anyio async def test_criticise_facts_live_llm( live_tools: ToolBox, realistic_text: str ) -> None: state = UnitFactsState( content_unit=_build_content_unit(realistic_text), ontology_snapshot=_build_seed_ontology(), facts_user_instruction=( "Prioritize correct entities, relations, and measurable outcomes." ), ) rendered = await render_facts(state, live_tools.get_atomic_tools()) assert len(rendered.content_unit.graph) > 0 critiqued = await criticise_facts(rendered, live_tools.get_atomic_tools()) assert ( critiqued.failure_stage is None or critiqued.failure_stage.name == "FACTS_CRITIQUE" ) assert critiqued.status in (Status.SUCCESS, Status.FAILED) assert critiqued.budget_tracker.calls_count > 0 @pytest.mark.anyio async def test_render_ontology_live_llm( live_tools: ToolBox, realistic_text: str ) -> None: null_ontology = Ontology() state = UnitOntologyState( content_unit=_build_content_unit(realistic_text), ontology_snapshot=null_ontology, ontology_user_instruction=( "Create a compact ontology for healthcare logistics collaboration." ), ) result = await render_ontology(state, live_tools.get_atomic_tools()) assert result.failure_stage is None assert result.status == Status.SUCCESS assert not result.current_ontology.is_null() assert len(result.current_ontology.graph) > 0 assert result.budget_tracker.calls_count > 0 @pytest.mark.anyio async def test_criticise_ontology_live_llm( live_tools: ToolBox, realistic_text: str ) -> None: null_ontology = Ontology() state = UnitOntologyState( content_unit=_build_content_unit(realistic_text), ontology_snapshot=null_ontology, ontology_user_instruction=( "Keep class hierarchy minimal and ensure relation naming consistency." ), ) rendered = await render_ontology(state, live_tools.get_atomic_tools()) assert not rendered.current_ontology.is_null() assert len(rendered.current_ontology.graph) > 0 critiqued = await criticise_ontology(rendered, live_tools.get_atomic_tools()) assert ( critiqued.failure_stage is None or critiqued.failure_stage.name == "ONTOLOGY_CRITIQUE" ) assert critiqued.status in (Status.SUCCESS, Status.FAILED) assert critiqued.budget_tracker.calls_count > 0