Skip to content

Commit 26b040f

Browse files
author
AgentPatterns
committed
feat(examples/python): add runnable example
1 parent 3f73405 commit 26b040f

File tree

3 files changed

+46
-29
lines changed

3 files changed

+46
-29
lines changed

examples/agent-patterns/routing-agent/python/gateway.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,22 @@ def _normalize_for_hash(value: Any) -> Any:
4444
return value
4545

4646

47+
def _normalize_ticket(value: str) -> str:
48+
return " ".join(value.strip().split())
49+
50+
4751
def args_hash(args: dict[str, Any]) -> str:
4852
normalized = _normalize_for_hash(args or {})
4953
raw = _stable_json(normalized)
5054
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
5155

5256

5357
def validate_route_action(
54-
action: Any, *, allowed_routes: set[str]
58+
action: Any,
59+
*,
60+
allowed_routes: set[str],
61+
previous_target: str | None = None,
62+
previous_status: str | None = None,
5563
) -> dict[str, Any]:
5664
if not isinstance(action, dict):
5765
raise StopRun("invalid_route:not_object")
@@ -81,8 +89,13 @@ def validate_route_action(
8189
ticket = args.get("ticket")
8290
if not isinstance(ticket, str) or not ticket.strip():
8391
raise StopRun("invalid_route:missing_ticket")
92+
ticket = _normalize_ticket(ticket)
93+
normalized_args = {**args, "ticket": ticket}
94+
95+
if previous_status == "needs_reroute" and target == previous_target:
96+
raise StopRun("invalid_route:repeat_target_after_reroute")
8497

85-
return {"kind": "route", "target": target, "args": args}
98+
return {"kind": "route", "target": target, "args": normalized_args}
8699

87100

88101
class RouteGateway:

examples/agent-patterns/routing-agent/python/llm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class LLMEmpty(Exception):
2525
2626
Rules:
2727
- Choose exactly one target from available_routes.
28+
- Never choose targets from forbidden_targets.
2829
- Keep args minimal and valid for that target.
2930
- If previous attempts failed with needs_reroute, choose a different target.
3031
- Respect routing budgets and avoid unnecessary retries.
@@ -97,6 +98,7 @@ def decide_route(
9798
*,
9899
max_route_attempts: int,
99100
remaining_attempts: int,
101+
forbidden_targets: list[str],
100102
) -> dict[str, Any]:
101103
recent_history = history[-3:]
102104
payload = {
@@ -105,6 +107,7 @@ def decide_route(
105107
"max_route_attempts": max_route_attempts,
106108
"remaining_attempts": remaining_attempts,
107109
},
110+
"forbidden_targets": forbidden_targets,
108111
"state_summary": _build_state_summary(history),
109112
"recent_history": recent_history,
110113
"available_routes": ROUTE_CATALOG,

examples/agent-patterns/routing-agent/python/main.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,36 @@ def run_routing(goal: str) -> dict[str, Any]:
5555
"history": history,
5656
}
5757

58+
previous_step = history[-1] if history else None
59+
previous_observation = (
60+
previous_step.get("observation")
61+
if isinstance(previous_step, dict)
62+
else None
63+
)
64+
previous_route = previous_step.get("route") if isinstance(previous_step, dict) else None
65+
previous_status = (
66+
previous_observation.get("status")
67+
if isinstance(previous_observation, dict)
68+
else None
69+
)
70+
previous_target = (
71+
previous_route.get("target")
72+
if isinstance(previous_route, dict)
73+
else None
74+
)
75+
forbidden_targets = (
76+
[previous_target]
77+
if previous_status == "needs_reroute" and isinstance(previous_target, str)
78+
else []
79+
)
80+
5881
try:
5982
raw_route = decide_route(
6083
goal=goal,
6184
history=history,
6285
max_route_attempts=BUDGET.max_route_attempts,
6386
remaining_attempts=(BUDGET.max_route_attempts - attempt + 1),
87+
forbidden_targets=forbidden_targets,
6488
)
6589
except LLMTimeout:
6690
return {
@@ -75,6 +99,8 @@ def run_routing(goal: str) -> dict[str, Any]:
7599
route_action = validate_route_action(
76100
raw_route,
77101
allowed_routes=ALLOWED_ROUTE_TARGETS_POLICY,
102+
previous_target=previous_target,
103+
previous_status=previous_status,
78104
)
79105
except StopRun as exc:
80106
return {
@@ -88,33 +114,6 @@ def run_routing(goal: str) -> dict[str, Any]:
88114

89115
target = route_action["target"]
90116
route_args = route_action["args"]
91-
previous_step = history[-1] if history else None
92-
previous_observation = (
93-
previous_step.get("observation")
94-
if isinstance(previous_step, dict)
95-
else None
96-
)
97-
previous_route = previous_step.get("route") if isinstance(previous_step, dict) else None
98-
previous_status = (
99-
previous_observation.get("status")
100-
if isinstance(previous_observation, dict)
101-
else None
102-
)
103-
previous_target = (
104-
previous_route.get("target")
105-
if isinstance(previous_route, dict)
106-
else None
107-
)
108-
109-
if previous_status == "needs_reroute" and target == previous_target:
110-
return {
111-
"status": "stopped",
112-
"stop_reason": "invalid_route:repeat_target_after_reroute",
113-
"phase": "route",
114-
"route": route_action,
115-
"trace": trace,
116-
"history": history,
117-
}
118117

119118
try:
120119
observation = gateway.call(target, route_args)
@@ -166,6 +165,8 @@ def run_routing(goal: str) -> dict[str, Any]:
166165
"stop_reason": "route_bad_observation",
167166
"phase": "delegate",
168167
"route": route_action,
168+
"expected_statuses": ["needs_reroute", "done"],
169+
"received_status": observation_status,
169170
"bad_observation": observation,
170171
"trace": trace,
171172
"history": history,

0 commit comments

Comments
 (0)