Files
kwcode/kaiwu/cli/formatters.py
Val-sss 2c4d792baa feat: v1.7.0 KAIJU架构借鉴 — bounded context + sub-task decomposition + DetailedLogger
三个核心改动:
1. Generator bounded context: 只传当前函数代码+相关failing tests,不传完整历史
2. 存根任务sub-task decomposition: 多pass函数逐个独立实现,每个函数独立context
3. DetailedLogger完整流水线日志: 不截断LLM输入输出,记录各节点IO和工程决策

附带修复:
- OpenAI兼容API检测: localhost非标准端口探测/api/tags判断是否Ollama
- LLM Backend on_call钩子: 每次调用自动触发回调记录

513 tests passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 18:24:56 +08:00

239 lines
9.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Rich output formatters for CLI: spinners, result summaries, headers, event handlers.
"""
import os
import logging
from rich.console import Console
logger = logging.getLogger(__name__)
console = Console()
# EventBus event icons (追加式渲染替代单行spinner)
EVENT_ICONS = {
"expert_start": ("", "blue"),
"reading_file": (" 📄", "dim"),
"file_written": ("", "green"),
"applying_patch": ("", "yellow"),
"patch_result": ("", "green"),
"generator_patch": ("", "yellow"),
"test_pass": ("", "green"),
"test_fail": ("", "red"),
"retry": ("🔄", "yellow"),
"circuit_break": ("", "red"),
"scope_narrow": ("🎯", "cyan"),
"search_start": ("🌐", "blue"),
"search_solution": ("💡", "cyan"),
"plan_generated": ("📋", "blue"),
"pre_compact": ("📦", "dim"),
"wink_intervene": ("🔧", "yellow"),
}
# 阶段级事件(换行显示)
_PHASE_EVENTS = {"expert_start", "retry", "circuit_break", "plan_generated", "wink_intervene"}
def eventbus_cli_handler(event: str, payload: dict):
"""EventBus 全局 CLI handler追加式渲染事件到终端。"""
icon_info = EVENT_ICONS.get(event)
if not icon_info:
return
icon, color = icon_info
detail = payload.get("path") or payload.get("msg") or payload.get("cmd", "")
if not detail:
return
if event in _PHASE_EVENTS:
console.print()
console.print(f"[bold {color}]{icon} {detail}[/bold {color}]")
else:
console.print(f"[{color}]{icon} {detail}[/{color}]")
# Spinner stage mapping (internal stage → user-friendly description)
SPINNER_STAGES = {
"gate": "分析任务...",
"locator": "定位代码...",
"locator_done": None, # silent
"generator": "生成修改...",
"generator_done": None,
"verifier": "验证结果...",
"verifier_done": None,
"search": "搜索增强中...",
"search_done": None,
"chat": "思考中...",
"vision": "分析图片...",
"reflection": "分析失败原因...",
"checkpoint": None,
"warning": None,
"suggest": None,
"retry": None,
}
# Verbose mode: old-style text output (only with --verbose)
def verbose_callback(stage: str, detail: str):
"""Verbose status callback — only used with --verbose flag."""
colors = {
"gate": "cyan", "locator": "blue", "locator_done": "green",
"generator": "blue", "generator_done": "green",
"verifier": "blue", "verifier_done": "green",
"search": "magenta", "search_done": "magenta",
}
if "fail" in stage or "retry" in stage:
console.print(f" [yellow]> {detail}[/yellow]")
elif "done" in stage:
console.print(f" [green]> {detail}[/green]")
else:
color = colors.get(stage, "dim")
console.print(f" [{color}]> {detail}[/{color}]")
try:
from importlib.metadata import version as _pkg_version
VERSION = _pkg_version("kwcode")
except Exception:
VERSION = "1.7.0"
# ── Shadow/重影大字 KAIWU ──
_KAIWU_SHADOW = [
" [bold white]██╗ ██╗ █████╗ ██╗██╗ ██╗██╗ ██╗[/bold white]",
" [bold white]██║ ██╔╝██╔══██╗██║██║ ██║██║ ██║[/bold white]",
" [bold white]█████╔╝ ███████║██║██║ █╗ ██║██║ ██║[/bold white]",
" [bold white]██╔═██╗ ██╔══██║██║██║███╗██║██║ ██║[/bold white]",
" [bold white]██║ ██╗██║ ██║██║╚███╔███╔╝╚██████╔╝[/bold white]",
" [bold white]╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══╝╚══╝ ╚═════╝[/bold white]",
]
def render_header(model: str, project_root: str, registry=None):
"""启动Header重影大字 KAIWU + 简洁信息行。"""
short = project_root.replace(os.path.expanduser("~"), "~")
if len(short) > 35:
short = "..." + short[-32:]
expert_count = len(registry.experts) if registry and hasattr(registry, 'experts') else 0
console.print()
for line in _KAIWU_SHADOW:
console.print(line)
console.print(f" [dim]天工开物 v{VERSION}[/dim]")
console.print(" " + "" * min(console.width - 4, 50))
console.print(
f" [green]{model}[/green] · [cyan]{short}[/cyan] · "
f"[dim]{expert_count} 专家[/dim]"
)
console.print()
def render_task_success(result: dict, et: str, project_root: str):
"""Render successful task output."""
elapsed = result.get("elapsed", 0)
ctx = result["context"]
# Chat: print reply directly
if et == "chat":
reply = ""
if ctx.generator_output:
reply = ctx.generator_output.get("explanation", "")
console.print(f"\n {reply}" if reply else
"\n 你好我是KWCode专注于代码任务。有什么代码问题需要帮忙吗")
return
# Vision: print the analysis/code result directly.
if et == "vision":
output = ""
if ctx.generator_output:
output = ctx.generator_output.get("explanation", "")
console.print(f"\n {output}" if output else "\n [yellow]图片处理完成,但没有返回内容[/yellow]")
return
# Collect file info
files = []
if ctx.generator_output:
files = [p.get("file", "") for p in ctx.generator_output.get("patches", [])]
elif ctx.locator_output:
files = ctx.locator_output.get("relevant_files", [])
is_codegen = et == "codegen" and not ctx.locator_output
# Success header
if is_codegen and files:
for f in files:
full = os.path.join(project_root, f) if not os.path.isabs(f) else f
console.print(f"\n [bold green]✓ 已生成 {full}[/bold green] ({elapsed:.1f}s)")
else:
if files:
console.print(f"\n [bold green]✓ 完成[/bold green] ({elapsed:.1f}s)")
for f in files[:3]:
console.print(f" 修改了 {f}")
else:
console.print(f"\n [bold green]✓ 完成[/bold green] ({elapsed:.1f}s)")
# Summary bullets from explanation
if ctx.generator_output and ctx.generator_output.get("explanation"):
explanation = ctx.generator_output["explanation"]
lines = [l.strip() for l in explanation.split("\n") if l.strip()][:3]
for line in lines:
console.print(f" · {line[:60]}")
# Test results
if ctx.verifier_output:
passed = ctx.verifier_output.get("tests_passed", 0)
total = ctx.verifier_output.get("tests_total", 0)
if total > 0:
console.print(f" 测试通过 ({passed}/{total})")
def render_task_failure(result: dict):
"""Render failed task output."""
elapsed = result.get("elapsed", 0)
console.print(f"\n [bold red]✗ 失败[/bold red] ({elapsed:.1f}s)")
ctx = result.get("context")
error = result.get("error")
if error:
console.print(f" 原因:{str(error)[:200]}")
if ctx and ctx.generator_output and ctx.generator_output.get("explanation"):
lines = [l.strip() for l in ctx.generator_output["explanation"].split("\n") if l.strip()][:3]
if lines and not error:
console.print(" 原因:")
for line in lines:
console.print(f" {line[:80]}")
if ctx and ctx.verifier_output:
detail = ctx.verifier_output.get("error_detail", "")
if detail:
lines = [l.strip() for l in detail.split("\n") if l.strip()][:3]
console.print(f" 原因:")
for line in lines:
console.print(f" {line[:80]}")
def render_model_error(e: Exception):
"""Render model call failure with suggestions."""
console.print(f"\n [red]❌ 模型调用失败[/red]")
console.print(f" [yellow]错误详情:[/yellow]{e}")
console.print("\n [cyan]💡 可能的解决方案:[/cyan]")
console.print(" 1. 检查模型是否正常运行:[dim]ollama list[/dim]")
console.print(" 2. 切换到其他模型:[dim]/model qwen3:8b[/dim]")
console.print(" 3. 检查 API 配置:[dim]/api show[/dim]")
console.print(" 4. 如果使用云端 API检查网络连接和 API key")
def render_execution_error(e: Exception):
"""Render execution exception with debug suggestions."""
import traceback
console.print(f"\n [red]❌ 执行异常[/red]")
console.print(f" [yellow]错误详情:[/yellow]{e}")
console.print("\n [cyan]💡 调试建议:[/cyan]")
console.print(" 1. 查看详细日志:[dim]~/.kwcode/kwcode.log[/dim]")
console.print(" 2. 尝试更简单的任务描述")
console.print(" 3. 使用 /plan 模式查看执行步骤:[dim]/plan <任务描述>[/dim]")
console.print(" 4. 如果问题持续,请在 GitHub 提交 issue")
console.print(f"\n [dim]堆栈跟踪:\n{traceback.format_exc()}[/dim]")
def escape_html(text: str) -> str:
"""Escape HTML special chars for prompt_toolkit HTML."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")