From bc7cabd16af9558ec63c7ae13b4ff41b0c1a1cfe Mon Sep 17 00:00:00 2001 From: xiaoxing0135 <706015750@qq.com> Date: Fri, 5 Jun 2026 14:30:03 +0800 Subject: [PATCH 1/3] fix: synthetic-monitor missing deps openpyxl/factory-boy/faker --- .github/workflows/synthetic-monitor.yml | 9 +++++---- runtime/pyproject.toml | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/synthetic-monitor.yml b/.github/workflows/synthetic-monitor.yml index a11ec5d..ef1158c 100644 --- a/.github/workflows/synthetic-monitor.yml +++ b/.github/workflows/synthetic-monitor.yml @@ -24,16 +24,17 @@ jobs: with: python-version: "3.11" - - name: Install runtime - run: pip install -e runtime/ + - name: Install runtime deps + run: | + pip install pydantic pydantic-settings typer rich loguru pyyaml openpyxl factory-boy faker prefect defusedxml prompt-toolkit httpx - name: Run demo (smoke) - run: tagent demo -y + run: python -m runtime.cli.main demo -y env: TAGENT_LLM_PROVIDER: stub - name: Run selftest - run: tagent selftest --e2e + run: python -m runtime.cli.main selftest --e2e env: TAGENT_LLM_PROVIDER: stub diff --git a/runtime/pyproject.toml b/runtime/pyproject.toml index 9b09bc9..c3e52ef 100644 --- a/runtime/pyproject.toml +++ b/runtime/pyproject.toml @@ -10,6 +10,8 @@ dependencies = [ "mcp>=1.0.0", "litellm>=1.55.0", "prefect>=2.20.0", + "factory-boy>=3.3.0", + "faker>=30.0.0", "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0", "typer>=0.16.0,<0.26.0", @@ -28,6 +30,7 @@ dependencies = [ "python-multipart>=0.0.12", "httpx>=0.27.0", "pyyaml>=6.0.1", + "openpyxl>=3.1.0", "pypdf>=6.0.0", "python-docx>=1.1.0", "rich>=13.9.0", @@ -69,6 +72,12 @@ ignore = ["E501"] asyncio_mode = "auto" testpaths = ["tests"] addopts = "--cov-branch" +log_cli = false +filterwarnings = [ + "ignore::DeprecationWarning:prefect.*", + "ignore::DeprecationWarning:starlette.*", + "ignore::pytest.PytestConfigWarning", +] markers = [ "flaky: flaky test (quarantine candidate)", "portability: portability / co-existence / replaceability test (ISO 25010)", From b4eb43d0b9af4aaf549b9b5c68dbeb88d347bb1b Mon Sep 17 00:00:00 2001 From: xiaoxing0135 <706015750@qq.com> Date: Fri, 5 Jun 2026 17:23:40 +0800 Subject: [PATCH 2/3] fix: REPL friendly error guidance + first-run onboarding + /model provider/model separation --- runtime/cli/interactive.py | 167 ++++++++++++++++++++++++++++++------- 1 file changed, 136 insertions(+), 31 deletions(-) diff --git a/runtime/cli/interactive.py b/runtime/cli/interactive.py index ccf3e98..3082a77 100644 --- a/runtime/cli/interactive.py +++ b/runtime/cli/interactive.py @@ -22,7 +22,8 @@ from runtime.cli._shared import console from runtime.cli.completer import SlashCompleter from runtime.cli.conversation import ConversationMemory -from runtime.cli.slash_commands import COMMAND_REGISTRY, resolve as resolve_command +from runtime.cli.slash_commands import COMMAND_REGISTRY +from runtime.cli.slash_commands import resolve as resolve_command _SHEEP = r""" ✧ ▗▛ 🐏 ▜▖ ✧ @@ -106,7 +107,7 @@ def _print_help() -> None: ("/ready", "Release readiness score"), ]), ("Control", [ - ("/model [name]", "Switch LLM provider (Tab to complete)"), + ("/model [provider] [model]", "Switch LLM (Tab to complete)"), ("/clear", "Reset conversation memory"), ("/setup [--preset]", "Generate config files"), ("/check [--e2e]", "Framework self-test"), @@ -130,11 +131,56 @@ def _print_help() -> None: console.print() +# ── Error Diagnosis ───────────────────────────────────────────────── + + +def _diagnose_error(exc: Exception) -> str | None: + """Return a friendly Chinese/English hint for common errors. None if no specific advice.""" + _msg = str(exc).lower() + _t = type(exc).__name__ + + # API key / auth errors + if any(k in _msg for k in ("api_key", "api key", "apikey", "unauthorized", "401", "credential", "authentication")): + provider = _current_provider() + return ( + f"LLM ({provider}) needs an API key. " + f"Set [cyan]TAGENT_LLM_API_KEY[/] in [cyan].env[/] or environment. " + f"Run [cyan]tagent setup --preset minimal[/] to generate a template." + ) + + # Missing module / import errors + if _t in ("ModuleNotFoundError", "ImportError"): + mod = _msg.split("'")[1] if "'" in _msg else "?" + return ( + f"Missing Python package: [cyan]{mod}[/]. " + f"Run [cyan]pip install -e runtime/[/] or [cyan]pip install {mod}[/]." + ) + + # Connection / network errors + if any(k in _msg for k in ("connection", "timeout", "refused", "unreachable", "ssl", "dns", "resolve")): + return ( + "Cannot reach the LLM service. Check your network, proxy settings, " + "or [cyan]TAGENT_LLM_API_BASE[/] in [cyan].env[/]." + ) + + # Rate limit + if any(k in _msg for k in ("rate limit", "429", "too many")): + return "Rate limited by the LLM provider. Wait a moment and try again." + + # Invalid request / bad gateway from LLM + if any(k in _msg for k in ("500", "502", "503", "internal", "bad gateway")): + provider = _current_provider() + return f"{provider} service returned a server error. The provider may be down — try again or switch with [cyan]/model[/]." + + # General: give the error message itself as info, with next steps + return None + + # ── Streaming Activity Feed ──────────────────────────────────────── def _handle_natural_language(text: str) -> None: - """Route through LLM with streaming activity output.""" + """Route through routing kernel with streaming activity output.""" if not text.strip(): return @@ -150,37 +196,42 @@ def _handle_natural_language(text: str) -> None: console.print(f"[dim]\"{summary}\"[/]") t0 = time.time() - _old_argv = None try: - import sys as _sys, contextlib, io - - _old_argv = _sys.argv[:] - _sys.argv = ["tagent", "run", context_input] + from runtime.cli._shared import _kernel, build_artifact with console.status("[bold green]Routing...", spinner="dots"): - from runtime.cli.commands.run import run as _run - _capture = io.StringIO() - with contextlib.redirect_stdout(_capture): - _run() + art = build_artifact(context_input, "") + run_id, decision = _kernel.submit(art, persist=False) + + print_dag = __import__("runtime.cli._shared", fromlist=["print_dag"]).print_dag + print_dag(decision) + summary = _kernel.execute_sync(run_id, decision) elapsed = (time.time() - t0) * 1000 - output = _capture.getvalue().strip() - if output: - console.print(output) - console.print(f" [dim]Completed in {elapsed:.0f}ms[/]") - mem.add("assistant", output[:500] if output else f"[Run: {text[:100]}]") - except SystemExit: - pass + total = summary["total"] + succ = summary["succeeded"] + rate = succ / total if total else 0.0 + console.print(f" [green]✓ {succ}/{total} ok ({rate:.0%})[/] [dim]({elapsed:.0f}ms)[/]") + mem.add("assistant", f"DAG: {succ}/{total} ok, {summary.get('failed', 0)} failed") except KeyboardInterrupt: console.print(f" [yellow]Cancelled[/] [dim]({(time.time()-t0)*1000:.0f}ms)[/]") mem.add("assistant", "[Cancelled]") - except Exception: - console.print(f" [red]Error[/] [dim]({(time.time()-t0)*1000:.0f}ms)[/]") - mem.add("assistant", "[Error: command failed]") - finally: - if _old_argv is not None: - import sys as _sys - _sys.argv = _old_argv + except Exception as _exc: + _err_msg = str(_exc)[:300] + elapsed = (time.time() - t0) * 1000 + console.print(f" [red]✗ {type(_exc).__name__}[/] [dim]({elapsed:.0f}ms)[/]") + + # ── friendly guidance based on error type ── + _hint = _diagnose_error(_exc) + if _hint: + console.print(f" [yellow]💡 {_hint}[/]") + elif _err_msg: + console.print(f" [dim]{_err_msg}[/]") + console.print(" [dim]Run [cyan]/help[/] for commands, [cyan]/doctor[/] for health check.[/]") + else: + console.print(" [dim]Run [cyan]/doctor[/] to check environment, [cyan]/help[/] for commands.[/]") + + mem.add("assistant", f"[Error: {type(_exc).__name__}]") # ── Fuzzy matching (thefuck-style) ───────────────────────────────── @@ -264,23 +315,49 @@ def _cmd_status(args: str) -> None: def _cmd_model(args: str) -> None: - name = args.strip().lower() + parts = args.strip().split() + name = parts[0].lower() if parts else "" + model_override = parts[1] if len(parts) > 1 else None current = _current_provider() if not name: console.print(f"Current: [cyan]{current}[/] → {_current_model()}\n") console.print("Available:") for p in _PROVIDERS: - console.print(f" {p}{' [bold green]← current[/]' if p == current else ''}") - console.print("\n[dim]Usage: /model Tab to see options[/]") + models = { + "claude": "sonnet-4-6 / opus-4-8 / haiku-4-5", + "openai": "gpt-4o / gpt-4.1 / o4-mini", + "gemini": "gemini-1.5-pro / gemini-2.5-flash", + "deepseek": "deepseek-chat / deepseek-reasoner", + "qwen": "qwen-plus / qwen-max / qwen-turbo", + "ollama": "any local model (e.g. qwen2.5:7b)", + } + marker = " [bold green]← current[/]" if p == current else "" + detail = models.get(p, "") + console.print(f" [cyan]{p}[/]{marker} [dim]{detail}[/]") + console.print("\n[dim]Usage: /model [model] e.g. /model deepseek deepseek-chat[/]") return + # Check if user typed a model name instead of provider if name not in _PROVIDERS: - console.print(f"[red]Unknown: {name}[/] Available: {', '.join(_PROVIDERS)}") + # Try fuzzy match against known models → providers + for p in _PROVIDERS: + if name.startswith(p) or p.startswith(name): + console.print(f"[yellow]'{name}' is a model name. Did you mean [cyan]/model {p}[/]?[/]") + break + else: + console.print(f"[red]Unknown provider: {name}[/]") + console.print(f"[dim]Available: {', '.join(_PROVIDERS)}[/]") + console.print("[dim]Tip: provider first, then model — e.g. [cyan]/model deepseek deepseek-chat[/][/]") return os.environ["TAGENT_LLM_PROVIDER"] = name - console.print(f"[green]Switched to {name}[/] → {_current_model()}") + if model_override: + os.environ["TAGENT_LLM_MODEL"] = model_override + else: + os.environ.pop("TAGENT_LLM_MODEL", None) # use default + + console.print(f"[green]Switched[/] → provider: [cyan]{name}[/] model: [cyan]{_current_model()}[/]") # ── /tools — dynamic agent/skill list ────────────────────────────── @@ -385,6 +462,7 @@ def _cmd_cost(args: str) -> None: def _cmd_sessions(args: str) -> None: from datetime import datetime + from rich.table import Table if not _SESSION_DIR.is_dir(): @@ -573,11 +651,38 @@ def _read_input(session: PromptSession | None) -> str | None: return None +def _check_first_run() -> None: + """Detect unconfigured state and show a friendly onboarding guide.""" + provider = _current_provider() + if provider in ("ollama", "stub"): + return # local/stub — no API key needed + + api_key = os.environ.get("TAGENT_LLM_API_KEY", "") + has_env_file = (_Path(__file__).resolve().parents[2] / ".env").exists() + + if not api_key and not has_env_file: + console.print( + "\n[yellow]👋 欢迎!看起来这是你第一次使用 Test-Agent。[/]\n" + "[bold]快速上手 (3 步):[/]\n" + " 1. [cyan]tagent setup --preset minimal[/] → 生成 .env 模板\n" + " 2. 编辑 [cyan].env[/] → 填入你的 LLM API key\n" + " 3. [cyan]tagent demo -y[/] → 一键验证 (0 费用, stub 模式)\n" + "\n[dim]现在可以先跑 demo 看看效果: /check --e2e[/]\n" + ) + elif not api_key: + console.print( + f"\n[yellow]⚠️ 当前 provider [cyan]{provider}[/] 未配置 API key。[/]\n" + f" [dim]设置: set TAGENT_LLM_API_KEY=你的key[/]\n" + f" [dim]或在 .env 文件中添加: TAGENT_LLM_API_KEY=你的key[/]\n" + ) + + def start() -> None: global _start_time _start_time = time.time() _print_banner() + _check_first_run() mem = _get_memory() if mem.messages: From cb4e7edfd66b2d1e459a781fcf055063a3f4db9e Mon Sep 17 00:00:00 2001 From: xiaoxing0135 <706015750@qq.com> Date: Fri, 5 Jun 2026 19:46:44 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20Agent=E4=BA=A4=E4=BA=92=E5=B1=82?= =?UTF-8?q?=E8=B7=AF=E7=BA=BF=E5=9B=BE=20(P0-P3,=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8E=E5=88=A0=E9=99=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...02\350\267\257\347\272\277\345\233\276.md" | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 "Agent\344\272\244\344\272\222\345\261\202\350\267\257\347\272\277\345\233\276.md" diff --git "a/Agent\344\272\244\344\272\222\345\261\202\350\267\257\347\272\277\345\233\276.md" "b/Agent\344\272\244\344\272\222\345\261\202\350\267\257\347\272\277\345\233\276.md" new file mode 100644 index 0000000..262b5eb --- /dev/null +++ "b/Agent\344\272\244\344\272\222\345\261\202\350\267\257\347\272\277\345\233\276.md" @@ -0,0 +1,89 @@ +# Agent 交互层路线图 + +> 策略:保留测试核心,渐进补齐 Agent 交互层 +> ⚠️ 临时文档,全部完成后删除 + +--- + +## 图例 + +- `[✓]` 已完成 · `[ ]` 待完成 · `[WIP]` 进行中 + +--- + +## P0 · 基础修复 + +| # | 功能 | 状态 | +|---|------|:--:| +| 1 | REPL 自然语言路由修复(`_handle_natural_language` Kernel API) | [✓] | +| 2 | 友好错误引导(`_diagnose_error` 分类提示) | [✓] | +| 3 | 首次使用引导(`_check_first_run` 3 步上手) | [✓] | +| 4 | `/model` 分离 provider + model | [✓] | + +--- + +## P1 · 体感层 + +| # | 功能 | 状态 | +|---|------|:--:| +| 5 | 流式输出(execute_sync → generator yield) | [ ] | +| 6 | MEMORY.md 持久化(跨会话记忆) | [ ] | +| 7 | Tab 补全增强(agent/skill 名) | [ ] | +| 8 | 错误交互全覆盖(所有 /command 友好提示) | [ ] | +| 9 | 启动欢迎动画 | [ ] | + +--- + +## P2 · 能力层 + +| # | 功能 | 状态 | +|---|------|:--:| +| 10 | IM 多渠道接入(Telegram / Discord / 飞书 webhook) | [ ] | +| 11 | Sub-agent 对话触发("帮我测试 X"→ 自动启编排) | [ ] | +| 12 | MCP client 完善(`runtime/mcp/` 已有基础) | [ ] | +| 13 | 定时主动任务(自检 + 报告推送) | [ ] | +| 14 | 模型自动路由(轻量分类 + 重量执行) | [ ] | +| 15 | 多行输入(代码块、长文本粘贴) | [ ] | + +--- + +## P3 · 深度层 + +| # | 功能 | 状态 | +|---|------|:--:| +| 16 | 会话全文搜索(SQLite FTS5) | [ ] | +| 17 | 上下文智能压缩(长对话自动总结) | [ ] | +| 18 | 技能自进化(自动创建/评分 Skill) | [ ] | +| 19 | 7×24 daemon 模式(`tagent serve --daemon`) | [ ] | +| 20 | 用户画像(自动学习偏好) | [ ] | +| 21 | 智能审批(学习信任命令,减少打断) | [ ] | +| 22 | 插件热加载(`plugins/` 目录 drop-in) | [ ] | +| 23 | 语音交互 | [ ] | + +--- + +## 架构原则 + +``` +不改测试核心(16 Experts + 32 Skills + 79 Utils + Prefect 编排) +只在上面加 Agent 交互层。 + + ┌──────────────────────────┐ + │ Agent 交互层 (新增) │ ← P1-P3 + │ REPL · IM · Memory │ + ├──────────────────────────┤ + │ 测试编排层 (已有) │ ← 不动 + │ test-coordinator · DAG │ + ├──────────────────────────┤ + │ 执行引擎层 (已有) │ ← 不动 + │ runtime · utils · CLI │ + └──────────────────────────┘ +``` + +## 修改纪律 + +每次修改遵守 Karpathy 四纪律: +1. 先想再写 — 查根因、列假设 +2. 简单优先 — 最小代码,不复用不抽象 +3. 手术修改 — 只改必须改,匹配现有风格 +4. 目标驱动 — 每行可追溯到任务