Files
kwcode/kaiwu/validation/e2e_30tasks.py
Val-sss c6197d0316 v0.7.0: P1+P2+搜索重构+UI全面优化
P1: KWCODE.md规则注入、/plan风险评估、Checkpoint快照、DocReader
P2: 模型能力自适应、飞轮通知、价值量化仪表盘
搜索: 四级提取管道、并行搜索+BM25重排、意图感知、ChatExpert门控
UI: spinner动画、结果摘要、静默日志、重影大字Header
新增: kwcode setup-search 一键安装SearXNG
测试: 282/282 PASS (含17个E2E真实模型测试)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 17:52:59 +08:00

334 lines
11 KiB
Python

"""
E2E test harness for kwcode: 30 tasks against a real Ollama LLM.
First half: framework infrastructure (no task definitions).
Usage:
python -m kaiwu.validation.e2e_30tasks --model gemma4:e2b --group all
python -m kaiwu.validation.e2e_30tasks --task 5
"""
import argparse
import logging
import os
import shutil
import sys
import tempfile
import time
import traceback
from dataclasses import dataclass
from typing import Callable
from kaiwu.llm.llama_backend import LLMBackend
from kaiwu.core.gate import Gate
from kaiwu.core.orchestrator import PipelineOrchestrator
from kaiwu.experts.locator import LocatorExpert
from kaiwu.experts.generator import GeneratorExpert
from kaiwu.experts.verifier import VerifierExpert
from kaiwu.experts.search_augmentor import SearchAugmentorExpert
from kaiwu.experts.office_handler import OfficeHandlerExpert
from kaiwu.experts.chat_expert import ChatExpert
from kaiwu.tools.executor import ToolExecutor
from kaiwu.memory.kaiwu_md import KaiwuMemory
from kaiwu.registry.expert_registry import ExpertRegistry
logger = logging.getLogger(__name__)
# ── Constants ──────────────────────────────────────────────────────────────
DEFAULT_OLLAMA_URL = "http://localhost:11434"
DEFAULT_MODEL = "gemma4:e2b"
GROUPS = [1, 2, 3]
VALID_CATEGORIES = {"fix", "codegen", "chat", "refactor", "doc"}
COL_ID = 4
COL_GROUP = 6
COL_CAT = 10
COL_PASS = 6
COL_TIME = 8
COL_REASON = 40
# ── Pipeline builder ──────────────────────────────────────────────────────
def build_pipeline(project_root: str, ollama_model: str = DEFAULT_MODEL):
"""
Build the full kwcode pipeline wired to a real Ollama backend.
Returns (gate, orchestrator, memory).
"""
llm = LLMBackend(ollama_url=DEFAULT_OLLAMA_URL, ollama_model=ollama_model)
tool_executor = ToolExecutor(project_root)
memory = KaiwuMemory()
memory.init(project_root)
# Experts
locator = LocatorExpert(llm, tool_executor)
generator = GeneratorExpert(llm, tool_executor)
verifier = VerifierExpert(llm, tool_executor)
search_augmentor = SearchAugmentorExpert(llm)
office_handler = OfficeHandlerExpert()
chat_expert = ChatExpert(llm, search_augmentor=search_augmentor)
# Registry
registry = ExpertRegistry()
registry.load_builtin()
# Orchestrator
orchestrator = PipelineOrchestrator(
locator=locator,
generator=generator,
verifier=verifier,
search_augmentor=search_augmentor,
office_handler=office_handler,
tool_executor=tool_executor,
memory=memory,
registry=registry,
chat_expert=chat_expert,
)
# Gate
gate = Gate(llm, registry=registry)
return gate, orchestrator, memory
# ── Single task runner ────────────────────────────────────────────────────
def run_task(task_desc: str, gate, orchestrator, memory, project_root: str) -> dict:
"""
Run a single task through the full pipeline (gate -> orchestrator).
Returns a result dict with success, expert_type, elapsed, output, files, error, ctx.
"""
t0 = time.time()
error = None
ctx = None
expert_type = "unknown"
output = ""
files_changed = []
try:
memory_context = memory.load(project_root)
gate_result = gate.classify(task_desc, memory_context=memory_context)
expert_type = gate_result.get("expert_type", "unknown")
result = orchestrator.run(
user_input=task_desc,
gate_result=gate_result,
project_root=project_root,
)
success = result.get("success", False)
ctx = result.get("context")
error = result.get("error")
# Extract output text
if ctx and ctx.generator_output:
output = ctx.generator_output.get("explanation", "")
patches = ctx.generator_output.get("patches", [])
files_changed = [p.get("file", "") for p in patches if isinstance(p, dict)]
except Exception as e:
success = False
error = f"{type(e).__name__}: {e}"
logger.error("Task failed with exception:\n%s", traceback.format_exc())
elapsed = time.time() - t0
return {
"success": success,
"expert_type": expert_type,
"elapsed": elapsed,
"output": output,
"files": files_changed,
"error": error,
"ctx": ctx,
}
# ── Task definition ──────────────────────────────────────────────────────
@dataclass
class TaskDef:
id: int
group: int # 1, 2, or 3
task: str # natural language task description
category: str # "fix", "codegen", "chat", "refactor", "doc"
setup: Callable # function(project_root) -> None, sets up files
check: Callable # function(project_root, result) -> (bool, str)
# ── Group runner ──────────────────────────────────────────────────────────
def run_group(tasks: list, ollama_model: str = DEFAULT_MODEL):
"""
Run a list of TaskDef items. Each task gets its own temp directory.
Prints a results table and returns list of (task_id, passed, reason, elapsed).
"""
results = []
# Header
header = (
f"{'ID':>{COL_ID}} | "
f"{'Group':>{COL_GROUP}} | "
f"{'Category':<{COL_CAT}} | "
f"{'Pass?':<{COL_PASS}} | "
f"{'Time':>{COL_TIME}} | "
f"{'Reason':<{COL_REASON}}"
)
sep = "-" * len(header)
print(f"\n{sep}")
print(header)
print(sep)
for td in tasks:
tid = td["id"] if isinstance(td, dict) else td.id
tgroup = td["group"] if isinstance(td, dict) else td.group
ttask = td["task"] if isinstance(td, dict) else td.task
tcat = td["category"] if isinstance(td, dict) else td.category
tsetup = td["setup"] if isinstance(td, dict) else td.setup
tcheck = td["check"] if isinstance(td, dict) else td.check
tmp_dir = tempfile.mkdtemp(prefix=f"e2e_task{tid}_")
try:
# Setup
tsetup(tmp_dir)
# Build pipeline fresh for each task
gate, orchestrator, memory = build_pipeline(tmp_dir, ollama_model)
# Run
result = run_task(ttask, gate, orchestrator, memory, tmp_dir)
# Check
try:
passed, reason = tcheck(tmp_dir, result)
except Exception as e:
passed = False
reason = f"check() error: {e}"
elapsed = result["elapsed"]
results.append((tid, passed, reason, elapsed))
# Print row
pass_str = "PASS" if passed else "FAIL"
reason_trunc = reason[:COL_REASON] if reason else ""
print(
f"{tid:>{COL_ID}} | "
f"{tgroup:>{COL_GROUP}} | "
f"{tcat:<{COL_CAT}} | "
f"{pass_str:<{COL_PASS}} | "
f"{elapsed:>{COL_TIME}.1f}s | "
f"{reason_trunc:<{COL_REASON}}"
)
except Exception as e:
results.append((tid, False, f"setup/run error: {e}", 0.0))
print(
f"{tid:>{COL_ID}} | "
f"{tgroup:>{COL_GROUP}} | "
f"{tcat:<{COL_CAT}} | "
f"{'ERROR':<{COL_PASS}} | "
f"{'0.0':>{COL_TIME}}s | "
f"{str(e)[:COL_REASON]:<{COL_REASON}}"
)
finally:
# Cleanup temp dir
try:
shutil.rmtree(tmp_dir)
except OSError:
pass
# Summary
total = len(results)
passed_count = sum(1 for _, p, _, _ in results if p)
total_time = sum(e for _, _, _, e in results)
print(sep)
print(f"Total: {passed_count}/{total} passed | {total_time:.1f}s elapsed")
print(sep)
return results
# ── Task registry (populated by second half of this file) ────────────────
ALL_TASKS: list = []
# Load task definitions from group files (dicts with keys: id, group, task, category, setup, check)
try:
from kaiwu.validation.e2e_tasks_group1 import GROUP1_TASKS
ALL_TASKS.extend(GROUP1_TASKS)
except ImportError:
pass
try:
from kaiwu.validation.e2e_tasks_group2 import GROUP2_TASKS
ALL_TASKS.extend(GROUP2_TASKS)
except ImportError:
pass
try:
from kaiwu.validation.e2e_tasks_group3 import GROUP3_TASKS
ALL_TASKS.extend(GROUP3_TASKS)
except ImportError:
pass
def _get(t, key):
return t[key] if isinstance(t, dict) else getattr(t, key)
def _tasks_by_group(group: int) -> list:
return [t for t in ALL_TASKS if _get(t, "group") == group]
def _task_by_id(task_id: int):
for t in ALL_TASKS:
if _get(t, "id") == task_id:
return t
return None
# ── Main ──────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="E2E 30-task validation for kwcode")
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Ollama model (default: {DEFAULT_MODEL})")
parser.add_argument("--group", default="all", help="Task group: 1, 2, 3, or 'all'")
parser.add_argument("--task", type=str, default=None, help="Run single task by ID (e.g. T16)")
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
if args.task is not None:
td = _task_by_id(args.task)
if td is None:
print(f"Task ID {args.task} not found. Available: {[_get(t,'id') for t in ALL_TASKS]}")
sys.exit(1)
results = run_group([td], ollama_model=args.model)
elif args.group == "all":
results = run_group(ALL_TASKS, ollama_model=args.model)
else:
try:
group_num = int(args.group)
except ValueError:
print(f"Invalid group: {args.group}. Use 1, 2, 3, or 'all'.")
sys.exit(1)
if group_num not in GROUPS:
print(f"Invalid group: {group_num}. Use 1, 2, or 3.")
sys.exit(1)
tasks = _tasks_by_group(group_num)
if not tasks:
print(f"No tasks defined for group {group_num}.")
sys.exit(1)
results = run_group(tasks, ollama_model=args.model)
# Exit code: 0 if all passed, 1 otherwise
all_passed = all(p for _, p, _, _ in results)
sys.exit(0 if all_passed else 1)
if __name__ == "__main__":
main()