scgnb666 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polyagent/__init__.py +10 -0
- polyagent/cli/__init__.py +682 -0
- polyagent/cli/__main__.py +6 -0
- polyagent/cli/demo.py +89 -0
- polyagent/config.py +41 -0
- polyagent/core/__init__.py +52 -0
- polyagent/core/agent.py +92 -0
- polyagent/core/exceptions.py +35 -0
- polyagent/core/types.py +92 -0
- polyagent/eval/__init__.py +18 -0
- polyagent/eval/dataset.py +23 -0
- polyagent/eval/runner.py +29 -0
- polyagent/eval/scorers.py +107 -0
- polyagent/eval/types.py +42 -0
- polyagent/llm/__init__.py +37 -0
- polyagent/llm/client.py +43 -0
- polyagent/llm/deepseek.py +266 -0
- polyagent/llm/middleware.py +168 -0
- polyagent/llm/mock.py +64 -0
- polyagent/llm/provider.py +22 -0
- polyagent/llm/types.py +27 -0
- polyagent/memory/__init__.py +15 -0
- polyagent/memory/base.py +31 -0
- polyagent/memory/buffer.py +26 -0
- polyagent/memory/compressor.py +70 -0
- polyagent/memory/vector_memory.py +31 -0
- polyagent/observability/__init__.py +32 -0
- polyagent/observability/exporters.py +71 -0
- polyagent/observability/logging.py +29 -0
- polyagent/observability/metrics.py +27 -0
- polyagent/observability/middleware.py +45 -0
- polyagent/observability/report.py +29 -0
- polyagent/observability/tracer.py +78 -0
- polyagent/orchestration/__init__.py +20 -0
- polyagent/orchestration/orchestrator.py +117 -0
- polyagent/orchestration/roles.py +78 -0
- polyagent/orchestration/types.py +36 -0
- polyagent/persistence/__init__.py +6 -0
- polyagent/persistence/chat_store.py +235 -0
- polyagent/persistence/store.py +114 -0
- polyagent/rag/__init__.py +18 -0
- polyagent/rag/embedder.py +67 -0
- polyagent/rag/index.py +51 -0
- polyagent/rag/splitter.py +25 -0
- polyagent/rag/vectorstore.py +82 -0
- polyagent/skills/__init__.py +25 -0
- polyagent/skills/builtins/datetime_skill.py +75 -0
- polyagent/skills/builtins/file_analyzer_skill.py +97 -0
- polyagent/skills/builtins/weather_skill.py +56 -0
- polyagent/skills/installer.py +128 -0
- polyagent/skills/registry.py +57 -0
- polyagent/tools/__init__.py +40 -0
- polyagent/tools/base.py +47 -0
- polyagent/tools/builtins.py +656 -0
- polyagent/tools/registry.py +73 -0
- polyagent/tools/sandbox.py +78 -0
- scgnb666-0.1.0.dist-info/METADATA +210 -0
- scgnb666-0.1.0.dist-info/RECORD +61 -0
- scgnb666-0.1.0.dist-info/WHEEL +5 -0
- scgnb666-0.1.0.dist-info/entry_points.txt +2 -0
- scgnb666-0.1.0.dist-info/top_level.txt +1 -0
polyagent/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""PolyAgent ā a model-agnostic multi-agent orchestration framework.
|
|
2
|
+
|
|
3
|
+
Declare Agent roles, let the orchestrator run them through a
|
|
4
|
+
Plan -> Execute(parallel) -> Critique -> Synthesize pipeline, with
|
|
5
|
+
reliability middleware (retry / fallback / budget) and observability at every layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""CLI: version / run / chat / shell / eval / runs / show."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from polyagent import __version__
|
|
12
|
+
from polyagent.cli.demo import build_demo_orchestrator
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="polyagent",
|
|
16
|
+
help="PolyAgent ā a model-agnostic multi-agent orchestration framework.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback()
|
|
22
|
+
def _main() -> None:
|
|
23
|
+
"""PolyAgent CLI."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def version() -> None:
|
|
28
|
+
"""Print the installed version."""
|
|
29
|
+
typer.echo(__version__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def run(
|
|
34
|
+
goal: str = typer.Argument(help="The goal to decompose and execute."),
|
|
35
|
+
provider: str = typer.Option(
|
|
36
|
+
"mock", help="mock | deepseek (deepseek needs DEEPSEEK_API_KEY)."
|
|
37
|
+
),
|
|
38
|
+
persist: bool = typer.Option(True, help="Save the run to polyagent.db."),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Run the multi-agent pipeline on a goal."""
|
|
41
|
+
asyncio.run(_run_async(goal, provider, persist))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _run_async(goal: str, provider: str, persist: bool = True) -> None:
|
|
45
|
+
if provider == "mock":
|
|
46
|
+
orch, tracer, cost = build_demo_orchestrator()
|
|
47
|
+
elif provider == "deepseek":
|
|
48
|
+
from polyagent.cli.demo import build_deepseek_orchestrator
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
orch, tracer, cost = build_deepseek_orchestrator()
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
typer.echo(str(exc), err=True)
|
|
54
|
+
raise typer.Exit(1) from exc
|
|
55
|
+
else:
|
|
56
|
+
typer.echo(f"unknown provider '{provider}'; use mock or deepseek.", err=True)
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
result = await orch.run(goal)
|
|
59
|
+
typer.echo(f"Answer: {result.answer}")
|
|
60
|
+
typer.echo("Task graph:")
|
|
61
|
+
for task in result.task_graph:
|
|
62
|
+
typer.echo(
|
|
63
|
+
f" [{task.status.value}] {task.id}: {task.description} (attempts={task.attempts})"
|
|
64
|
+
)
|
|
65
|
+
typer.echo(
|
|
66
|
+
f"Cost: ${cost.estimated_cost_usd:.6f} | Latency: {result.latency:.3f}s | "
|
|
67
|
+
f"Attempts: {result.total_attempts} | Trace roots: {len(tracer.roots)}"
|
|
68
|
+
)
|
|
69
|
+
if persist:
|
|
70
|
+
from polyagent.persistence import RunStore
|
|
71
|
+
|
|
72
|
+
store = RunStore()
|
|
73
|
+
run_id = store.save(goal=goal, result=result, trace=tracer.to_dict())
|
|
74
|
+
store.close()
|
|
75
|
+
typer.echo(f"Run ID: {run_id} (saved to polyagent.db)")
|
|
76
|
+
|
|
77
|
+
# Observability export (only if backends configured in .env)
|
|
78
|
+
from polyagent.config import get_observability_settings
|
|
79
|
+
|
|
80
|
+
obs = get_observability_settings()
|
|
81
|
+
if obs.otel_exporter_otlp_endpoint:
|
|
82
|
+
try:
|
|
83
|
+
from polyagent.observability import export_traces_to_otlp
|
|
84
|
+
|
|
85
|
+
n = export_traces_to_otlp(tracer, obs.otel_exporter_otlp_endpoint)
|
|
86
|
+
typer.echo(f"Exported {n} spans to OTLP")
|
|
87
|
+
except ImportError:
|
|
88
|
+
typer.echo("OTLP export needs: pip install '.[observability]'", err=True)
|
|
89
|
+
if obs.prometheus_pushgateway:
|
|
90
|
+
try:
|
|
91
|
+
from polyagent.observability import Metrics, export_metrics_to_prometheus
|
|
92
|
+
|
|
93
|
+
m = Metrics()
|
|
94
|
+
m.inc("runs")
|
|
95
|
+
m.observe("run_latency_s", result.latency)
|
|
96
|
+
m.inc("run_cost_usd", result.estimated_cost_usd)
|
|
97
|
+
export_metrics_to_prometheus(m, obs.prometheus_pushgateway)
|
|
98
|
+
typer.echo("Pushed metrics to Prometheus pushgateway")
|
|
99
|
+
except ImportError:
|
|
100
|
+
typer.echo("Prometheus export needs: pip install '.[observability]'", err=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def chat(
|
|
105
|
+
provider: str = typer.Option("mock", help="mock | deepseek."),
|
|
106
|
+
stream: bool = typer.Option(False, help="Stream tokens live (deepseek only)."),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Interactive single-agent chat (Ctrl-D to exit)."""
|
|
109
|
+
asyncio.run(_chat_async(provider, stream))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _chat_async(provider: str, stream: bool) -> None:
|
|
113
|
+
from polyagent.core.agent import Agent
|
|
114
|
+
from polyagent.core.types import AgentSpec
|
|
115
|
+
from polyagent.llm.client import LLMClient
|
|
116
|
+
from polyagent.llm.deepseek import DeepSeekProvider
|
|
117
|
+
from polyagent.llm.mock import MockProvider
|
|
118
|
+
|
|
119
|
+
if provider == "mock":
|
|
120
|
+
spec = AgentSpec(name="chat", role="worker", model="mock")
|
|
121
|
+
agent = Agent(spec, LLMClient(MockProvider()))
|
|
122
|
+
elif provider == "deepseek":
|
|
123
|
+
from polyagent.config import get_settings
|
|
124
|
+
|
|
125
|
+
s = get_settings()
|
|
126
|
+
if not s.api_key:
|
|
127
|
+
typer.echo("DEEPSEEK_API_KEY not set; check .env.", err=True)
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
spec = AgentSpec(
|
|
130
|
+
name="chat",
|
|
131
|
+
role="worker",
|
|
132
|
+
model=s.model,
|
|
133
|
+
system_prompt="You are a helpful assistant.",
|
|
134
|
+
)
|
|
135
|
+
ds = DeepSeekProvider(api_key=s.api_key, base_url=s.base_url, model=s.model)
|
|
136
|
+
agent = Agent(spec, LLMClient(ds))
|
|
137
|
+
else:
|
|
138
|
+
typer.echo(f"unknown provider '{provider}'.", err=True)
|
|
139
|
+
raise typer.Exit(1)
|
|
140
|
+
|
|
141
|
+
typer.echo(f"Chat ({provider}). Ctrl-D to exit.")
|
|
142
|
+
while True:
|
|
143
|
+
try:
|
|
144
|
+
line = input("you> ")
|
|
145
|
+
except EOFError:
|
|
146
|
+
break
|
|
147
|
+
if not line.strip():
|
|
148
|
+
continue
|
|
149
|
+
if stream and provider == "deepseek":
|
|
150
|
+
typer.echo("agent> ", nl=False)
|
|
151
|
+
async for chunk in agent.run_stream(line):
|
|
152
|
+
typer.echo(chunk, nl=False)
|
|
153
|
+
typer.echo()
|
|
154
|
+
else:
|
|
155
|
+
resp = await agent.run(line)
|
|
156
|
+
typer.echo(f"agent> {resp.content}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# --------------------------------------------------------------------------- #
|
|
160
|
+
# shell ā åø¦å
Øå„å·„å
·ēęŗč½å©ę
|
|
161
|
+
# --------------------------------------------------------------------------- #
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_SHELL_PROMPT = """\
|
|
165
|
+
You are a powerful AI terminal assistant running inside a Python framework. \
|
|
166
|
+
You have the following tools at your disposal. USE THEM PROACTIVELY ā do not \
|
|
167
|
+
just talk, DO things.
|
|
168
|
+
|
|
169
|
+
## Available tools
|
|
170
|
+
|
|
171
|
+
- **web_search(query, max_results=5, engine="bing")** ā Search the web for current \
|
|
172
|
+
information. Supports engines: bing (default, works in China), duckduckgo, sogou. \
|
|
173
|
+
Use this for real-time data, news, documentation, facts you're unsure about.
|
|
174
|
+
- **python_execute(code, timeout=10)** ā Execute Python code and return stdout/stderr. \
|
|
175
|
+
Use this for calculations, system info, data processing, generating content.
|
|
176
|
+
- **read_file(path, max_bytes=65536)** ā Read a text file from the local filesystem.
|
|
177
|
+
- **write_file(path, content, append=False)** ā Write text content to a local file.
|
|
178
|
+
- **http_request(method, url, headers={}, body=None, timeout=15)** ā Make HTTP requests \
|
|
179
|
+
to any URL. Use this for fetching API data, downloading content, checking websites.
|
|
180
|
+
- **grep_files(directory, pattern, max_results=50)** ā Search files for a regex pattern.
|
|
181
|
+
- **marketplace_search(query)** ā Search the PolyAgent builtin marketplace.
|
|
182
|
+
- **marketplace_install(name)** ā Install a builtin skill. Tools available next turn.
|
|
183
|
+
- **skillhub_search(query)** ā Search **Tencent SkillHub** (skillhub.tencent.com) with 76,000+ AI skills.
|
|
184
|
+
- **skillhub_install(slug)** ā Install a skill from Tencent SkillHub by slug. Downloads SKILL.md + assets.
|
|
185
|
+
- **skillhub_install_from_prompt(prompt)** ā Install a SkillHub skill from the official prompt the user copied from the website. Extracts the skill slug and installs automatically.
|
|
186
|
+
- **skillhub_list()** ā List installed SkillHub skills with descriptions.
|
|
187
|
+
|
|
188
|
+
## Skill marketplaces
|
|
189
|
+
|
|
190
|
+
You have two skill marketplaces:
|
|
191
|
+
|
|
192
|
+
1. **PolyAgent builtin** (marketplace_search / marketplace_install):
|
|
193
|
+
- weather: query weather for any city
|
|
194
|
+
- file_analyzer: analyze file stats (lines, size, encoding)
|
|
195
|
+
- datetime_utils: current time, date diff, timestamp formatting
|
|
196
|
+
|
|
197
|
+
2. **Tencent SkillHub** (skillhub_search / skillhub_install / skillhub_install_from_prompt / skillhub_list):
|
|
198
|
+
- 76,000+ AI skills from https://skillhub.tencent.com
|
|
199
|
+
- Skills cover: coding helpers, PDF processing, data analysis, web scraping, etc.
|
|
200
|
+
- Uses direct REST API (no CLI needed). Search by keyword, install by slug.
|
|
201
|
+
- **Official install prompt**: when the user copies the prompt from a SkillHub skill page and pastes it, call skillhub_install_from_prompt(prompt_text) immediately.
|
|
202
|
+
- After installing, read the SKILL.md with read_file to understand its instructions.
|
|
203
|
+
|
|
204
|
+
When the user asks for something not covered by your current tools, first search the \
|
|
205
|
+
PolyAgent marketplace, then try SkillHub. Search SkillHub with simple keywords.
|
|
206
|
+
|
|
207
|
+
**ā ļø CRITICAL RULE: When the user asks to search for skills on SkillHub, Tencent SkillHub, \
|
|
208
|
+
č
¾č®Æęč½åøåŗ, or any skill marketplace ā you MUST call skillhub_search(query) directly. \
|
|
209
|
+
Do NOT use web_search to search for SkillHub or its website. The skillhub_search tool calls \
|
|
210
|
+
the official SkillHub API directly. Similarly, use skillhub_install(slug) to install, not web_search or http_request.**
|
|
211
|
+
|
|
212
|
+
## Behaviour rules
|
|
213
|
+
|
|
214
|
+
1. When the user asks about current events, web data, or anything recent ā call web_search.
|
|
215
|
+
2. When asked to calculate, transform data, or generate something ā call python_execute.
|
|
216
|
+
3. When asked to read/show a file ā call read_file.
|
|
217
|
+
4. When asked to save/write something ā call write_file.
|
|
218
|
+
5. When asked to download content or call an API ā call http_request.
|
|
219
|
+
6. **When asked to search/install skills from SkillHub ā call skillhub_search / skillhub_install**.
|
|
220
|
+
7. You can chain multiple tool calls. After each tool result, decide whether you need \
|
|
221
|
+
more tools or can give the final answer.
|
|
222
|
+
8. **Always explain briefly what you're doing** before or during tool calls.
|
|
223
|
+
9. When you have all the information needed, give a clear, complete answer.
|
|
224
|
+
10. Be concise but thorough. Write Chinese when the user writes Chinese.
|
|
225
|
+
|
|
226
|
+
## ā ļø Search strategy (CRITICAL)
|
|
227
|
+
|
|
228
|
+
- **Stop searching after 3 web_search attempts** if you keep getting no results. \
|
|
229
|
+
Do not keep trying different queries endlessly.
|
|
230
|
+
- After max 3 searches, immediately summarize what you found (even if nothing) \
|
|
231
|
+
and give a direct answer. Do not ask the user for more information.
|
|
232
|
+
- If Bing returns nothing, try switching engine="sogou" once. If that also fails, \
|
|
233
|
+
stop and answer with what you have.
|
|
234
|
+
- **Always give a complete final answer** after searching. Never leave the user \
|
|
235
|
+
waiting or ask the user to provide missing info.\
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def shell(
|
|
241
|
+
stream: bool = typer.Option(True, help="Show streaming output."),
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Interactive AI assistant with all tools (web search, code exec, file I/O, ...)."""
|
|
244
|
+
asyncio.run(_shell_async(stream))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _shell_async(stream: bool) -> None:
|
|
248
|
+
from polyagent.config import get_settings
|
|
249
|
+
from polyagent.core.agent import Agent
|
|
250
|
+
from polyagent.core.types import AgentSpec, Message, Role
|
|
251
|
+
from polyagent.llm.client import LLMClient
|
|
252
|
+
from polyagent.llm.deepseek import DeepSeekProvider
|
|
253
|
+
from polyagent.llm.types import LLMRequest
|
|
254
|
+
from polyagent.persistence.chat_store import ChatStore
|
|
255
|
+
from polyagent.skills import list_installed, load_skills_into
|
|
256
|
+
from polyagent.tools import with_builtins
|
|
257
|
+
|
|
258
|
+
s = get_settings()
|
|
259
|
+
if not s.api_key:
|
|
260
|
+
typer.echo("DEEPSEEK_API_KEY not set; check .env.", err=True)
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
|
|
263
|
+
ds = DeepSeekProvider(api_key=s.api_key, base_url=s.base_url, model=s.model)
|
|
264
|
+
tools = with_builtins()
|
|
265
|
+
|
|
266
|
+
# Load previously installed skills
|
|
267
|
+
n_registered = load_skills_into(tools)
|
|
268
|
+
installed_skills = list_installed()
|
|
269
|
+
|
|
270
|
+
spec = AgentSpec(
|
|
271
|
+
name="shell",
|
|
272
|
+
role="worker",
|
|
273
|
+
model=s.model,
|
|
274
|
+
system_prompt=_SHELL_PROMPT,
|
|
275
|
+
)
|
|
276
|
+
agent = Agent(spec, LLMClient(ds), tools=tools, max_tool_iters=8)
|
|
277
|
+
|
|
278
|
+
typer.echo("PolyAgent Shell ā čē½ęē“¢ / 代ē ę§č” / 读åęä»¶ / ęč½åøåŗ")
|
|
279
|
+
typer.echo(f" Provider: {s.model} | å
置巄å
·: {', '.join(tools.names())}")
|
|
280
|
+
if installed_skills:
|
|
281
|
+
names = [s["name"] for s in installed_skills]
|
|
282
|
+
typer.echo(f" š¦ å·²å®č£
ęč½: {', '.join(names)} ({n_registered} äøŖå·„å
·)")
|
|
283
|
+
typer.echo(" Ctrl-D éåŗ | č¾å
„éę±ļ¼Agent čŖåØå³å®ēØä»ä¹å·„å
·")
|
|
284
|
+
typer.echo(" ę示: åÆå
ēØćęē“¢ęč½ä»åŗćę„ę„ę¾åÆå®č£
ēęč½\n")
|
|
285
|
+
|
|
286
|
+
messages: list[Message] = []
|
|
287
|
+
if spec.system_prompt:
|
|
288
|
+
messages.append(Message(role=Role.SYSTEM, content=spec.system_prompt))
|
|
289
|
+
|
|
290
|
+
# Load and inject last session's context as a SYSTEM reminder
|
|
291
|
+
chat_store = ChatStore()
|
|
292
|
+
history = chat_store.load_last_session("shell")
|
|
293
|
+
if history:
|
|
294
|
+
summary = "仄äøęÆä½ äøę¬”ē对čÆč®°å½ļ¼ä¾åčäøäøęļ¼\n\n"
|
|
295
|
+
for m in history:
|
|
296
|
+
label = {"user": "ä½ čÆ“", "assistant": "ę诓"}.get(m.role.value, m.role.value)
|
|
297
|
+
text = m.content[:300] if m.content else ""
|
|
298
|
+
if m.tool_calls:
|
|
299
|
+
names = [tc.name for tc in m.tool_calls]
|
|
300
|
+
text = f"[č°ēØäŗ: {', '.join(names)}]"
|
|
301
|
+
summary += f"{label}ļ¼{text}\n"
|
|
302
|
+
messages.append(Message(role=Role.SYSTEM, content=summary))
|
|
303
|
+
typer.echo(f" š¬ å·²å č½½äøę¬”对čÆč®°å½ ({len(history)} ę”)\n")
|
|
304
|
+
|
|
305
|
+
while True:
|
|
306
|
+
try:
|
|
307
|
+
line = input("you> ")
|
|
308
|
+
except EOFError:
|
|
309
|
+
break
|
|
310
|
+
if not line.strip():
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
messages.append(Message(role=Role.USER, content=line))
|
|
314
|
+
tool_schemas = tools.schemas()
|
|
315
|
+
|
|
316
|
+
full_answer = ""
|
|
317
|
+
for iteration in range(agent.max_tool_iters):
|
|
318
|
+
# --- LLM call ---
|
|
319
|
+
req = LLMRequest(
|
|
320
|
+
model=spec.model,
|
|
321
|
+
messages=messages,
|
|
322
|
+
temperature=spec.temperature,
|
|
323
|
+
tools=tool_schemas,
|
|
324
|
+
)
|
|
325
|
+
try:
|
|
326
|
+
resp = await agent.client.chat(req)
|
|
327
|
+
except Exception as exc:
|
|
328
|
+
typer.echo(f"\n ā ļø LLM č°ēØå¼åøø: {exc}")
|
|
329
|
+
typer.echo(" 对čÆå·²ę¢å¤ļ¼čÆ·éę°č¾å
„ć\n")
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
# --- stream output ---
|
|
333
|
+
if resp.content:
|
|
334
|
+
if iteration == 0:
|
|
335
|
+
typer.echo("agent> ", nl=False)
|
|
336
|
+
typer.echo(resp.content, nl=False)
|
|
337
|
+
full_answer += resp.content
|
|
338
|
+
|
|
339
|
+
# --- check tool calls ---
|
|
340
|
+
if not resp.tool_calls:
|
|
341
|
+
typer.echo()
|
|
342
|
+
break # done
|
|
343
|
+
|
|
344
|
+
# Append assistant message ONCE before executing tools
|
|
345
|
+
messages.append(
|
|
346
|
+
Message(
|
|
347
|
+
role=Role.ASSISTANT,
|
|
348
|
+
content=resp.content,
|
|
349
|
+
tool_calls=resp.tool_calls,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# execute each tool and show result
|
|
354
|
+
for call in resp.tool_calls:
|
|
355
|
+
try:
|
|
356
|
+
args = json.loads(call.arguments) if call.arguments else {}
|
|
357
|
+
except json.JSONDecodeError:
|
|
358
|
+
args = {}
|
|
359
|
+
|
|
360
|
+
args_str = ", ".join(
|
|
361
|
+
f"{k}={repr(v)[:60]}" for k, v in args.items()
|
|
362
|
+
)
|
|
363
|
+
typer.echo(f"\n ā” č°ēØ {call.name}({args_str})")
|
|
364
|
+
|
|
365
|
+
tool = tools.get(call.name)
|
|
366
|
+
result = await tool.call(args)
|
|
367
|
+
|
|
368
|
+
status = "ā
" if not result.error else "ā"
|
|
369
|
+
output_preview = result.output[:300].replace("\n", " ")
|
|
370
|
+
typer.echo(f" {status} {output_preview}")
|
|
371
|
+
if len(result.output) > 300:
|
|
372
|
+
typer.echo(f" ... ({len(result.output)} å符ļ¼å·²ęŖę)")
|
|
373
|
+
|
|
374
|
+
# append tool result to conversation (cap at 2000 chars)
|
|
375
|
+
content = result.output[:2000]
|
|
376
|
+
if len(result.output) > 2000:
|
|
377
|
+
content += "\n...(truncated)"
|
|
378
|
+
messages.append(
|
|
379
|
+
Message(role=Role.TOOL, content=content, tool_call_id=call.id)
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
# Tool iterations exhausted ā force a final summary without tools
|
|
383
|
+
typer.echo()
|
|
384
|
+
try:
|
|
385
|
+
final_req = LLMRequest(
|
|
386
|
+
model=spec.model,
|
|
387
|
+
messages=messages,
|
|
388
|
+
temperature=spec.temperature,
|
|
389
|
+
tools=None,
|
|
390
|
+
)
|
|
391
|
+
final_resp = await agent.client.chat(final_req)
|
|
392
|
+
if final_resp.content:
|
|
393
|
+
typer.echo("agent> ", nl=False)
|
|
394
|
+
typer.echo(final_resp.content)
|
|
395
|
+
full_answer = final_resp.content
|
|
396
|
+
except Exception as exc:
|
|
397
|
+
typer.echo(f" ā ļø ę»ē»å¤±č“„: {exc}")
|
|
398
|
+
|
|
399
|
+
# reset messages for next user input
|
|
400
|
+
if full_answer:
|
|
401
|
+
messages.append(Message(role=Role.ASSISTANT, content=full_answer))
|
|
402
|
+
|
|
403
|
+
# Persist conversation to SQLite for cross-session memory
|
|
404
|
+
try:
|
|
405
|
+
chat_store.save_messages(messages, "shell")
|
|
406
|
+
except Exception:
|
|
407
|
+
pass # non-critical; skip silently
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# --------------------------------------------------------------------------- #
|
|
411
|
+
# talk ā č天樔å¼ļ¼ęäŗŗåļ¼éčå·„å
·č°ēØļ¼
|
|
412
|
+
# --------------------------------------------------------------------------- #
|
|
413
|
+
|
|
414
|
+
_TALK_PROMPT = """\
|
|
415
|
+
You are a warm, intelligent friend. You talk like a real person ā friendly, casual, with a \
|
|
416
|
+
sense of humor. But you are also smart: you use web search and other tools behind the scenes \
|
|
417
|
+
whenever you need up-to-date information.
|
|
418
|
+
|
|
419
|
+
## The #1 rule (NON-NEGOTIABLE)
|
|
420
|
+
When the user asks about **current events, sports matches, weather, schedules, recent news, \
|
|
421
|
+
or anything time-sensitive**, you MUST call web_search FIRST, before you say anything. \
|
|
422
|
+
Do not answer from your training data. Do not guess. Get the latest info, then answer naturally.
|
|
423
|
+
|
|
424
|
+
## How to use tools (silently)
|
|
425
|
+
- You have web_search, python_execute, read_file, write_file, http_request. Use them silently.
|
|
426
|
+
- NEVER tell the user you're searching or using tools.
|
|
427
|
+
- NEVER say "based on my search" or "according to". Just answer as if you already know.
|
|
428
|
+
- If the first search query doesn't find good results, try another query (max 3 attempts). \
|
|
429
|
+
Then stop and answer with what you found.
|
|
430
|
+
|
|
431
|
+
## Example
|
|
432
|
+
User: ę天äøēęÆč°ä¼čµ¢ļ¼
|
|
433
|
+
Your action: call web_search(query='2026äøēęÆ 7ę13ę„ åå³čµ 对éµ')
|
|
434
|
+
Your response: ååļ¼ęēäŗäøļ¼ę天åå³čµęÆ A é对 B éćA éęčæē¶ęā¦ā¦ęåčę“ē儽 B éļ¼ä½ ęÆęåŖč¾¹ļ¼
|
|
435
|
+
|
|
436
|
+
## Response style
|
|
437
|
+
- Be warm, concise, and natural. Use Chinese when the user writes Chinese.
|
|
438
|
+
- Weave facts into conversation, don't list them robotically.
|
|
439
|
+
- Express opinions and emotions when appropriate.
|
|
440
|
+
- After answering, occasionally ask a short, natural follow-up about the user's opinion or experience.
|
|
441
|
+
|
|
442
|
+
## What NOT to do
|
|
443
|
+
- NEVER answer from your training data cutoff. ALWAYS use web_search for anything current.
|
|
444
|
+
- NEVER guess dates like "ē°åØęÆ 1 ę" or "ęÆčµčæę²”å¼å§". If search results are insufficient, \
|
|
445
|
+
say "ęęäŗäøåļ¼ę²”ę¾å°čæåŗęÆčµ/äæ”ęÆ" naturally.
|
|
446
|
+
- NEVER ask clarifying questions to answer the question. If the query is vague, answer based on \
|
|
447
|
+
your best interpretation.
|
|
448
|
+
- NEVER say "ä½ ęÆäøęÆę³é®ę¬§ę“²ęÆ/ē¾ę“²ęÆ/äŗę“²ęÆ". If search shows no relevant match, just say \
|
|
449
|
+
"ę天儽åę²”ęäøēęÆęÆčµ" and mention what is happening.
|
|
450
|
+
- NEVER ask the user to provide missing information so you can answer. If you can't find it, \
|
|
451
|
+
just admit it casually.\
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@app.command()
|
|
456
|
+
def talk(
|
|
457
|
+
temperature: float = typer.Option(1.0, help="Creativity (0-2, default 1.0)."),
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Chat mode ā emotional, human-like, no tech output."""
|
|
460
|
+
asyncio.run(_talk_async(temperature))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def _talk_async(temperature: float) -> None:
|
|
464
|
+
from polyagent.config import get_settings
|
|
465
|
+
from polyagent.core.agent import Agent
|
|
466
|
+
from polyagent.core.types import AgentSpec, Message, Role
|
|
467
|
+
from polyagent.llm.client import LLMClient
|
|
468
|
+
from polyagent.llm.deepseek import DeepSeekProvider
|
|
469
|
+
from polyagent.llm.types import LLMRequest
|
|
470
|
+
from polyagent.persistence.chat_store import ChatStore
|
|
471
|
+
from polyagent.tools import with_builtins
|
|
472
|
+
|
|
473
|
+
s = get_settings()
|
|
474
|
+
if not s.api_key:
|
|
475
|
+
typer.echo("DEEPSEEK_API_KEY not set; check .env.", err=True)
|
|
476
|
+
raise typer.Exit(1)
|
|
477
|
+
|
|
478
|
+
ds = DeepSeekProvider(
|
|
479
|
+
api_key=s.api_key, base_url=s.base_url, model=s.model
|
|
480
|
+
)
|
|
481
|
+
tools = with_builtins()
|
|
482
|
+
|
|
483
|
+
prompt = f"Today is {datetime.now().strftime('%Y-%m-%d')}.\n\n{_TALK_PROMPT}"
|
|
484
|
+
|
|
485
|
+
spec = AgentSpec(
|
|
486
|
+
name="talk",
|
|
487
|
+
role="worker",
|
|
488
|
+
model=s.model,
|
|
489
|
+
temperature=temperature,
|
|
490
|
+
system_prompt=prompt,
|
|
491
|
+
)
|
|
492
|
+
agent = Agent(spec, LLMClient(ds), tools=tools, max_tool_iters=6)
|
|
493
|
+
|
|
494
|
+
typer.echo("š¬ čå¤©ęØ”å¼ (talk) ā ęäŗŗååÆ¹čÆ / čē½ / 代ē ę§č”")
|
|
495
|
+
typer.echo(f" Provider: {s.model} | č¾å
„ exit ę Ctrl-D éåŗ\n")
|
|
496
|
+
|
|
497
|
+
messages: list[Message] = []
|
|
498
|
+
if spec.system_prompt:
|
|
499
|
+
messages.append(Message(role=Role.SYSTEM, content=spec.system_prompt))
|
|
500
|
+
|
|
501
|
+
# Load and inject last session's context as a SYSTEM reminder
|
|
502
|
+
chat_store = ChatStore()
|
|
503
|
+
history = chat_store.load_last_session("talk")
|
|
504
|
+
if history:
|
|
505
|
+
summary = "仄äøęÆä½ äøę¬”ē对čÆč®°å½ļ¼ä¾åčäøäøęļ¼\n\n"
|
|
506
|
+
for m in history:
|
|
507
|
+
label = {"user": "ä½ čÆ“", "assistant": "ę诓"}.get(m.role.value, m.role.value)
|
|
508
|
+
text = m.content[:300] if m.content else ""
|
|
509
|
+
if m.tool_calls:
|
|
510
|
+
names = [tc.name for tc in m.tool_calls]
|
|
511
|
+
text = f"[č°ēØäŗ: {', '.join(names)}]"
|
|
512
|
+
summary += f"{label}ļ¼{text}\n"
|
|
513
|
+
messages.append(Message(role=Role.SYSTEM, content=summary))
|
|
514
|
+
typer.echo(f" š¬ å·²å č½½äøę¬”对čÆč®°å½ ({len(history)} ę”)\n")
|
|
515
|
+
|
|
516
|
+
while True:
|
|
517
|
+
try:
|
|
518
|
+
line = input("ä½ > ")
|
|
519
|
+
except EOFError:
|
|
520
|
+
break
|
|
521
|
+
if not line.strip():
|
|
522
|
+
continue
|
|
523
|
+
if line.strip().lower() == "exit":
|
|
524
|
+
break
|
|
525
|
+
|
|
526
|
+
messages.append(Message(role=Role.USER, content=line))
|
|
527
|
+
tool_schemas = tools.schemas()
|
|
528
|
+
|
|
529
|
+
full_answer = ""
|
|
530
|
+
for iteration in range(agent.max_tool_iters):
|
|
531
|
+
req = LLMRequest(
|
|
532
|
+
model=spec.model,
|
|
533
|
+
messages=messages,
|
|
534
|
+
temperature=spec.temperature,
|
|
535
|
+
tools=tool_schemas,
|
|
536
|
+
)
|
|
537
|
+
try:
|
|
538
|
+
resp = await agent.client.chat(req)
|
|
539
|
+
except Exception:
|
|
540
|
+
# silently retry or break ā user never sees the error
|
|
541
|
+
break
|
|
542
|
+
|
|
543
|
+
full_answer += resp.content or ""
|
|
544
|
+
|
|
545
|
+
if not resp.tool_calls:
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
# Append assistant message + execute tools SILENTLY
|
|
549
|
+
messages.append(
|
|
550
|
+
Message(
|
|
551
|
+
role=Role.ASSISTANT,
|
|
552
|
+
content=resp.content,
|
|
553
|
+
tool_calls=resp.tool_calls,
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
for call in resp.tool_calls:
|
|
557
|
+
tool = tools.get(call.name)
|
|
558
|
+
if tool:
|
|
559
|
+
try:
|
|
560
|
+
args = (
|
|
561
|
+
json.loads(call.arguments)
|
|
562
|
+
if call.arguments
|
|
563
|
+
else {}
|
|
564
|
+
)
|
|
565
|
+
result = await tool.call(args)
|
|
566
|
+
content = (result.output or "")[:2000]
|
|
567
|
+
except Exception:
|
|
568
|
+
content = ""
|
|
569
|
+
messages.append(
|
|
570
|
+
Message(
|
|
571
|
+
role=Role.TOOL,
|
|
572
|
+
content=content,
|
|
573
|
+
tool_call_id=call.id,
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
# Force final summary without tools
|
|
578
|
+
try:
|
|
579
|
+
final_req = LLMRequest(
|
|
580
|
+
model=spec.model,
|
|
581
|
+
messages=messages,
|
|
582
|
+
temperature=spec.temperature,
|
|
583
|
+
tools=None,
|
|
584
|
+
)
|
|
585
|
+
final_resp = await agent.client.chat(final_req)
|
|
586
|
+
if final_resp.content:
|
|
587
|
+
full_answer = final_resp.content
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
# Output only the final answer ā no tech noise
|
|
592
|
+
if full_answer:
|
|
593
|
+
typer.echo(f"\nš¤ {full_answer}\n")
|
|
594
|
+
messages.append(
|
|
595
|
+
Message(role=Role.ASSISTANT, content=full_answer)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Persist conversation to SQLite for cross-session memory
|
|
599
|
+
try:
|
|
600
|
+
chat_store.save_messages(messages, "talk")
|
|
601
|
+
except Exception:
|
|
602
|
+
pass # non-critical; skip silently
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@app.command("eval")
|
|
606
|
+
def eval_() -> None:
|
|
607
|
+
"""Run the default eval dataset and print a pass-rate report."""
|
|
608
|
+
asyncio.run(_eval_async())
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
async def _eval_async() -> None:
|
|
612
|
+
from polyagent.core.agent import Agent
|
|
613
|
+
from polyagent.core.types import AgentSpec
|
|
614
|
+
from polyagent.eval import ContainsScorer, Dataset, EvalRunner
|
|
615
|
+
from polyagent.llm.client import LLMClient
|
|
616
|
+
from polyagent.llm.mock import MockProvider
|
|
617
|
+
|
|
618
|
+
agent = Agent(AgentSpec(name="eval", role="worker", model="mock"), LLMClient(MockProvider()))
|
|
619
|
+
|
|
620
|
+
async def subject(inp: str) -> str:
|
|
621
|
+
return (await agent.run(inp)).content
|
|
622
|
+
|
|
623
|
+
report = await EvalRunner(subject, ContainsScorer()).run(Dataset.default())
|
|
624
|
+
typer.echo(
|
|
625
|
+
f"Eval: {report.passed}/{report.n} passed ({report.pass_rate:.0%}), "
|
|
626
|
+
f"avg_score={report.avg_score:.2f}"
|
|
627
|
+
)
|
|
628
|
+
for r in report.results:
|
|
629
|
+
flag = "PASS" if r.passed else "FAIL"
|
|
630
|
+
typer.echo(f" [{flag}] {r.case_id}")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@app.command("runs")
|
|
634
|
+
def runs_(limit: int = typer.Option(20, help="Max runs to list.")) -> None:
|
|
635
|
+
"""List recent persisted runs."""
|
|
636
|
+
from polyagent.persistence import RunStore
|
|
637
|
+
|
|
638
|
+
store = RunStore()
|
|
639
|
+
rows = store.list(limit)
|
|
640
|
+
store.close()
|
|
641
|
+
if not rows:
|
|
642
|
+
typer.echo("no runs yet.")
|
|
643
|
+
return
|
|
644
|
+
for r in rows:
|
|
645
|
+
typer.echo(
|
|
646
|
+
f"{r['id']} ${r['cost_usd']:.6f} {r['latency']:.2f}s "
|
|
647
|
+
f"{r['created_at']} {r['goal'][:40]}"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@app.command("show")
|
|
652
|
+
def show(run_id: str = typer.Argument(help="Run id to inspect.")) -> None:
|
|
653
|
+
"""Show a run's answer, task graph, and trace summary."""
|
|
654
|
+
from polyagent.persistence import RunStore
|
|
655
|
+
|
|
656
|
+
store = RunStore()
|
|
657
|
+
data = store.get(run_id)
|
|
658
|
+
store.close()
|
|
659
|
+
if not data:
|
|
660
|
+
typer.echo(f"run not found: {run_id}", err=True)
|
|
661
|
+
raise typer.Exit(1)
|
|
662
|
+
run = data["run"]
|
|
663
|
+
typer.echo(f"Run {run['id']}: {run['goal']}")
|
|
664
|
+
typer.echo(f"Answer: {run['answer']}")
|
|
665
|
+
typer.echo(f"Cost: ${run['cost_usd']:.6f} | Latency: {run['latency']}s")
|
|
666
|
+
typer.echo("Tasks:")
|
|
667
|
+
for t in data["tasks"]:
|
|
668
|
+
typer.echo(
|
|
669
|
+
f" [{t['status']}] {t['id']}: {t['description']} (attempts={t['attempts']})"
|
|
670
|
+
)
|
|
671
|
+
trace = data["trace"]
|
|
672
|
+
if trace:
|
|
673
|
+
roots = trace.get("spans", [])
|
|
674
|
+
typer.echo(f"Trace: {len(roots)} root span(s)")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def main() -> None:
|
|
678
|
+
app()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
if __name__ == "__main__": # pragma: no cover
|
|
682
|
+
main()
|