Skip to content

Commit c2887b8

Browse files
author
AgentPatterns
committed
feat(examples/python): add stop-conditions runnable example
1 parent 8a495a0 commit c2887b8

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from dataclasses import dataclass
2+
from typing import Any
3+
4+
from llm import choose_next_action
5+
from tools import build_summary, fetch_orders, make_initial_state
6+
7+
TOOLS = {
8+
"fetch_orders": fetch_orders,
9+
"build_summary": build_summary,
10+
}
11+
12+
13+
@dataclass
14+
class StopPolicy:
15+
max_steps: int
16+
max_errors: int
17+
max_no_progress: int
18+
19+
20+
def evaluate_stop_conditions(
21+
state: dict[str, Any],
22+
steps: int,
23+
errors: int,
24+
no_progress: int,
25+
policy: StopPolicy,
26+
) -> str | None:
27+
if "summary" in state:
28+
return "goal_reached"
29+
if steps >= policy.max_steps:
30+
return "step_limit"
31+
if errors >= policy.max_errors:
32+
return "too_many_errors"
33+
if no_progress >= policy.max_no_progress:
34+
return "no_progress"
35+
return None
36+
37+
38+
def run_agent(task: str, user_id: int, fail_fetch_times: int, policy: StopPolicy) -> dict[str, Any]:
39+
state = make_initial_state(user_id=user_id, fail_fetch_times=fail_fetch_times)
40+
history: list[dict[str, Any]] = []
41+
42+
steps = 0
43+
errors = 0
44+
no_progress = 0
45+
stop_reason: str | None = None
46+
47+
while True:
48+
stop_reason = evaluate_stop_conditions(
49+
state=state,
50+
steps=steps,
51+
errors=errors,
52+
no_progress=no_progress,
53+
policy=policy,
54+
)
55+
if stop_reason is not None:
56+
break
57+
58+
steps += 1
59+
60+
call = choose_next_action(task, state)
61+
action = call["action"]
62+
history.append({"step": steps, "action": action, "status": "requested"})
63+
64+
tool = TOOLS.get(action)
65+
if not tool:
66+
errors += 1
67+
no_progress += 1
68+
state["last_error"] = f"unknown_action:{action}"
69+
history.append({"step": steps, "action": action, "status": "error"})
70+
else:
71+
before_keys = set(state.keys())
72+
result = tool(state)
73+
74+
if result.get("ok"):
75+
after_keys = set(state.keys())
76+
progress = len(after_keys - before_keys) > 0
77+
no_progress = 0 if progress else no_progress + 1
78+
state.pop("last_error", None)
79+
history.append({"step": steps, "action": action, "status": "ok"})
80+
else:
81+
errors += 1
82+
no_progress += 1
83+
state["last_error"] = result.get("error", "unknown_error")
84+
history.append({"step": steps, "action": action, "status": "error"})
85+
86+
return {
87+
"done": stop_reason == "goal_reached",
88+
"stop_reason": stop_reason,
89+
"steps": steps,
90+
"errors": errors,
91+
"no_progress": no_progress,
92+
"summary": state.get("summary"),
93+
"history": history,
94+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from typing import Any
2+
3+
4+
def choose_next_action(task: str, state: dict[str, Any]) -> dict[str, Any]:
5+
# Learning version: fixed policy keeps behavior easy to reason about.
6+
_ = task
7+
8+
if "orders" not in state:
9+
return {"action": "fetch_orders", "parameters": {}}
10+
return {"action": "build_summary", "parameters": {}}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import json
2+
3+
from agent import StopPolicy, run_agent
4+
5+
TASK = "Build weekly orders summary"
6+
POLICY = StopPolicy(max_steps=6, max_errors=2, max_no_progress=3)
7+
STEP_LIMIT_POLICY = StopPolicy(max_steps=1, max_errors=2, max_no_progress=3)
8+
9+
10+
def compact_result(result: dict) -> str:
11+
return (
12+
"{"
13+
f"\"done\": {str(bool(result.get('done'))).lower()}, "
14+
f"\"stop_reason\": {json.dumps(result.get('stop_reason'), ensure_ascii=False)}, "
15+
f"\"steps\": {int(result.get('steps', 0))}, "
16+
f"\"errors\": {int(result.get('errors', 0))}, "
17+
f"\"no_progress\": {int(result.get('no_progress', 0))}, "
18+
f"\"summary\": {json.dumps(result.get('summary'), ensure_ascii=False)}, "
19+
"\"history\": [{...}]"
20+
"}"
21+
)
22+
23+
24+
def print_policy(policy: StopPolicy) -> None:
25+
print(
26+
"Policy:",
27+
json.dumps(
28+
{
29+
"max_steps": policy.max_steps,
30+
"max_errors": policy.max_errors,
31+
"max_no_progress": policy.max_no_progress,
32+
},
33+
ensure_ascii=False,
34+
),
35+
)
36+
37+
38+
def main() -> None:
39+
print("=== SCENARIO 1: GOAL REACHED ===")
40+
print_policy(POLICY)
41+
result_ok = run_agent(
42+
task=TASK,
43+
user_id=42,
44+
fail_fetch_times=1,
45+
policy=POLICY,
46+
)
47+
print("Run result:", compact_result(result_ok))
48+
49+
print("\n=== SCENARIO 2: STOPPED BY ERROR LIMIT ===")
50+
print_policy(POLICY)
51+
result_stopped = run_agent(
52+
task=TASK,
53+
user_id=42,
54+
fail_fetch_times=10,
55+
policy=POLICY,
56+
)
57+
print("Run result:", compact_result(result_stopped))
58+
59+
print("\n=== SCENARIO 3: STOPPED BY STEP LIMIT ===")
60+
print_policy(STEP_LIMIT_POLICY)
61+
result_step_limit = run_agent(
62+
task=TASK,
63+
user_id=42,
64+
fail_fetch_times=0,
65+
policy=STEP_LIMIT_POLICY,
66+
)
67+
print("Run result:", compact_result(result_step_limit))
68+
69+
70+
if __name__ == "__main__":
71+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# No external dependencies for this learning example.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Any
2+
3+
4+
def make_initial_state(user_id: int, fail_fetch_times: int) -> dict[str, Any]:
5+
return {
6+
"user_id": user_id,
7+
"fetch_calls": 0,
8+
"fail_fetch_times": fail_fetch_times,
9+
}
10+
11+
12+
def fetch_orders(state: dict[str, Any]) -> dict[str, Any]:
13+
state["fetch_calls"] += 1
14+
15+
if state["fetch_calls"] <= state["fail_fetch_times"]:
16+
return {"ok": False, "error": "orders_api_timeout"}
17+
18+
orders = [
19+
{"id": "ord-2001", "total": 49.9, "status": "paid"},
20+
{"id": "ord-2002", "total": 19.0, "status": "shipped"},
21+
]
22+
state["orders"] = orders
23+
return {"ok": True, "orders": orders}
24+
25+
26+
def build_summary(state: dict[str, Any]) -> dict[str, Any]:
27+
orders = state.get("orders")
28+
if not orders:
29+
return {"ok": False, "error": "missing_orders"}
30+
31+
summary = f"Prepared report for {len(orders)} recent orders."
32+
state["summary"] = summary
33+
return {"ok": True, "summary": summary}

0 commit comments

Comments
 (0)