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>
This commit is contained in:
Val-sss
2026-05-07 18:24:56 +08:00
parent ca55970ad8
commit 2c4d792baa
12 changed files with 582 additions and 36 deletions

View File

@@ -4,6 +4,32 @@ All notable changes to KWCode are documented here.
---
## [1.7.0] - 2026-05-07
### KAIJU架构借鉴 + DetailedLogger + Bounded Context
**核心理念**借鉴KAIJU三个具体机制——bounded context per node、sub-task decomposition、完整流水线日志。
### Added
- **DetailedLogger完整流水线日志**`kaiwu/audit/detailed_logger.py`每个任务生成独立JSON日志到`logs/`目录记录LLM完整prompt/output不截断、各节点输入输出、工程决策重试/熔断/搜索)。环境变量`KWCODE_DETAIL_LOG_DIR`可配置输出目录
- **LLM Backend on_call钩子**每次LLM调用自动触发回调记录完整messages和response到DetailedLogger
- **存根任务sub-task decomposition**`_run_stub_decomposed`多个pass函数不再一次性让LLM实现而是逐函数独立调用每个函数独立context+独立失败筛选,一个函数失败不影响其他
- **`_find_stub_functions`**精确检测文件中的stub函数pass/.../ return None/raise NotImplementedError
- **`_filter_relevant_failures`**:按函数名/文件名筛选相关测试失败只给LLM看它需要的信息
### Changed
- **Generator bounded context**`_generate_modified`不再注入全部8条structured_failures和完整retry_hint而是只传与当前函数相关的失败通过函数名/文件名匹配筛选retry_hint截断到300字符
- **OpenAI兼容API检测修复**localhost非标准端口如kaiwu部署器11435现在通过探测`/api/tags`判断是否Ollama不再错误地走`/api/chat`导致404
- **Gate LLM调用记录**`_llm_minimal_classify`中增加debug日志记录prompt和raw output
### Fixed
- **kaiwu部署器llama.cpp server兼容**`_detect_openai_compat``127.0.0.1:11435`正确返回TrueOpenAI兼容不再当作Ollama
---
## [1.6.2] - 2026-05-07
### 执行反馈深度升级 + 存根任务修复

View File

@@ -10,13 +10,13 @@
[![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://python.org)
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Mac%20%7C%20Linux-lightgrey.svg)]()
[![Multi-Platform Tests](https://github.com/val1813/kwcode/actions/workflows/test.yml/badge.svg)](https://github.com/val1813/kwcode/actions/workflows/test.yml)
[![Version](https://img.shields.io/badge/Version-1.6.2-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-1.7.0-blue.svg)]()
</div>
---
> **v1.6.2 已发布!** 执行反馈深度升级:结构化测试失败解析 + TraceCoder历史教训累积 + whole_file写入修复。安装命令:
> **v1.7.0 已发布!** KAIJU架构借鉴Generator bounded context + 存根任务sub-task decomposition + DetailedLogger完整流水线日志。安装命令:
>
> ```bash
> pip install kwcode
@@ -30,6 +30,7 @@
| 日期 | 内容 |
|------|------|
| 05-07 | **v1.7.0** KAIJU架构借鉴Generator bounded context(只传当前函数+相关测试) + 存根任务sub-task decomposition(逐函数独立实现) + DetailedLogger完整流水线日志(不截断LLM输入输出) + OpenAI兼容API检测修复(kaiwu部署器兼容) |
| 05-07 | **v1.6.2** 执行反馈深度升级:结构化测试失败解析(parse_test_failures) + TraceCoder历史教训累积(attempt_history) + whole_file写入修复(存根任务不再patches=0) + 完整审计日志(llm_calls/node_io) + pytest -v详细输出 |
| 05-07 | **v1.6.1** 架构收敛删除WholeFileImplExpert/DependencyFixExpert纯确定性机制驱动pipeline。Generator增强(upstream_constraints注入system prompt + retry_hint携带上次代码 + tier=small填空框架)。License改为Apache-2.0。513 tests green |
| 05-07 | **v1.6.0** MoE确定性架构GapDetector(11种GapType,零LLM) + ExecutionStateTracker(回归检测) + EnvProber(工具链/依赖自动修复) + Gate确定性优先路由 + 63个专项诊断测试 |

View File

@@ -10,8 +10,7 @@
[![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://python.org)
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Mac%20%7C%20Linux-lightgrey.svg)]()
[![Tests](https://img.shields.io/badge/Tests-282%2F282-brightgreen.svg)]()
[![Version](https://img.shields.io/badge/Version-0.7.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-1.7.0-blue.svg)]()
</div>
---

View File

@@ -0,0 +1,184 @@
"""
DetailedLogger: 完整不截断的流水线日志记录器。
每个任务生成一个 JSON 文件,记录:
- LLM 完整 prompt/output不截断
- 各节点Gate/Locator/Generator/Verifier的输入输出
- 工程机制决策(重试策略、搜索、熔断等)
输出目录:环境变量 KWCODE_DETAIL_LOG_DIR默认为项目 logs/ 目录。
设为空字符串可禁用。
"""
import json
import logging
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# 默认日志目录(相对于源码根目录,开发调试用)
_DEFAULT_LOG_DIR = Path(__file__).resolve().parent.parent.parent / "logs"
def _get_log_dir() -> Optional[Path]:
"""获取详细日志输出目录。返回 None 表示禁用。"""
env_val = os.environ.get("KWCODE_DETAIL_LOG_DIR")
if env_val is not None:
if env_val == "":
return None # 显式禁用
return Path(env_val)
return _DEFAULT_LOG_DIR
class DetailedLogger:
"""单任务详细日志记录器。非阻塞,所有操作失败静默。"""
def __init__(self, user_input: str = "", model: str = "unknown"):
self._enabled = True
self._log_dir = _get_log_dir()
if self._log_dir is None:
self._enabled = False
return
self._start_time = time.time()
self._task_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
self._user_input = user_input
self._model = model
self._timeline: list[dict] = []
self._metadata: dict = {}
@property
def enabled(self) -> bool:
return self._enabled
def set_metadata(self, **kwargs):
"""设置任务级元数据expert_type, difficulty, routing_source 等)。"""
if not self._enabled:
return
self._metadata.update(kwargs)
def log_llm(self, caller: str, prompt: str, system: str,
raw_output: str, tokens: Optional[dict] = None,
elapsed_ms: float = 0, messages: Optional[list] = None):
"""
记录一次 LLM 调用(完整不截断)。
Args:
caller: 调用方标识gate/generator/verifier/reflection 等)
prompt: 完整 prompt 文本(非 chat 模式时)
system: system prompt
raw_output: LLM 原始输出
tokens: {"input": N, "output": N}
elapsed_ms: 调用耗时毫秒
messages: chat 模式的完整 messages 列表
"""
if not self._enabled:
return
try:
entry = {
"time": datetime.now().strftime("%H:%M:%S.%f")[:-3],
"elapsed_s": round(time.time() - self._start_time, 2),
"type": "llm_call",
"caller": caller,
"input": {},
"output": raw_output,
"elapsed_ms": round(elapsed_ms, 1),
"tokens": tokens or {},
}
if messages:
entry["input"]["messages"] = messages
else:
entry["input"]["system"] = system
entry["input"]["prompt"] = prompt
self._timeline.append(entry)
except Exception:
pass
def log_node(self, stage: str, input_data: dict, output_data: dict,
detail: str = ""):
"""
记录一个流水线节点的输入输出。
Args:
stage: 节点名称gate/locator/generator/verifier/search 等)
input_data: 节点接收的输入
output_data: 节点产出的输出
detail: 可选的补充说明
"""
if not self._enabled:
return
try:
entry = {
"time": datetime.now().strftime("%H:%M:%S.%f")[:-3],
"elapsed_s": round(time.time() - self._start_time, 2),
"type": "node_io",
"stage": stage,
"input": input_data,
"output": output_data,
}
if detail:
entry["detail"] = detail
self._timeline.append(entry)
except Exception:
pass
def log_decision(self, stage: str, decision: str, reason: str = "",
context: Optional[dict] = None):
"""
记录一个工程决策(重试策略选择、熔断、搜索触发等)。
Args:
stage: 决策发生的阶段
decision: 决策内容
reason: 决策原因
context: 相关上下文数据
"""
if not self._enabled:
return
try:
entry = {
"time": datetime.now().strftime("%H:%M:%S.%f")[:-3],
"elapsed_s": round(time.time() - self._start_time, 2),
"type": "decision",
"stage": stage,
"decision": decision,
"reason": reason,
}
if context:
entry["context"] = context
self._timeline.append(entry)
except Exception:
pass
def write(self, expert_type: str = "unknown", success: bool = False):
"""任务结束时写入日志文件。非阻塞。"""
if not self._enabled:
return
try:
self._log_dir.mkdir(parents=True, exist_ok=True)
record = {
"task_id": self._task_id,
"user_input": self._user_input,
"model": self._model,
"expert_type": expert_type,
"success": success,
"total_elapsed_s": round(time.time() - self._start_time, 2),
"timestamp": datetime.now().isoformat(),
**self._metadata,
"timeline": self._timeline,
}
filename = f"{self._task_id}_{expert_type}.json"
filepath = self._log_dir / filename
filepath.write_text(
json.dumps(record, ensure_ascii=False, indent=2),
encoding="utf-8",
)
logger.debug("DetailedLog written: %s", filepath)
except Exception as e:
logger.debug("DetailedLogger write failed (non-blocking): %s", e)

View File

@@ -94,7 +94,7 @@ try:
from importlib.metadata import version as _pkg_version
VERSION = _pkg_version("kwcode")
except Exception:
VERSION = "1.6.2"
VERSION = "1.7.0"
# ── Shadow/重影大字 KAIWU ──
_KAIWU_SHADOW = [

View File

@@ -110,3 +110,6 @@ class TaskContext:
# 审计日志引用Generator等专家通过此记录LLM调用
_audit_logger: "Any" = None
# DetailedLogger引用完整不截断的流水线日志
_detailed_logger: "Any" = None

View File

@@ -263,6 +263,9 @@ class Gate:
max_tokens=30,
temperature=0.0,
)
# DetailedLogger: 记录 Gate LLM 调用(通过 llm._on_llm_call 已自动记录,
# 这里额外记录解析结果到 logger
logger.debug("[gate_llm] prompt=%s, raw=%s", prompt[:200], raw[:200])
# 尝试解析JSON响应
json_str = self._extract_json(raw)

View File

@@ -45,6 +45,7 @@ from kaiwu.flywheel.strategy_stats import StrategyStats
from kaiwu.flywheel.user_pattern_memory import UserPatternMemory
from kaiwu.telemetry.client import TelemetryClient
from kaiwu.audit.logger import AuditLogger
from kaiwu.audit.detailed_logger import DetailedLogger
from kaiwu.core.model_capability import detect_model_tier, STRATEGIES, ModelTier
logger = logging.getLogger(__name__)
@@ -178,6 +179,28 @@ class PipelineOrchestrator:
start_time = time.time()
self._audit.start()
# ── DetailedLogger完整流水线日志 ──
model_name = getattr(self, '_model_name', 'unknown')
self._detailed = DetailedLogger(user_input=user_input, model=model_name)
self._detailed.set_metadata(
project_root=project_root,
gate_result=gate_result,
)
# 设置 LLM 回调,自动记录每次调用
if self._detailed.enabled:
def _llm_hook(messages, raw_output, elapsed_ms):
self._detailed.log_llm(
caller="llm_backend",
prompt="",
system="",
raw_output=raw_output,
elapsed_ms=elapsed_ms,
messages=messages,
)
llm = getattr(self.generator, 'llm', None)
if llm and hasattr(llm, '_on_llm_call'):
llm._on_llm_call = _llm_hook
# 任务级超时看门狗
TASK_TIMEOUT_S = 300 # 单任务最长5分钟
_watchdog_triggered = threading.Event()
@@ -201,6 +224,8 @@ class PipelineOrchestrator:
)
# 注入审计日志引用让Generator等专家能记录LLM调用
ctx._audit_logger = self._audit
# 注入 DetailedLogger 引用到 ctx让 Generator 等专家能直接写入
ctx._detailed_logger = self._detailed
# 模型能力等级注入ctx
ctx.model_tier = self._model_tier.value
@@ -331,6 +356,10 @@ class PipelineOrchestrator:
sequence = EXPERT_SEQUENCES.get(expert_type, ["generator", "verifier"])
self._emit(on_status, "gate", f"任务类型:{expert_type} | 难度:{gate_result.get('difficulty', '?')} | 路由:{ctx.routing_source}")
# DetailedLogger: gate 决策
self._detailed.log_node("gate",
{"user_input": user_input[:500]},
{"expert_type": expert_type, "difficulty": gate_result.get("difficulty", "?"), "routing_source": ctx.routing_source, "confidence": gate_result.get("confidence", 0), "sequence": sequence})
# ── MoE专家选择已移除独立Expert类统一走pipeline ──
moe_expert = self._select_moe_expert(ctx, expert_type)
@@ -468,6 +497,11 @@ class PipelineOrchestrator:
self._emit(on_status, "retry", f"{ctx.retry_count}次尝试失败:{error_detail[:100]}")
self.bus.emit("retry", {"count": ctx.retry_count, "error": error_detail[:100]})
# DetailedLogger: 记录重试决策
self._detailed.log_decision("retry",
decision=f"retry #{ctx.retry_count}, strategy={retry_strategy.get('sequence', [])}",
reason=f"error_type={current_error_type}, detail={error_detail[:200]}",
context={"error_type_streak": ctx._error_type_streak, "attempt_record": attempt_record})
# 第2次重试前反思让LLM分析失败原因
if ctx.retry_count == 1 and ctx.verifier_output and ctx.generator_output:
@@ -566,6 +600,8 @@ class PipelineOrchestrator:
ctx.generator_output = {"explanation": "我是KWCode专注于代码任务。", "patches": []}
result = {"passed": True}
elapsed = time.time() - start_time
self._detailed.log_node("chat", {"user_input": ctx.user_input[:500]}, {"explanation": (ctx.generator_output or {}).get("explanation", "")[:500]})
self._detailed.write(expert_type="chat", success=True)
return {
"success": True,
"context": ctx,
@@ -723,6 +759,8 @@ class PipelineOrchestrator:
self._record_flywheel(ctx, gate_result, True)
# 审计日志
self._audit.write(ctx, elapsed, True, getattr(self, '_model_name', 'unknown'))
# DetailedLogger: 写入成功日志
self._detailed.write(expert_type=gate_result.get("expert_type", "unknown"), success=True)
return {
"success": True,
"context": ctx,
@@ -763,6 +801,8 @@ class PipelineOrchestrator:
self._record_flywheel(ctx, gate_result, False)
# 审计日志
self._audit.write(ctx, elapsed, False, getattr(self, '_model_name', 'unknown'))
# DetailedLogger: 写入失败日志
self._detailed.write(expert_type=gate_result.get("expert_type", "unknown"), success=False)
return {
"success": False,
"context": ctx,
@@ -779,6 +819,7 @@ class PipelineOrchestrator:
search_result = self._search_subagent.search(ctx, self._manifest)
if not search_result or not search_result.get("relevant_files"):
self._emit(on_status, "locator_fail", "定位失败")
self._detailed.log_node("locator", {"user_input": ctx.user_input[:200]}, {"result": None}, detail="定位失败")
return False
# 将干净结果传给ctxGenerator只看到这些
ctx.locator_output = {
@@ -795,17 +836,26 @@ class PipelineOrchestrator:
funcs = search_result["relevant_functions"]
func_str = ', '.join(funcs[:3]) if funcs else "(文件级修改)"
self._emit(on_status, "locator_done", f"文件:{', '.join(files[:3])} | 函数:{func_str}")
# DetailedLogger: locator 节点
self._detailed.log_node("locator",
{"user_input": ctx.user_input[:200], "gap_type": str(ctx.gap.gap_type.value) if ctx.gap and hasattr(ctx.gap, 'gap_type') else ""},
{"files": files, "functions": funcs, "method": search_result.get("method", ""), "upstream_constraints": search_result.get("upstream_constraints", "")[:500]})
elif step == "generator":
self._emit(on_status, "generator", "生成patch...")
result = self.generator.run(ctx)
if not result:
self._emit(on_status, "generator_fail", "生成失败")
self._detailed.log_node("generator", {"files": (ctx.locator_output or {}).get("relevant_files", [])}, {"result": None}, detail="生成失败")
return False
n_patches = len(result.get("patches", []))
self._emit(on_status, "generator_done", f"生成{n_patches}个patch")
# 用新patch更新manifest跨文件追踪
self._manifest.update(result.get("patches", []))
# DetailedLogger: generator 节点
self._detailed.log_node("generator",
{"files": (ctx.locator_output or {}).get("relevant_files", []), "functions": (ctx.locator_output or {}).get("relevant_functions", [])},
{"patch_count": n_patches, "files_modified": [p.get("file", "") for p in result.get("patches", [])], "explanation": result.get("explanation", "")})
elif step == "verifier":
self._emit(on_status, "verifier", "验证中...")
@@ -826,15 +876,22 @@ class PipelineOrchestrator:
"error_message": detail[:200],
"failed_tests": [],
}
self._detailed.log_node("verifier", {"patch_count": len((ctx.generator_output or {}).get("patches", []))}, {"passed": False, "error_type": "contract_violation", "detail": detail[:300]})
return False
result = self.verifier.run(ctx)
if not result or not result.get("passed"):
detail = result.get("error_detail", "unknown") if result else "no result"
self._emit(on_status, "verifier_fail", f"验证失败:{detail[:80]}")
self._detailed.log_node("verifier",
{"patch_count": len((ctx.generator_output or {}).get("patches", []))},
{"passed": False, "error_type": result.get("error_type", "") if result else "", "error_detail": detail[:1000], "tests_passed": result.get("tests_passed", 0) if result else 0, "tests_total": result.get("tests_total", 0) if result else 0})
return False
tp = result.get("tests_passed", 0)
tt = result.get("tests_total", 0)
self._emit(on_status, "verifier_done", f"语法OK | 测试:{tp}/{tt}")
self._detailed.log_node("verifier",
{"patch_count": len((ctx.generator_output or {}).get("patches", []))},
{"passed": True, "tests_passed": tp, "tests_total": tt})
elif step == "office":
self._emit(on_status, "office", "生成Office文档...")

View File

@@ -217,11 +217,12 @@ class GeneratorExpert:
if self._is_test_generation_task(ctx):
return self._run_test_generation(ctx, files)
# ── whole_file scope: 存根实现LLM返回完整文件 ──
# ── whole_file scope: 存根实现 ──
# Sub-task decomposition: 逐函数独立实现每个函数独立context
if ctx.gap and hasattr(ctx.gap, 'gap_type'):
from kaiwu.core.gap_detector import GapType
if ctx.gap.gap_type in (GapType.NOT_IMPLEMENTED, GapType.STUB_RETURNS_NONE):
return self._run_whole_file(ctx, files)
return self._run_stub_decomposed(ctx, files, funcs)
# For each file+function pair, extract original and generate modified
# Deduplicate: only patch each (file, function) once
@@ -367,7 +368,8 @@ class GeneratorExpert:
return any(kw in lower for kw in _WEB_KEYWORDS)
def _generate_modified(self, ctx: TaskContext, fpath: str, original: str, task_desc: str) -> Optional[str]:
"""Ask LLM to generate modified code. Hashline primary, full-function fallback."""
"""Ask LLM to generate modified code. Hashline primary, full-function fallback.
Bounded context: only pass current function + relevant failing tests."""
search_ctx = ""
if ctx.search_results:
search_ctx = f"参考资料:\n{ctx.search_results}"
@@ -392,38 +394,55 @@ class GeneratorExpert:
if ctx.doc_context:
prompt += f"\n\n## 相关文档参考\n{ctx.doc_context[:800]}"
# 注入初始测试失败信息让LLM第一次就看到具体报错
# ── Bounded context: 只注入与当前函数相关的 failing tests ──
# 从 structured_failures 中筛选与当前函数/文件相关的条目
initial_failure = getattr(ctx, 'initial_test_failure', '')
# 结构化失败信息精确告诉LLM每个测试为什么失败— 首次和重试都注入
structured = (ctx.verifier_output or {}).get("structured_failures", [])
if not structured and initial_failure:
from kaiwu.core.test_parser import parse_test_failures
structured = parse_test_failures(initial_failure)
if structured:
lines = ["## 必须修复的测试失败(精确信息)"]
for f in structured[:8]:
name = f.get("test_name", "?")
expected = f.get("expected", "")
actual = f.get("actual", "")
snippet = f.get("snippet", "")
err_type = f.get("error_type", "")
if expected and actual:
lines.append(f"- {name}: 期望 {expected},实际 {actual}")
elif err_type and snippet:
lines.append(f"- {name}: {err_type}: {snippet[:120]}")
elif snippet:
lines.append(f"- {name}: {snippet[:120]}")
else:
lines.append(f"- {name}")
prompt += "\n\n" + "\n".join(lines)
elif initial_failure and ctx.retry_count == 0:
# fallback: 没解析出结构化信息时给raw output
prompt += f"\n\n## 当前测试失败\n{initial_failure[:800]}"
# Inject retry_hint if available
if structured:
# 提取当前函数名(从 original 的第一行 def/class 获取)
current_func = self._extract_func_name_from_code(original)
# 筛选与当前函数/文件相关的失败
relevant_failures = self._filter_relevant_failures(structured, current_func, fpath)
if relevant_failures:
lines = ["## 必须修复的测试失败(精确信息)"]
for f in relevant_failures[:5]:
name = f.get("test_name", "?")
expected = f.get("expected", "")
actual = f.get("actual", "")
snippet = f.get("snippet", "")
err_type = f.get("error_type", "")
if expected and actual:
lines.append(f"- {name}: 期望 {expected},实际 {actual}")
elif err_type and snippet:
lines.append(f"- {name}: {err_type}: {snippet[:120]}")
elif snippet:
lines.append(f"- {name}: {snippet[:120]}")
else:
lines.append(f"- {name}")
prompt += "\n\n" + "\n".join(lines)
elif not relevant_failures and structured:
# 没有精确匹配时给前3条作为上下文但不是全部
lines = ["## 相关测试失败"]
for f in structured[:3]:
name = f.get("test_name", "?")
snippet = f.get("snippet", "")
lines.append(f"- {name}: {snippet[:100]}" if snippet else f"- {name}")
prompt += "\n\n" + "\n".join(lines)
elif initial_failure and ctx.retry_count == 0:
# fallback: 没解析出结构化信息时给精简的 raw output
prompt += f"\n\n## 当前测试失败\n{initial_failure[:500]}"
# Inject retry_hint: 只传一句话总结,不传完整历史
if ctx.retry_hint:
prompt += f"\n\n## 重试提示\n{ctx.retry_hint}"
# 截断 retry_hint,避免注入过多历史
hint = ctx.retry_hint
if len(hint) > 300:
hint = hint[:300] + "..."
prompt += f"\n\n## 重试提示\n{hint}"
system = self._build_system(ctx)
@@ -449,6 +468,41 @@ class GeneratorExpert:
logger.warning("Generator: all candidates identical to original or empty")
return None
@staticmethod
def _extract_func_name_from_code(code: str) -> str:
"""从代码片段中提取函数/类名。"""
for line in code.split("\n"):
stripped = line.strip()
if stripped.startswith("def "):
# def func_name(...)
name = stripped[4:].split("(")[0].strip()
return name
if stripped.startswith("class "):
name = stripped[6:].split("(")[0].split(":")[0].strip()
return name
return ""
@staticmethod
def _filter_relevant_failures(failures: list, func_name: str, file_path: str) -> list:
"""筛选与当前函数/文件相关的测试失败。"""
if not func_name:
return failures[:5] # 无法确定函数名时返回前5条
relevant = []
fname_lower = func_name.lower()
fpath_base = file_path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] if file_path else ""
for f in failures:
test_name = f.get("test_name", "").lower()
snippet = f.get("snippet", "").lower()
# 测试名包含函数名,或 snippet 中提到函数名/文件名
if (fname_lower in test_name or
fname_lower in snippet or
(fpath_base and fpath_base.lower() in snippet)):
relevant.append(f)
return relevant
def _try_hashline(self, ctx: TaskContext, fpath: str, original: str,
task_desc: str, search_ctx: str) -> Optional[str]:
"""Try Hashline anchor-based editing. Returns modified code or None."""
@@ -853,11 +907,22 @@ class GeneratorExpert:
output_tokens=output_tokens,
engineering_actions={},
)
# DetailedLogger: 完整不截断记录
detailed = getattr(ctx, '_detailed_logger', None)
if detailed and detailed.enabled:
detailed.log_llm(
caller=caller,
prompt=prompt,
system=system,
raw_output=raw_output or '',
tokens={"input": len(prompt) // 4 + len(system) // 4, "output": len(raw_output or '') // 4},
)
except Exception:
pass # 非阻塞
def _run_whole_file(self, ctx: TaskContext, files: list[str]) -> Optional[dict]:
"""whole_file scopeLLM返回完整文件内容直接write_file不走apply_patch。"""
"""whole_file scopeLLM返回完整文件内容直接write_file不走apply_patch。
Fallback for _run_stub_decomposed when decomposition fails."""
patches = []
explanation_parts = []
@@ -917,3 +982,178 @@ class GeneratorExpert:
}
ctx.generator_output = result
return result
def _run_stub_decomposed(self, ctx: TaskContext, files: list[str], funcs: list[str]) -> Optional[dict]:
"""
Sub-task decomposition for stub tasks.
Instead of asking LLM to implement all pass functions at once,
decompose into per-function subtasks with independent bounded context.
Each function gets its own LLM call with only its code + relevant tests.
Falls back to _run_whole_file if decomposition fails.
"""
patches = []
explanation_parts = []
for fpath in files[:3]:
if "test" in fpath.lower():
continue
# Read file content
if self.tools:
content = self.tools.read_file(fpath)
else:
content = ctx.relevant_code_snippets.get(fpath, "")
if not content or content.startswith("[ERROR]"):
continue
# Find all stub functions (pass/... body)
stub_funcs = self._find_stub_functions(content)
if not stub_funcs:
continue
# If only 1-2 stubs, or file is small, use whole_file (simpler)
if len(stub_funcs) <= 2 or len(content.split("\n")) < 50:
result = self._run_whole_file(ctx, [fpath])
if result:
return result
continue
# ── Per-function decomposition ──
initial_failure = getattr(ctx, 'initial_test_failure', '')
structured_failures = (ctx.verifier_output or {}).get("structured_failures", [])
if not structured_failures and initial_failure:
from kaiwu.core.test_parser import parse_test_failures
structured_failures = parse_test_failures(initial_failure)
file_patches = []
for func_name, func_code in stub_funcs:
# Build bounded context for this single function
relevant_tests = self._filter_relevant_failures(
structured_failures, func_name, fpath
)
test_info = ""
if relevant_tests:
lines = []
for f in relevant_tests[:3]:
name = f.get("test_name", "?")
expected = f.get("expected", "")
actual = f.get("actual", "")
snippet = f.get("snippet", "")
if expected and actual:
lines.append(f"- {name}: 期望 {expected},实际 {actual}")
elif snippet:
lines.append(f"- {name}: {snippet[:120]}")
else:
lines.append(f"- {name}")
test_info = "\n\n## 相关测试\n" + "\n".join(lines)
prompt = (
f"任务:{ctx.user_input[:200]}\n\n"
f"实现以下函数(来自 {fpath}\n"
f"```\n{func_code}\n```\n\n"
f"只输出实现后的完整函数代码从def开始\n"
f"保持原始缩进和签名不变只替换pass为实现。\n"
f"纯代码无markdown无解释。"
f"{test_info}"
)
if ctx.search_results:
prompt += f"\n\n参考资料:\n{ctx.search_results[:500]}"
system = self._build_system(ctx)
raw = self.llm.generate(prompt=prompt, system=system, max_tokens=2048, temperature=0.0)
self._log_llm_call(ctx, f"generator_stub_{func_name}", prompt, system, raw)
modified = self._clean_code_output(raw)
if modified and modified.strip() != func_code.strip():
modified = modified.strip("\n")
modified = self._align_indentation(func_code, modified)
file_patches.append({
"file": fpath,
"original": func_code,
"modified": modified,
})
explanation_parts.append(f"{fpath}:{func_name}")
if file_patches:
patches.extend(file_patches)
if not patches:
logger.debug("Stub decomposition produced no patches, falling back to whole_file")
return self._run_whole_file(ctx, files)
result = {
"patches": patches,
"explanation": f"Stub impl: {', '.join(explanation_parts)}",
}
ctx.generator_output = result
return result
@staticmethod
def _find_stub_functions(content: str) -> list[tuple[str, str]]:
"""
Find all stub functions (body is just 'pass', '...', or 'return None') in file content.
Returns list of (func_name, full_function_code) tuples.
"""
lines = content.split("\n")
stubs = []
i = 0
while i < len(lines):
line = lines[i]
stripped = line.lstrip()
if stripped.startswith("def "):
func_name = stripped[4:].split("(")[0].strip()
indent_level = len(line) - len(stripped)
start_idx = i
# Find end of function
end_idx = i + 1
while end_idx < len(lines):
l = lines[end_idx]
if l.strip() == "":
end_idx += 1
continue
current_indent = len(l) - len(l.lstrip())
if current_indent <= indent_level and l.strip():
break
end_idx += 1
func_code = "\n".join(lines[start_idx:end_idx]).rstrip()
# Check if body is stub (skip docstrings and comments)
in_docstring = False
clean_body = []
for bl in lines[start_idx + 1:end_idx]:
s = bl.strip()
if not s:
continue
if s.startswith('"""') or s.startswith("'''"):
if s.count('"""') >= 2 or s.count("'''") >= 2:
continue # single-line docstring
in_docstring = not in_docstring
continue
if in_docstring:
continue
if s.startswith("#"):
continue
clean_body.append(s)
is_stub = (
len(clean_body) == 0 or
(len(clean_body) == 1 and clean_body[0] in (
"pass", "...", "return None",
"raise NotImplementedError",
"raise NotImplementedError()",
))
)
if is_stub:
stubs.append((func_name, func_code))
i = end_idx
else:
i += 1
return stubs

View File

@@ -73,6 +73,8 @@ class LLMBackend:
self._total_output_tokens: int = 0
self._call_count: int = 0
self._token_budget: int = 0 # 0 = unlimited
# DetailedLogger 回调:每次 LLM 调用后触发(由 orchestrator 设置)
self._on_llm_call = None # Callable(caller, messages, raw_output, tokens, elapsed_ms)
# Prefer native llama.cpp if model_path provided and library available
if model_path and HAS_LLAMA_CPP:
@@ -122,7 +124,14 @@ class LLMBackend:
# If it's not localhost at all, assume OpenAI-compatible
if "localhost" not in url_lower and "127.0.0.1" not in url_lower:
return True
return False
# For localhost on non-standard ports, probe /api/tags to detect Ollama
try:
resp = httpx.get(f"{url.rstrip('/')}/api/tags", timeout=3)
if resp.status_code == 200 and "models" in resp.text:
return False # It's Ollama
except Exception:
pass
return True # Not Ollama, assume OpenAI-compatible
@property
def token_usage(self) -> dict:
@@ -211,6 +220,20 @@ class LLMBackend:
self._last_elapsed = _time.perf_counter() - t0
if self._tps_estimator:
self._tps_estimator.record(result, self._last_elapsed)
# DetailedLogger 回调
if self._on_llm_call:
try:
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
self._on_llm_call(
messages=messages,
raw_output=result,
elapsed_ms=self._last_elapsed * 1000,
)
except Exception:
pass
return result
def _generate_native(
@@ -281,6 +304,16 @@ class LLMBackend:
self._last_elapsed = _time.perf_counter() - t0
if self._tps_estimator:
self._tps_estimator.record(result, self._last_elapsed)
# DetailedLogger 回调
if self._on_llm_call:
try:
self._on_llm_call(
messages=messages,
raw_output=result,
elapsed_ms=self._last_elapsed * 1000,
)
except Exception:
pass
return result
def _chat_ollama(

0
logs/.gitkeep Normal file
View File

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "kwcode"
version = "1.6.2"
version = "1.7.0"
description = "KwCode - Local-model coding agent with MoE expert pipeline"
requires-python = ">=3.10"
readme = "README.md"