Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/s2p-ontology/agent/.env.example
Original file line number Diff line number Diff line change
@@ -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-...
72 changes: 72 additions & 0 deletions examples/s2p-ontology/agent/README.md
Original file line number Diff line number Diff line change
@@ -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
```

Comment on lines +31 to +37
## Running the test script

```bash
cd examples/s2p-ontology/agent
python test_eog.py
```
Comment on lines +38 to +43

## 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`.
68 changes: 68 additions & 0 deletions examples/s2p-ontology/agent/graph.py
Original file line number Diff line number Diff line change
@@ -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,
)
Comment on lines +17 to +30

# 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,
)
Comment on lines +59 to +65

# Compile and export
graph = eog_agent.compile()
6 changes: 6 additions & 0 deletions examples/s2p-ontology/agent/langgraph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": ["."],
"graphs": {
"agent": "./graph.py:graph"
}
}
103 changes: 103 additions & 0 deletions examples/s2p-ontology/agent/test_eog.py
Original file line number Diff line number Diff line change
@@ -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())
117 changes: 117 additions & 0 deletions src/uipath_langchain/agent/eog/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
"""
Comment on lines +1 to +6

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).
Comment on lines +87 to +94

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."
)
Comment on lines +105 to +108


__all__ = [
"Belief",
"EoGState",
"InvestigationConfig",
"OntologyClient",
"create_eog_agent",
]
Loading