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.
Files changed (61) hide show
  1. polyagent/__init__.py +10 -0
  2. polyagent/cli/__init__.py +682 -0
  3. polyagent/cli/__main__.py +6 -0
  4. polyagent/cli/demo.py +89 -0
  5. polyagent/config.py +41 -0
  6. polyagent/core/__init__.py +52 -0
  7. polyagent/core/agent.py +92 -0
  8. polyagent/core/exceptions.py +35 -0
  9. polyagent/core/types.py +92 -0
  10. polyagent/eval/__init__.py +18 -0
  11. polyagent/eval/dataset.py +23 -0
  12. polyagent/eval/runner.py +29 -0
  13. polyagent/eval/scorers.py +107 -0
  14. polyagent/eval/types.py +42 -0
  15. polyagent/llm/__init__.py +37 -0
  16. polyagent/llm/client.py +43 -0
  17. polyagent/llm/deepseek.py +266 -0
  18. polyagent/llm/middleware.py +168 -0
  19. polyagent/llm/mock.py +64 -0
  20. polyagent/llm/provider.py +22 -0
  21. polyagent/llm/types.py +27 -0
  22. polyagent/memory/__init__.py +15 -0
  23. polyagent/memory/base.py +31 -0
  24. polyagent/memory/buffer.py +26 -0
  25. polyagent/memory/compressor.py +70 -0
  26. polyagent/memory/vector_memory.py +31 -0
  27. polyagent/observability/__init__.py +32 -0
  28. polyagent/observability/exporters.py +71 -0
  29. polyagent/observability/logging.py +29 -0
  30. polyagent/observability/metrics.py +27 -0
  31. polyagent/observability/middleware.py +45 -0
  32. polyagent/observability/report.py +29 -0
  33. polyagent/observability/tracer.py +78 -0
  34. polyagent/orchestration/__init__.py +20 -0
  35. polyagent/orchestration/orchestrator.py +117 -0
  36. polyagent/orchestration/roles.py +78 -0
  37. polyagent/orchestration/types.py +36 -0
  38. polyagent/persistence/__init__.py +6 -0
  39. polyagent/persistence/chat_store.py +235 -0
  40. polyagent/persistence/store.py +114 -0
  41. polyagent/rag/__init__.py +18 -0
  42. polyagent/rag/embedder.py +67 -0
  43. polyagent/rag/index.py +51 -0
  44. polyagent/rag/splitter.py +25 -0
  45. polyagent/rag/vectorstore.py +82 -0
  46. polyagent/skills/__init__.py +25 -0
  47. polyagent/skills/builtins/datetime_skill.py +75 -0
  48. polyagent/skills/builtins/file_analyzer_skill.py +97 -0
  49. polyagent/skills/builtins/weather_skill.py +56 -0
  50. polyagent/skills/installer.py +128 -0
  51. polyagent/skills/registry.py +57 -0
  52. polyagent/tools/__init__.py +40 -0
  53. polyagent/tools/base.py +47 -0
  54. polyagent/tools/builtins.py +656 -0
  55. polyagent/tools/registry.py +73 -0
  56. polyagent/tools/sandbox.py +78 -0
  57. scgnb666-0.1.0.dist-info/METADATA +210 -0
  58. scgnb666-0.1.0.dist-info/RECORD +61 -0
  59. scgnb666-0.1.0.dist-info/WHEEL +5 -0
  60. scgnb666-0.1.0.dist-info/entry_points.txt +2 -0
  61. 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()
@@ -0,0 +1,6 @@
1
+ """Allow `python -m polyagent.cli ...`."""
2
+
3
+ from polyagent.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()