Skip to content

Commit 9a4075e

Browse files
committed
test(uipath-agents): four coded-framework e2e tests
Adds zero-to-running tests for the four coded-agent frameworks the skill teaches, each going scaffold -> uip codedagent init -> uip codedagent run end-to-end: - skill-agent-coded-simple-echo (smoke) — Pydantic Input/Output, no LLM, deterministic; runs the agent locally and asserts echoed output. - skill-agent-coded-langgraph-classifier (e2e) — StateGraph + lazy UiPathChat() inside a node, schema-sync from Pydantic GraphInput/GraphOutput. - skill-agent-coded-llamaindex-workflow (e2e) — Workflow subclass with StartEvent/StopEvent fields and lazy UiPathOpenAI inside a @step. - skill-agent-coded-openai-agents-handoff (e2e) — multi-agent triage with handoffs to billing/technical specialists, exercising the factory-function pattern (def main() -> Agent). Each task uses a goal-only prompt so the skill drives the "how". Each check script asserts: pyproject hygiene (no [build-system], authors present, framework dep declared), runtime config file shape (langgraph.json / llama_index.json / openai_agents.json / uipath.json functions), source-code shape (Pydantic models, framework-specific exports), the lazy-LLM-init invariant via _shared/ast_lazy_init_check, schema-sync between declared models and entry-points.json, and bindings.json envelope well-formedness.
1 parent 66479db commit 9a4075e

8 files changed

Lines changed: 1007 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
"""LangGraph agent project-shape check.
3+
4+
Asserts the lazy-LLM-init invariant (Critical Rule C4 — the highest-cost
5+
coded mistake the report flags) and the schema-sync invariant
6+
(`agent.json`/`entry-points.json` reflect the Pydantic Input/Output
7+
classes the agent declared).
8+
9+
Checks performed:
10+
11+
1. `support-classifier/pyproject.toml` declares `uipath-langchain`
12+
as a dependency, has `[project]` with `authors`, and contains NO
13+
`[build-system]` section.
14+
2. The project has either `langgraph.json` (Pattern A — recommended)
15+
OR `uipath.json` with a `functions.graph` entry (Pattern B). Both
16+
are valid per the LangGraph integration guide.
17+
3. `main.py` (or `graph.py`) defines `GraphInput`/`GraphOutput`
18+
Pydantic models, exports a top-level `graph` variable, and has
19+
NO module-level UiPath* construction (`UiPathChat`,
20+
`UiPathAzureChatOpenAI`, etc.).
21+
4. `entry-points.json` has one entrypoint whose schemas mention
22+
`text` (input) and `category` (output) — proves `uip codedagent
23+
init` ran AFTER the Pydantic models were written.
24+
5. `bindings.json` is the v2.0 envelope (resource count is not
25+
asserted — the classifier itself uses no SDK resources).
26+
27+
Exits 0 on PASS, with a `FAIL: ...` message on the first violation.
28+
"""
29+
30+
from __future__ import annotations
31+
32+
import json
33+
import os
34+
import sys
35+
from pathlib import Path
36+
37+
ROOT = Path(os.getcwd())
38+
39+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
40+
from _shared.bindings_assertions import load_bindings # noqa: E402
41+
from _shared.ast_lazy_init_check import find_module_level_llm_clients # noqa: E402
42+
43+
44+
def _read_text(path: Path) -> str:
45+
if not path.is_file():
46+
sys.exit(f"FAIL: Missing {path}")
47+
return path.read_text(encoding="utf-8")
48+
49+
50+
def _load_json(path: Path) -> dict:
51+
raw = _read_text(path)
52+
try:
53+
return json.loads(raw)
54+
except json.JSONDecodeError as e:
55+
sys.exit(f"FAIL: {path} is not valid JSON: {e}")
56+
57+
58+
def check_pyproject() -> None:
59+
text = _read_text(ROOT / "pyproject.toml")
60+
if "[build-system]" in text:
61+
sys.exit(
62+
"FAIL: pyproject.toml contains a [build-system] section — "
63+
"Critical Rule C1 forbids it."
64+
)
65+
if "[project]" not in text or "authors" not in text:
66+
sys.exit("FAIL: pyproject.toml is missing [project] or `authors`")
67+
if "uipath-langchain" not in text:
68+
sys.exit(
69+
"FAIL: pyproject.toml does not declare `uipath-langchain` — "
70+
"the LangGraph integration guide makes this dependency mandatory."
71+
)
72+
print("OK: pyproject.toml is hygienic and declares uipath-langchain")
73+
74+
75+
def find_graph_module() -> Path:
76+
for candidate in ("main.py", "graph.py"):
77+
path = ROOT / candidate
78+
if path.is_file():
79+
return path
80+
sys.exit(
81+
"FAIL: neither main.py nor graph.py found under "
82+
f"{ROOT} — LangGraph integration guide requires one."
83+
)
84+
85+
86+
def check_graph_module(path: Path) -> None:
87+
text = _read_text(path)
88+
for needle in ("GraphInput", "GraphOutput", "graph"):
89+
if needle not in text:
90+
sys.exit(f"FAIL: {path.name} is missing `{needle}`")
91+
if "StateGraph" not in text and "CompiledStateGraph" not in text:
92+
sys.exit(f"FAIL: {path.name} does not reference StateGraph / CompiledStateGraph")
93+
print(f"OK: {path.name} defines GraphInput, GraphOutput, and a graph variable")
94+
violations = find_module_level_llm_clients(path)
95+
if violations:
96+
sys.exit("FAIL: " + " | ".join(violations))
97+
print(
98+
f"OK: {path.name} has no module-level UiPath* construction "
99+
"(lazy-LLM-init invariant holds)"
100+
)
101+
102+
103+
def check_runtime_config() -> None:
104+
langgraph_json = ROOT / "langgraph.json"
105+
uipath_json = ROOT / "uipath.json"
106+
if langgraph_json.is_file():
107+
doc = _load_json(langgraph_json)
108+
graphs = doc.get("graphs") or {}
109+
if not graphs:
110+
sys.exit("FAIL: langgraph.json has no `graphs` mapping")
111+
target = next(iter(graphs.values()))
112+
if not isinstance(target, str) or ":graph" not in target:
113+
sys.exit(
114+
f'FAIL: langgraph.json graphs entry should map to a `<file>:graph` '
115+
f'reference, got {target!r}'
116+
)
117+
print(f"OK: langgraph.json registers a graph -> {target!r}")
118+
return
119+
if uipath_json.is_file():
120+
doc = _load_json(uipath_json)
121+
functions = doc.get("functions") or {}
122+
graph_entry = functions.get("graph")
123+
if not graph_entry or ":graph" not in graph_entry:
124+
sys.exit(
125+
"FAIL: neither langgraph.json nor uipath.json `functions.graph` "
126+
"is present — the runtime cannot find the compiled graph."
127+
)
128+
print(f'OK: uipath.json registers functions.graph -> {graph_entry!r}')
129+
return
130+
sys.exit(
131+
"FAIL: project has neither langgraph.json nor uipath.json — at "
132+
"least one is required for `uip codedagent init` to succeed."
133+
)
134+
135+
136+
def check_entry_points() -> None:
137+
doc = _load_json(ROOT / "entry-points.json")
138+
entrypoints = doc.get("entryPoints") or []
139+
if not entrypoints:
140+
sys.exit("FAIL: entry-points.json has no entryPoints — `uip codedagent init` did not run successfully")
141+
raw = json.dumps(entrypoints)
142+
for field in ("text", "category"):
143+
if field not in raw:
144+
sys.exit(
145+
f'FAIL: entry-points.json schemas do not mention `{field}`. '
146+
f'Either `uip codedagent init` ran before the Pydantic models '
147+
f'were written, or the models did not declare the expected '
148+
f'fields. Got: {raw}'
149+
)
150+
print(
151+
"OK: entry-points.json schemas reflect the GraphInput/GraphOutput "
152+
"fields (text, category)"
153+
)
154+
155+
156+
def check_bindings() -> None:
157+
load_bindings(ROOT / "bindings.json")
158+
print("OK: bindings.json envelope is well-formed")
159+
160+
161+
def main() -> None:
162+
if not ROOT.is_dir():
163+
sys.exit(f"FAIL: project directory {ROOT} does not exist")
164+
check_pyproject()
165+
graph_module = find_graph_module()
166+
check_graph_module(graph_module)
167+
check_runtime_config()
168+
check_entry_points()
169+
check_bindings()
170+
171+
172+
if __name__ == "__main__":
173+
main()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
task_id: skill-agent-coded-langgraph-classifier
2+
description: >
3+
LangGraph coded agent zero-to-running. Verifies the skill guides the
4+
agent to scaffold a LangGraph project (`uv add uipath-langchain` →
5+
`uip codedagent new` → `langgraph.json` + `main.py` exporting a
6+
compiled `graph`), keep LLM construction inside a node body
7+
(Critical Rule C4 — module-level `UiPathChat()` would break
8+
`uip codedagent init` with `ImportError`), let `uip codedagent
9+
init` regenerate the schema from the actual code, and run the
10+
classifier end-to-end via `uip codedagent run agent`. Closes
11+
report Top-10 #1 (highest-priority coded gap, lazy-LLM-init
12+
invariant).
13+
tags: [uipath-agents, e2e, coded, lifecycle:generate, lifecycle:execute, feature:framework-langgraph, feature:schema-sync]
14+
max_iterations: 1
15+
16+
agent:
17+
type: claude-code
18+
permission_mode: acceptEdits
19+
allowed_tools: ["Skill", "Bash", "Read", "Write", "Edit", "Glob", "Grep"]
20+
turn_timeout: 1200
21+
22+
sandbox:
23+
driver: tempdir
24+
python: {}
25+
26+
initial_prompt: |
27+
Build a LangGraph UiPath coded agent named `support-classifier` that
28+
classifies a customer support ticket into one of three categories:
29+
`billing`, `technical`, `general`. The agent uses `UiPathChat`
30+
(routed through the UiPath LLM Gateway) for the classification.
31+
32+
Input: `{"text": "<support ticket text>"}` — a Pydantic
33+
`GraphInput` with one `text: str` field.
34+
Output: `{"category": "<billing|technical|general>", "text": "..."}`
35+
— a Pydantic `GraphOutput` with `category: str` and `text: str`.
36+
37+
Take the agent end-to-end through scaffold → init → run. Use
38+
`gpt-4o-mini-2024-07-18` (or another low-cost model the gateway
39+
exposes) and pin `temperature=0` so the classification is
40+
reproducible. Run the agent against
41+
`'{"text": "I was charged twice for my last invoice."}'` — the
42+
expected category is `billing`.
43+
44+
After `uip codedagent run agent '{"text": "..."}'` succeeds, write
45+
`RAN_OK` to `run_marker.txt` in the project root so the test
46+
harness can verify the run completed cleanly.
47+
48+
Do NOT publish, upload, or deploy. Do NOT pause between planning
49+
and implementation. Complete the entire task end-to-end in a
50+
single pass.
51+
52+
success_criteria:
53+
- type: command_executed
54+
description: "Agent installed the LangGraph framework dependency"
55+
tool_name: "Bash"
56+
command_pattern: 'uv\s+add\s+uipath-langchain'
57+
min_count: 1
58+
weight: 1.0
59+
pass_threshold: 1.0
60+
61+
- type: command_executed
62+
description: "Agent scaffolded with uip codedagent new"
63+
tool_name: "Bash"
64+
command_pattern: 'uip\s+codedagent\s+new'
65+
min_count: 1
66+
weight: 1.5
67+
pass_threshold: 1.0
68+
69+
- type: command_executed
70+
description: "Agent ran uip codedagent init to generate entry-points.json"
71+
tool_name: "Bash"
72+
command_pattern: 'uip\s+codedagent\s+init'
73+
min_count: 1
74+
weight: 2.0
75+
pass_threshold: 1.0
76+
77+
- type: command_executed
78+
description: "Agent executed the agent locally with uip codedagent run"
79+
tool_name: "Bash"
80+
command_pattern: 'uip\s+codedagent\s+run\s+agent'
81+
min_count: 1
82+
weight: 2.0
83+
pass_threshold: 1.0
84+
85+
- type: file_exists
86+
description: "Agent wrote run_marker.txt after a successful run"
87+
path: "run_marker.txt"
88+
weight: 2.0
89+
pass_threshold: 1.0
90+
91+
- type: run_command
92+
description: "LangGraph project shape, lazy-LLM-init invariant, schema sync"
93+
command: "python3 $TASK_DIR/check_langgraph_classifier.py"
94+
timeout: 30
95+
expected_exit_code: 0
96+
weight: 5.0
97+
pass_threshold: 1.0

0 commit comments

Comments
 (0)