-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathrun_agent.py
More file actions
169 lines (143 loc) · 6.39 KB
/
Copy pathrun_agent.py
File metadata and controls
169 lines (143 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# Tencent is pleased to support the open source community by making trpc-agent-python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# trpc-agent-python is licensed under the Apache License Version 2.0.
#
"""Run the dynamic_subagent demo.
Usage::
python run_agent.py # minimal mode (default)
python run_agent.py --mode bounded # bounded / progressive disclosure
"""
import argparse
import asyncio
import os
import sys
import uuid
from dotenv import load_dotenv
from trpc_agent_sdk.runners import Runner
from trpc_agent_sdk.sessions import InMemorySessionService
from trpc_agent_sdk.types import Content
from trpc_agent_sdk.types import Part
load_dotenv()
EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__))
if EXAMPLE_DIR not in sys.path:
sys.path.insert(0, EXAMPLE_DIR)
def _truncate(text: str, max_len: int = 200) -> str:
"""Truncate long tool output for display."""
if not isinstance(text, str):
text = str(text)
if len(text) <= max_len:
return text
return text[:max_len] + f"\n... (truncated, total {len(text)} chars)"
def _print_subagent_progress(payload: dict) -> None:
"""Render one forwarded sub-agent execution event.
``payload`` is a :class:`SubAgentProgress` dict: ``author`` / ``partial``,
the framework-native ``content`` dump (``parts`` with ``function_call`` /
``function_response`` / ``text`` / ``thought``), and optional ``error`` /
``usage``. Indented under the parent output so the sub-agent's steps are
visually distinct from the orchestrator's.
"""
name = payload.get("author") or "subagent"
# Errors first — always surface them, even on an otherwise-partial event.
err = payload.get("error")
if err:
print(f"\n \U0001F9E9 [{name}] !! error {err.get('code')}: {err.get('message')}")
if payload.get("partial"):
# Skip streaming text deltas to keep the demo output readable; the
# non-partial steps below already summarize the sub-agent's work.
return
parts = (payload.get("content") or {}).get("parts") or []
has_calls = any(p.get("function_call") or p.get("function_response") for p in parts)
for p in parts:
fc = p.get("function_call")
if fc:
print(f"\n \U0001F9E9 [{name}] -> tool {fc.get('name')}({_truncate(fc.get('args'))})")
fr = p.get("function_response")
if fr:
print(f" \U0001F9E9 [{name}] <- {_truncate(fr.get('response'))}")
text = p.get("text")
if text and not p.get("thought") and not has_calls:
print(f"\n \U0001F9E9 [{name}] {_truncate(text)}")
_QUERIES = {
"minimal": [
# Simple task: orchestrator may call word_count directly.
'Count the words in: "the quick brown fox".',
# Delegate self-contained subtasks via dynamic_subagent.
"Use a sub-agent to compute (123 * 456) + 789. Grant it only the calculator.",
"Use a sub-agent to tell me the current time in UTC.",
],
"bounded": [
"Use a sub-agent to compute (123 * 456) + 789. Grant it only the calculator.",
'Use one sub-agent to compute 50 * 12, and a separate sub-agent to count '
'the words in "the quick brown fox jumps". Grant each only the tool it needs.',
],
}
async def run_demo(mode: str):
app_name = "dynamic_subagent_demo"
if mode == "bounded":
from agent.agent import create_bounded_agent
agent = create_bounded_agent()
else:
from agent.agent import create_minimal_agent
agent = create_minimal_agent()
session_service = InMemorySessionService()
runner = Runner(app_name=app_name, agent=agent, session_service=session_service)
user_id = "demo_user"
queries = _QUERIES.get(mode, _QUERIES["minimal"])
for query in queries:
current_session_id = str(uuid.uuid4())
await session_service.create_session(
app_name=app_name,
user_id=user_id,
session_id=current_session_id,
)
print(f"\n{'=' * 60}")
print(f"\U0001F194 Mode: {mode} | Session ID: {current_session_id[:8]}...")
print(f"{'-' * 60}")
print(f"\U0001F4DD User: {query}")
user_content = Content(parts=[Part.from_text(text=query)])
print("\U0001F916 Assistant: ", end="", flush=True)
async for event in runner.run_async(
user_id=user_id,
session_id=current_session_id,
new_message=user_content,
):
# Forwarded sub-agent execution events (SubAgentConfig
# forward_events=True). These are partial progress events carrying
# the sub-agent's own steps under custom_metadata.payload; they
# never reach the parent LLM's context. This demo registers only the
# sub-agent tool, so tool_progress alone identifies them; an app with
# several progress tools would also branch on meta["tool_name"].
meta = event.custom_metadata or {}
payload = meta.get("payload")
if meta.get("tool_progress") and isinstance(payload, dict):
_print_subagent_progress(payload)
continue
if event.content and event.content.parts and event.author != "user":
if event.partial:
for part in event.content.parts:
if part.text:
print(part.text, end="", flush=True)
else:
for part in event.content.parts:
if part.thought:
continue
if part.function_call:
print(f"\n\n\U0001F527 [Invoke Tool:: {part.function_call.name}"
f"{_truncate(part.function_call.args)}]\n")
elif part.function_response:
print(f"\n\U0001F4CA [Tool Result: "
f"{_truncate(part.function_response.response)}]\n")
print(f"\n{'─' * 60}\n")
await runner.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="DynamicSubAgentTool demo")
parser.add_argument(
"--mode",
choices=["minimal", "bounded"],
default="minimal",
help="minimal: workspace tools + dynamic_subagent; bounded: only dynamic_subagent",
)
args = parser.parse_args()
asyncio.run(run_demo(args.mode))