Files
kwcode/kaiwu/core/gate.py
dashitongzhi b2e9d33164 feat: add multimodal vision expert for image analysis and code generation
- VisionExpert class: image analysis + code generation from images
- Anthropic Messages API integration (mimo-v2-omni model)
- Gate classifier: new 'vision' expert type
- Orchestrator: vision pipeline with image_paths support
- CLI: /paste (clipboard) and /image (file) commands
- Optional deps: pip install kwcode[multimodal] (Pillow + pyperclip)
- Architecture diagram in docs/
2026-04-30 11:46:49 +08:00

217 lines
10 KiB
Python
Raw 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.
"""
Gate: single LLM call, structured JSON output, routes to expert pipeline.
RED-1: Must output structured JSON, no string parsing.
v0.4.3: LLM通用分类为主专家知识叠加不替代
"""
import json
import logging
from typing import Optional, TYPE_CHECKING
from kaiwu.llm.llama_backend import LLMBackend
from kaiwu.core.orchestrator import EXPERT_SEQUENCES
if TYPE_CHECKING:
from kaiwu.registry.expert_registry import ExpertRegistry
logger = logging.getLogger(__name__)
GATE_SYSTEM = "你是任务分类器。只返回JSON不要有其他内容。"
GATE_PROMPT = """分析用户输入返回分类JSON。
expert_type选项
- locator_repair修复bug、修改已有代码用户明确提到已有文件路径如src/xxx.py
- codegen从零创建全新文件或全新项目"写一个""生成""创建"开头的代码任务)
- refactor重构、优化、整理已有代码结构用户明确提到已有文件+重构/拆分/提取)
- doc写注释、文档、README仅限代码相关文档用户明确提到已有文件+docstring/注释)
- office仅限生成Excel(.xlsx)/Word(.docx)/PPT(.pptx)办公文档,不包括代码文件
- chat问候、闲聊、非编码问题、询问天气、询问知识
- vision图片分析、图片代码生成、UI截图分析、设计图实现用户上传了图片或提到图片相关内容
difficulty选项easy | hard
hard条件满足任意一条
- 明确涉及多个不同类型的操作(如"找bug AND 生成网页 AND 搜索数据"
- 涉及文件数估计 > 3个
- 需要先获取外部信息才能生成代码
needs_search选项true | false
true条件任务需要实时数据天气/股价/当前时间/最新版本或需要查询API文档/库文档
subtask_hint仅difficulty=hard时填写逗号分隔的子任务简述。不建议拆分时留空字符串。
关键区分规则:
- office仅用于生成.pptx/.xlsx/.docx办公文档文件HTML/网页展示不算office
- "生成PPT"/"做个幻灯片"/"演示文稿"/"汇报PPT" → office
- "写个展示PPT内容的HTML页面"/"用HTML做演示" → codegen不是office
- 代码文件(.py/.js/.html/.css/.json/.go/.ts/.sh)一律不选office
- "写一个xxx.py/html/js/css/json/go/ts/sh" → codegen不是office
- "修复src/xxx.py" → locator_repair
- "重构src/xxx.py" → refactor
- 不确定时优先选codegen或locator_repair不要选office
- 图片相关任务(上传图片、分析截图、根据设计图生成代码) → vision
示例:
- "你好"{{"expert_type":"chat","task_summary":"问候","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "帮我修复登录bug"{{"expert_type":"locator_repair","task_summary":"修复登录","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "搜索南京天气做网页"{{"expert_type":"codegen","task_summary":"天气网页","difficulty":"hard","needs_search":true,"subtask_hint":"搜索天气数据,生成展示网页"}}
- "重构认证模块"{{"expert_type":"refactor","task_summary":"重构认证","difficulty":"hard","needs_search":false,"subtask_hint":""}}
- "生成一个Excel报表"{{"expert_type":"office","task_summary":"Excel报表","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "根据内容生成项目介绍PPT"{{"expert_type":"office","task_summary":"项目PPT","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "做个幻灯片汇报"{{"expert_type":"office","task_summary":"汇报PPT","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "分析这张截图"{{"expert_type":"vision","task_summary":"图片分析","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "根据UI设计图生成HTML代码"{{"expert_type":"vision","task_summary":"UI代码生成","difficulty":"easy","needs_search":false,"subtask_hint":""}}
格式:{{"expert_type":"...","task_summary":"10字内","difficulty":"...","needs_search":false,"subtask_hint":""}}
用户输入:{user_input}"""
# JSON grammar constraint for llama.cpp
GATE_GRAMMAR = r'''
root ::= "{" ws expert-type "," ws task-summary "," ws difficulty "}" ws
expert-type ::= "\"expert_type\"" ws ":" ws "\"" expert-val "\""
expert-val ::= "locator_repair" | "codegen" | "refactor" | "doc" | "office" | "chat" | "vision"
task-summary ::= "\"task_summary\"" ws ":" ws string
difficulty ::= "\"difficulty\"" ws ":" ws ("\"easy\"" | "\"hard\"")
string ::= "\"" [^"]* "\""
ws ::= [ \t\n]*
'''
VALID_EXPERT_TYPES = {"locator_repair", "codegen", "refactor", "doc", "office", "chat", "vision"}
VALID_DIFFICULTIES = {"easy", "hard"}
class Gate:
"""Task classifier. Expert registry first, LLM fallback."""
# Map expert pipeline to existing expert_type for orchestrator compatibility
_PIPELINE_TO_TYPE = {
("locator", "generator", "verifier"): "locator_repair",
("generator", "verifier"): "codegen",
("locator", "generator"): "doc",
("generator",): "codegen",
("office",): "office",
("chat",): "chat",
}
def __init__(self, llm: LLMBackend, use_grammar: bool = False, registry: "ExpertRegistry | None" = None):
self.llm = llm
self.use_grammar = use_grammar
self.registry = registry
def classify(self, user_input: str, memory_context: str = "") -> dict:
"""
Classify user input: LLM通用分类为主专家知识为辅叠加模式
1. LLM通用分类 → expert_type (codegen/locator_repair/refactor/doc/chat)
2. 专家关键词匹配 → 叠加领域知识(system_prompt),不替代通用分类
"""
# ── Step 1: LLM通用分类始终执行作为主分类结果──
prompt = GATE_PROMPT.format(user_input=user_input)
if memory_context:
prompt = f"项目记忆:\n{memory_context}\n\n{prompt}"
grammar = GATE_GRAMMAR if self.use_grammar else None
raw = self.llm.generate(
prompt=prompt,
system=GATE_SYSTEM,
max_tokens=150,
temperature=0.01,
stop=["\n\n"],
grammar_str=grammar,
)
result = self._parse(raw, user_input)
result = self._postprocess(result, user_input)
# ── Step 2: 专家关键词匹配(叠加模式,不替代通用分类)──
result["expert_name"] = None
result["route_type"] = "general"
if self.registry:
match = self.registry.match(user_input)
if match:
expert = match["expert"]
expert_pipeline = tuple(expert["pipeline"])
general_pipeline = tuple(
EXPERT_SEQUENCES.get(result["expert_type"], ["generator", "verifier"])
)
# 专家pipeline和通用分类一致 → 用专家加载system_prompt
# 不一致 → 以通用分类为准专家system_prompt作为附加知识注入
result["expert_name"] = match["name"]
result["confidence"] = match["confidence"]
# Progressive disclosure: use instructions (SKILL.md) or system_prompt (YAML)
result["system_prompt"] = expert.get("instructions") or expert.get("system_prompt", "")
if expert_pipeline == general_pipeline:
# 完全一致:走专家路由
result["route_type"] = "expert_registry"
result["pipeline"] = list(expert_pipeline)
else:
# 不一致:通用分类为主,专家知识为辅
result["route_type"] = "general_with_expert"
# 不覆盖pipeline让orchestrator用通用的EXPERT_SEQUENCES
return result
@staticmethod
def _postprocess(result: dict, user_input: str) -> dict:
"""最后一道防线仅纠正office误分类。不替代模型分类能力。"""
et = result.get("expert_type", "chat")
lower = user_input.lower()
# office仅限Excel/Word/PPT办公文档代码任务不应走office
if et == "office":
# 只有明确提到办公文档格式才保留office
_OFFICE_FORMATS = (
".xlsx", ".docx", ".pptx",
"excel", "word文档", "ppt", "幻灯片", "演示文稿", "汇报",
)
if not any(fmt in lower for fmt in _OFFICE_FORMATS):
result["expert_type"] = "chat" # 降级到chat
return result
def _parse(self, raw: str, user_input: str) -> dict:
"""Parse and validate Gate output. Fallback on any failure."""
# Try to extract JSON from response (model might wrap it in text)
json_str = self._extract_json(raw)
try:
result = json.loads(json_str)
# Validate required fields
et = result.get("expert_type", "")
diff = result.get("difficulty", "")
summary = result.get("task_summary", "")
if et not in VALID_EXPERT_TYPES:
raise ValueError(f"Invalid expert_type: {et}")
if diff not in VALID_DIFFICULTIES:
raise ValueError(f"Invalid difficulty: {diff}")
return {
"expert_type": et,
"task_summary": summary[:20] if summary else user_input[:10],
"difficulty": diff,
"needs_search": bool(result.get("needs_search", False)),
"subtask_hint": str(result.get("subtask_hint", "")),
}
except (json.JSONDecodeError, ValueError, KeyError, AttributeError, TypeError) as e:
logger.warning("Gate parse failed (raw=%r): %s", raw[:200], e)
return {
"expert_type": "chat",
"task_summary": user_input[:10],
"difficulty": "easy",
"_parse_error": str(e),
}
@staticmethod
def _extract_json(text: str) -> str:
"""Extract first JSON object from text."""
# Find first { and last }
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1 and end > start:
return text[start:end + 1]
return text.strip()