From b574e233cf1230b61a9368062d9440cd161e09b4 Mon Sep 17 00:00:00 2001 From: Harshit Rohatgi Date: Wed, 15 Jul 2026 22:01:01 +0530 Subject: [PATCH] feat: add S2P EoG sample agent and stub eog package Add a sample agent at examples/s2p-ontology/agent/ demonstrating the EoG (Explanations over Graphs) pattern on the S2P procurement ontology. Includes graph.py, test script, langgraph.json, and env example. Also adds stub types at src/uipath_langchain/agent/eog/ (OntologyClient, InvestigationConfig, Belief, EoGState, create_eog_agent) so downstream code is importable before the real WU2 implementation lands. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/s2p-ontology/agent/.env.example | 15 +++ examples/s2p-ontology/agent/README.md | 72 +++++++++++++ examples/s2p-ontology/agent/graph.py | 68 ++++++++++++ examples/s2p-ontology/agent/langgraph.json | 6 ++ examples/s2p-ontology/agent/test_eog.py | 103 ++++++++++++++++++ src/uipath_langchain/agent/eog/__init__.py | 117 +++++++++++++++++++++ 6 files changed, 381 insertions(+) create mode 100644 examples/s2p-ontology/agent/.env.example create mode 100644 examples/s2p-ontology/agent/README.md create mode 100644 examples/s2p-ontology/agent/graph.py create mode 100644 examples/s2p-ontology/agent/langgraph.json create mode 100644 examples/s2p-ontology/agent/test_eog.py create mode 100644 src/uipath_langchain/agent/eog/__init__.py diff --git a/examples/s2p-ontology/agent/.env.example b/examples/s2p-ontology/agent/.env.example new file mode 100644 index 000000000..a71863851 --- /dev/null +++ b/examples/s2p-ontology/agent/.env.example @@ -0,0 +1,15 @@ +# Ontology Runtime +ONTOLOGY_BASE_URL=http://localhost:5002 +ONTOLOGY_ACCOUNT=datafabric +ONTOLOGY_TENANT=DefaultTenant +ONTOLOGY_NAME=s2p + +# LLM (choose one) +LLM_MODEL=gpt-4o-2024-08-06 + +# For UiPath Chat (if using UiPath LLM gateway) +# UIPATH_URL=https://cloud.uipath.com +# UIPATH_TENANT_ID=... + +# For direct OpenAI +# OPENAI_API_KEY=sk-... diff --git a/examples/s2p-ontology/agent/README.md b/examples/s2p-ontology/agent/README.md new file mode 100644 index 000000000..7a4dbd273 --- /dev/null +++ b/examples/s2p-ontology/agent/README.md @@ -0,0 +1,72 @@ +# S2P EoG Investigation Agent + +Sample agent demonstrating the **EoG (Explanations over Graphs)** pattern on +the Source-to-Pay (S2P) procurement ontology. + +The agent connects to a local ontology-runtime, queries the S2P knowledge graph, +and iteratively builds an *explanatory frontier* -- a set of labelled beliefs +that diagnose procurement issues such as tolerance exceptions, maverick spend, +and invoice mismatches. + +## Prerequisites + +1. **FQS mock** running (provides the SPARQL/query backend): + + ```bash + cd ../fqs-data && ./fqs_start.sh + ``` + +2. **Ontology-runtime** running: + + ```bash + java -jar ontology-app.jar --spring.profiles.active=local + ``` + +3. **S2P ontology deployed**: + + ```bash + cd ../scripts && ./run-e2e.sh + ``` + +4. **Environment variables** -- copy `.env.example` to `.env` and fill in values + (or export them directly): + + ```bash + cp .env.example .env + ``` + +## Running the test script + +```bash +cd examples/s2p-ontology/agent +python test_eog.py +``` + +## Expected output + +The script prints a step-by-step ledger of label assignments, the final belief +map for each investigated entity, and a summary frontier of findings. Example +(abbreviated): + +``` +S2P EoG Investigation Agent -- Test Run +============================================================ +Seeding investigation with: ['ToleranceException', 'Invoice', ...] +------------------------------------------------------------ + +INVESTIGATION COMPLETE +============================================================ +Ledger (12 entries): + [inv-001] -- -> Source: Invoice amount exceeds PO by 15% + ... +Final Beliefs: + [inv-001] Source: Invoice amount exceeds PO by 15% + [po-042] DerivedEffect: PO linked to flagged supplier + ... +Steps taken: 12 +``` + +## LangGraph configuration + +`langgraph.json` exposes the compiled graph as `agent` so it can be served by +the LangGraph CLI or deployed to UiPath Cloud via `uipath run`. diff --git a/examples/s2p-ontology/agent/graph.py b/examples/s2p-ontology/agent/graph.py new file mode 100644 index 000000000..4e9f0f314 --- /dev/null +++ b/examples/s2p-ontology/agent/graph.py @@ -0,0 +1,68 @@ +"""S2P EoG Investigation Agent. + +Demonstrates the EoG (Explanations over Graphs) pattern on the S2P procurement +ontology. Connects to a local ontology-runtime and uses graph-guided investigation +to diagnose procurement issues (tolerance exceptions, maverick spend, etc.). +""" + +import os + +from uipath_langchain.agent.eog import ( + InvestigationConfig, + OntologyClient, + create_eog_agent, +) + +# LLM -- support both UiPath Chat and direct OpenAI +try: + from uipath_langchain.chat import UiPathChat + + llm = UiPathChat( + model=os.getenv("LLM_MODEL", "gpt-4o-2024-08-06"), + temperature=0.0, + ) +except Exception: + from langchain_openai import ChatOpenAI + + llm = ChatOpenAI( + model=os.getenv("LLM_MODEL", "gpt-4o-2024-08-06"), + temperature=0.0, + ) + +# Ontology client +client = OntologyClient( + base_url=os.getenv("ONTOLOGY_BASE_URL", "http://localhost:5002"), + account=os.getenv("ONTOLOGY_ACCOUNT", "datafabric"), + tenant=os.getenv("ONTOLOGY_TENANT", "DefaultTenant"), +) + +# S2P investigation configuration +S2P_LABELS = [ + "Source", # Root cause / origin of the issue + "DerivedEffect", # Consequence of an upstream issue + "PolicyViolation", # SHACL/business rule violation + "CandidateMatch", # Potential resolution or match + "SupportingEvidence", # Corroborating data point + "Contradiction", # Data that conflicts with the emerging explanation + "Defer", # Insufficient evidence to classify +] + +investigation_config = InvestigationConfig( + label_vocabulary=S2P_LABELS, + seed_entities=[], # Populated by seed function or at invocation + max_steps=30, + max_flips=3, + default_label="Defer", + max_results_per_function=50, +) + +# Create EoG agent (returns uncompiled StateGraph) +eog_agent = create_eog_agent( + model=llm, + ontology_client=client, + ontology_name=os.getenv("ONTOLOGY_NAME", "s2p"), + investigation_config=investigation_config, +) + +# Compile and export +graph = eog_agent.compile() diff --git a/examples/s2p-ontology/agent/langgraph.json b/examples/s2p-ontology/agent/langgraph.json new file mode 100644 index 000000000..765359577 --- /dev/null +++ b/examples/s2p-ontology/agent/langgraph.json @@ -0,0 +1,6 @@ +{ + "dependencies": ["."], + "graphs": { + "agent": "./graph.py:graph" + } +} diff --git a/examples/s2p-ontology/agent/test_eog.py b/examples/s2p-ontology/agent/test_eog.py new file mode 100644 index 000000000..13e41a9ee --- /dev/null +++ b/examples/s2p-ontology/agent/test_eog.py @@ -0,0 +1,103 @@ +"""Test script for the S2P EoG investigation agent. + +Prerequisites: + 1. FQS mock running: cd ../fqs-data && ./fqs_start.sh + 2. Ontology-runtime running: + java -jar ontology-app.jar --spring.profiles.active=local + 3. S2P ontology deployed: cd ../scripts && ./run-e2e.sh + +Run: + python test_eog.py +""" + +from __future__ import annotations + +import asyncio +import sys +from typing import Any + + +async def main() -> None: + """Run an end-to-end EoG investigation on the S2P ontology.""" + # Import the compiled graph and its config from the sibling module. + from graph import graph, investigation_config + from uipath_langchain.agent.eog import InvestigationConfig + + print("=" * 60) + print("S2P EoG Investigation Agent -- Test Run") + print("=" * 60) + + # Seed the investigation with entity types that have open exceptions. + seed_entities = [ + "ToleranceException", + "Invoice", + "PurchaseOrder", + "Supplier", + ] + + config = InvestigationConfig( + label_vocabulary=investigation_config.label_vocabulary, + seed_entities=seed_entities, + max_steps=investigation_config.max_steps, + max_flips=investigation_config.max_flips, + default_label=investigation_config.default_label, + max_results_per_function=investigation_config.max_results_per_function, + ) + + print(f"\nSeeding investigation with: {seed_entities}") + print(f"Label vocabulary: {config.label_vocabulary}") + print(f"Max steps: {config.max_steps}") + print("-" * 60) + + try: + result: dict[str, Any] = await graph.ainvoke( + {"investigation_config": config}, + ) + + print("\n" + "=" * 60) + print("INVESTIGATION COMPLETE") + print("=" * 60) + + # Print ledger (step-by-step trace) + if "ledger" in result: + print(f"\nLedger ({len(result['ledger'])} entries):") + for entry in result["ledger"]: + old = entry.get("old_label", "--") + new = entry.get("new_label", "?") + entity = entry.get("entity_id", "?") + evidence = entry.get("evidence", "")[:80] + print(f" [{entity}] {old} -> {new}: {evidence}") + + # Print beliefs + if "beliefs" in result: + print("\nFinal Beliefs:") + for entity, belief in result["beliefs"].items(): + if isinstance(belief, dict): + label = belief.get("label", "?") + evidence = belief.get("evidence", "")[:80] + else: + label = belief.label + evidence = belief.evidence[:80] + print(f" [{entity}] {label}: {evidence}") + + # Print frontier + if "frontier" in result: + print( + f"\nExplanatory Frontier" + f" ({len(result['frontier'])} findings):" + ) + for item in result["frontier"]: + print(f" - {item}") + + print(f"\nSteps taken: {result.get('steps_taken', '?')}") + + except Exception as exc: + print(f"\nError: {exc}", file=sys.stderr) + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/uipath_langchain/agent/eog/__init__.py b/src/uipath_langchain/agent/eog/__init__.py new file mode 100644 index 000000000..0e3398489 --- /dev/null +++ b/src/uipath_langchain/agent/eog/__init__.py @@ -0,0 +1,117 @@ +"""EoG (Explanations over Graphs) agent package. + +Stubs for the EoG agent API. The real implementation is being built in +parallel (WU2). These placeholders let downstream code import and type-check +against the expected surface. +""" + +from __future__ import annotations + +from typing import Any, Optional + +from langgraph.graph import StateGraph +from pydantic import BaseModel, Field + + +class OntologyClient: + """HTTP client for the UiPath Ontology Runtime. + + Args: + base_url: Root URL of the ontology-runtime service. + account: UiPath account (logical tenant group). + tenant: UiPath tenant within the account. + """ + + def __init__( + self, + base_url: str, + account: str, + tenant: str, + ) -> None: + self.base_url = base_url + self.account = account + self.tenant = tenant + + +class InvestigationConfig(BaseModel): + """Configuration that governs a single EoG investigation run.""" + + label_vocabulary: list[str] = Field( + default_factory=list, + description="Allowed belief labels the agent can assign.", + ) + seed_entities: list[str] = Field( + default_factory=list, + description="Entity types or IRIs that seed the investigation.", + ) + max_steps: int = Field( + default=30, + description="Maximum investigation steps before forced termination.", + ) + max_flips: int = Field( + default=3, + description="Maximum label flips per entity before it is frozen.", + ) + default_label: str = Field( + default="Defer", + description="Label assigned when evidence is insufficient.", + ) + max_results_per_function: int = Field( + default=50, + description="Cap on rows returned by any single ontology query.", + ) + + +class Belief(BaseModel): + """A belief about a single entity in the investigation.""" + + label: str = Field(description="Current classification label.") + evidence: str = Field( + default="", description="Free-text evidence supporting the label." + ) + flip_count: int = Field( + default=0, description="Number of times the label has changed." + ) + + +class EoGState(BaseModel): + """Minimal state schema for an EoG investigation graph.""" + + investigation_config: Optional[InvestigationConfig] = None + beliefs: dict[str, Belief] = Field(default_factory=dict) + ledger: list[dict[str, Any]] = Field(default_factory=list) + frontier: list[str] = Field(default_factory=list) + steps_taken: int = 0 + + +def create_eog_agent( + model: Any, + ontology_client: OntologyClient, + ontology_name: str, + *, + investigation_config: InvestigationConfig, +) -> StateGraph: + """Build an EoG investigation graph (uncompiled). + + Args: + model: LangChain chat model used for reasoning. + ontology_client: Client connected to the ontology-runtime. + ontology_name: Name of the deployed ontology to query. + investigation_config: Parameters controlling the investigation. + + Returns: + An uncompiled ``StateGraph`` ready for ``.compile()``. + """ + raise NotImplementedError( + "create_eog_agent is a stub. " + "Install the real eog package (WU2) to use this function." + ) + + +__all__ = [ + "Belief", + "EoGState", + "InvestigationConfig", + "OntologyClient", + "create_eog_agent", +]