weijie-codeagent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeagent/__init__.py +2 -0
- codeagent/config.py +79 -0
- codeagent/graph/__init__.py +0 -0
- codeagent/graph/builder.py +77 -0
- codeagent/graph/nodes/__init__.py +0 -0
- codeagent/graph/nodes/code_agent.py +152 -0
- codeagent/graph/nodes/context_loader.py +45 -0
- codeagent/graph/nodes/context_qa.py +72 -0
- codeagent/graph/nodes/intent_router.py +66 -0
- codeagent/graph/nodes/memory_writer.py +71 -0
- codeagent/graph/nodes/reflection.py +82 -0
- codeagent/graph/nodes/task_decomposer.py +177 -0
- codeagent/graph/state.py +36 -0
- codeagent/main.py +365 -0
- codeagent/memory/__init__.py +0 -0
- codeagent/memory/compressor.py +74 -0
- codeagent/memory/long_term.py +122 -0
- codeagent/memory/short_term.py +28 -0
- codeagent/memory/vector_store.py +164 -0
- codeagent/tools/__init__.py +33 -0
- codeagent/tools/code_tools.py +57 -0
- codeagent/tools/edit_tools.py +231 -0
- codeagent/tools/file_tools.py +75 -0
- codeagent/tools/mcp_tools.py +129 -0
- codeagent/tools/search_tools.py +163 -0
- codeagent/tools/shell_tools.py +107 -0
- codeagent/tools/task_tools.py +106 -0
- codeagent/ui/__init__.py +0 -0
- codeagent/ui/renderer.py +191 -0
- weijie_codeagent-0.1.0.dist-info/METADATA +243 -0
- weijie_codeagent-0.1.0.dist-info/RECORD +33 -0
- weijie_codeagent-0.1.0.dist-info/WHEEL +4 -0
- weijie_codeagent-0.1.0.dist-info/entry_points.txt +2 -0
codeagent/__init__.py
ADDED
codeagent/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config.py —— pydantic-settings 配置管理(Phase 4 升级版)
|
|
3
|
+
|
|
4
|
+
env 文件加载优先级(后者覆盖前者):
|
|
5
|
+
1. ~/.codeagent/.env (用户全局配置)
|
|
6
|
+
2. .env (项目本地配置,最高优先级)
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
from langchain_openai import ChatOpenAI
|
|
12
|
+
|
|
13
|
+
# 用户全局配置路径
|
|
14
|
+
_USER_ENV = Path.home() / ".codeagent" / ".env"
|
|
15
|
+
_LOCAL_ENV = Path(".env")
|
|
16
|
+
|
|
17
|
+
# 收集存在的 env 文件(按优先级从低到高)
|
|
18
|
+
_ENV_FILES: list[str] = []
|
|
19
|
+
if _USER_ENV.exists():
|
|
20
|
+
_ENV_FILES.append(str(_USER_ENV))
|
|
21
|
+
if _LOCAL_ENV.exists():
|
|
22
|
+
_ENV_FILES.append(str(_LOCAL_ENV))
|
|
23
|
+
if not _ENV_FILES:
|
|
24
|
+
_ENV_FILES = [".env"] # 回退,让 pydantic-settings 自行处理
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Settings(BaseSettings):
|
|
28
|
+
PROJECT_NAME: str = "CodeAgent"
|
|
29
|
+
|
|
30
|
+
# LLM(火山引擎 Ark,OpenAI 兼容)
|
|
31
|
+
LLM_API_KEY: str = os.getenv("LLM_API_KEY", "")
|
|
32
|
+
LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "https://ark.cn-beijing.volces.com/api/coding/v3")
|
|
33
|
+
LLM_MODEL: str = os.getenv("LLM_MODEL", "deepseek-v3.2")
|
|
34
|
+
|
|
35
|
+
# Embedding(Phase 2 启用)
|
|
36
|
+
EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "doubao-embedding-vision-250615")
|
|
37
|
+
EMBEDDING_BASE_URL: str = os.getenv("EMBEDDING_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
|
38
|
+
EMBEDDING_API_KEY: str = os.getenv("EMBEDDING_API_KEY", "")
|
|
39
|
+
|
|
40
|
+
# 存储
|
|
41
|
+
VECTOR_BACKEND: str = os.getenv("VECTOR_BACKEND", "qdrant")
|
|
42
|
+
DB_URL: str = os.getenv("DB_URL", "sqlite:///./codeagent.db")
|
|
43
|
+
|
|
44
|
+
# 记忆窗口
|
|
45
|
+
WINDOW_SIZE: int = int(os.getenv("WINDOW_SIZE", "10"))
|
|
46
|
+
|
|
47
|
+
# Qdrant
|
|
48
|
+
QDRANT_URL: str = os.getenv("QDRANT_URL", "http://localhost:6333")
|
|
49
|
+
|
|
50
|
+
# Shell 沙箱
|
|
51
|
+
WORKSPACE_DIR: str = os.getenv("WORKSPACE_DIR", ".")
|
|
52
|
+
SHELL_SANDBOX: str = os.getenv("SHELL_SANDBOX", "subprocess")
|
|
53
|
+
|
|
54
|
+
# 重试
|
|
55
|
+
LLM_MAX_RETRIES: int = int(os.getenv("LLM_MAX_RETRIES", "3"))
|
|
56
|
+
|
|
57
|
+
model_config = SettingsConfigDict(
|
|
58
|
+
env_file=_ENV_FILES, # type: ignore[arg-type]
|
|
59
|
+
extra="ignore",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def embedding_api_key(self) -> str:
|
|
64
|
+
return self.EMBEDDING_API_KEY or self.LLM_API_KEY
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
settings = Settings()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_llm(streaming: bool = False) -> ChatOpenAI:
|
|
71
|
+
return ChatOpenAI(
|
|
72
|
+
api_key=settings.LLM_API_KEY,
|
|
73
|
+
base_url=settings.LLM_BASE_URL,
|
|
74
|
+
model=settings.LLM_MODEL,
|
|
75
|
+
temperature=0.3,
|
|
76
|
+
max_tokens=4096,
|
|
77
|
+
streaming=streaming,
|
|
78
|
+
max_retries=settings.LLM_MAX_RETRIES,
|
|
79
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
builder.py —— LangGraph 图构建(Phase 3 版)
|
|
3
|
+
|
|
4
|
+
图结构:
|
|
5
|
+
START
|
|
6
|
+
→ context_loader
|
|
7
|
+
→ intent_router
|
|
8
|
+
→(条件) code_agent | task_decomposer | context_qa
|
|
9
|
+
→ (code/decompose 后) reflection
|
|
10
|
+
→(条件) code_agent | task_decomposer | memory_writer ← revision 或直接结束
|
|
11
|
+
→ (context_qa 后) memory_writer
|
|
12
|
+
→ END
|
|
13
|
+
"""
|
|
14
|
+
from langgraph.graph import StateGraph, START, END
|
|
15
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
16
|
+
|
|
17
|
+
from codeagent.graph.state import AgentState
|
|
18
|
+
from codeagent.graph.nodes.context_loader import context_loader_node
|
|
19
|
+
from codeagent.graph.nodes.intent_router import intent_router_node, route_by_intent
|
|
20
|
+
from codeagent.graph.nodes.code_agent import code_agent_node
|
|
21
|
+
from codeagent.graph.nodes.task_decomposer import task_decomposer_node
|
|
22
|
+
from codeagent.graph.nodes.context_qa import context_qa_node
|
|
23
|
+
from codeagent.graph.nodes.reflection import reflection_node, route_after_reflection
|
|
24
|
+
from codeagent.graph.nodes.memory_writer import memory_writer_node
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_graph():
|
|
28
|
+
builder = StateGraph(AgentState)
|
|
29
|
+
|
|
30
|
+
# ── 注册节点 ──────────────────────────────────
|
|
31
|
+
builder.add_node("context_loader", context_loader_node)
|
|
32
|
+
builder.add_node("intent_router", intent_router_node)
|
|
33
|
+
builder.add_node("code_agent", code_agent_node)
|
|
34
|
+
builder.add_node("task_decomposer", task_decomposer_node)
|
|
35
|
+
builder.add_node("context_qa", context_qa_node)
|
|
36
|
+
builder.add_node("reflection", reflection_node)
|
|
37
|
+
builder.add_node("memory_writer", memory_writer_node)
|
|
38
|
+
|
|
39
|
+
# ── 固定边 ────────────────────────────────────
|
|
40
|
+
builder.add_edge(START, "context_loader")
|
|
41
|
+
builder.add_edge("context_loader", "intent_router")
|
|
42
|
+
|
|
43
|
+
# code / decompose → reflection(Reflection 节点内部决定是否真正执行)
|
|
44
|
+
builder.add_edge("code_agent", "reflection")
|
|
45
|
+
builder.add_edge("task_decomposer", "reflection")
|
|
46
|
+
|
|
47
|
+
# context_qa 直接跳过 reflection
|
|
48
|
+
builder.add_edge("context_qa", "memory_writer")
|
|
49
|
+
builder.add_edge("memory_writer", END)
|
|
50
|
+
|
|
51
|
+
# ── 意图路由:intent_router → 各 Agent ─────────
|
|
52
|
+
builder.add_conditional_edges(
|
|
53
|
+
"intent_router",
|
|
54
|
+
route_by_intent,
|
|
55
|
+
{
|
|
56
|
+
"code_agent": "code_agent",
|
|
57
|
+
"task_decomposer": "task_decomposer",
|
|
58
|
+
"context_qa": "context_qa",
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# ── Reflection 后路由:通过/重试/结束 ───────────
|
|
63
|
+
builder.add_conditional_edges(
|
|
64
|
+
"reflection",
|
|
65
|
+
route_after_reflection,
|
|
66
|
+
{
|
|
67
|
+
"code_agent": "code_agent",
|
|
68
|
+
"task_decomposer": "task_decomposer",
|
|
69
|
+
"memory_writer": "memory_writer",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
checkpointer = MemorySaver()
|
|
74
|
+
return builder.compile(checkpointer=checkpointer)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
graph = build_graph()
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code_agent —— 代码生成节点(Phase 4 升级版)
|
|
3
|
+
|
|
4
|
+
变更:
|
|
5
|
+
- 集成工具调用(bind_tools + ToolMessage 循环,最多 3 轮)
|
|
6
|
+
- revision 模式保留
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
10
|
+
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
|
11
|
+
from codeagent.graph.state import AgentState
|
|
12
|
+
from codeagent.config import get_llm
|
|
13
|
+
from codeagent.tools import all_tools
|
|
14
|
+
|
|
15
|
+
_TOOL_MAP = {t.name: t for t in all_tools}
|
|
16
|
+
_MAX_TOOL_ROUNDS = 3
|
|
17
|
+
|
|
18
|
+
# ── 正常生成 ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
_CODE_SYSTEM = """\
|
|
21
|
+
你是一个专业的编程助手。根据用户请求生成高质量的代码。
|
|
22
|
+
|
|
23
|
+
你可以调用以下工具来辅助完成任务:
|
|
24
|
+
文件读写:
|
|
25
|
+
- read_file(path): 读取工作目录内的文件
|
|
26
|
+
- write_file(path, content): 写入文件(整体覆盖)
|
|
27
|
+
- list_dir(path): 列出目录内容
|
|
28
|
+
精确编辑(优先用这些替代 write_file):
|
|
29
|
+
- apply_diff(path, diff_content): 应用 unified diff 补丁,精确修改文件局部
|
|
30
|
+
- search_and_replace(path, old_text, new_text, use_regex): 搜索并替换文本
|
|
31
|
+
- insert_content(path, insert_after_line, content): 在指定行号后插入内容
|
|
32
|
+
搜索:
|
|
33
|
+
- search_files(query, file_pattern, case_sensitive): 多策略搜索(git grep/rg/Python)
|
|
34
|
+
- search_code(pattern, file_glob): 正则搜索代码文件
|
|
35
|
+
执行与追踪:
|
|
36
|
+
- run_shell(command): 在沙箱内执行命令(危险命令会被拦截)
|
|
37
|
+
- update_todos(action, content, todo_id): 管理任务列表(add/list/complete/remove/clear)
|
|
38
|
+
- use_mcp_tool(server_name, tool_name, arguments): 调用外部 MCP 工具服务器
|
|
39
|
+
|
|
40
|
+
要求:
|
|
41
|
+
1. 代码完整、可直接运行
|
|
42
|
+
2. 使用正确的代码块标记(```python / ```javascript 等)
|
|
43
|
+
3. 代码前后用一两句话说明思路或注意事项
|
|
44
|
+
4. 需要时先调用工具了解项目结构,再生成代码
|
|
45
|
+
|
|
46
|
+
── 历史摘要 ──────────────────────
|
|
47
|
+
{long_term_summary}
|
|
48
|
+
|
|
49
|
+
── 相关记忆 ──────────────────────
|
|
50
|
+
{retrieved_memory}
|
|
51
|
+
|
|
52
|
+
── 近期对话 ──────────────────────
|
|
53
|
+
{context}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# ── Revision 模式 ─────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
_REVISION_SYSTEM = """\
|
|
59
|
+
你是一个专业的编程助手。你的上一次代码输出收到了审查反馈,请根据反馈进行修正。
|
|
60
|
+
|
|
61
|
+
审查反馈(请重点修正这些问题):
|
|
62
|
+
{reflection_feedback}
|
|
63
|
+
|
|
64
|
+
你的原始输出:
|
|
65
|
+
{previous_response}
|
|
66
|
+
|
|
67
|
+
请修正上述问题并输出完整的最终版本。
|
|
68
|
+
你可以调用工具(read_file / run_shell 等)来验证修正结果。
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fmt_context(short_term: list) -> str:
|
|
73
|
+
if not short_term:
|
|
74
|
+
return "(无)"
|
|
75
|
+
return "\n".join(
|
|
76
|
+
f"{m['role'].upper()}: {m['content'][:300]}"
|
|
77
|
+
for m in short_term[-6:]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_tool(tool_call: dict) -> str:
|
|
82
|
+
"""执行单个工具调用,返回结果字符串"""
|
|
83
|
+
name = tool_call.get("name", "")
|
|
84
|
+
args = tool_call.get("args", {})
|
|
85
|
+
tool_fn = _TOOL_MAP.get(name)
|
|
86
|
+
if tool_fn is None:
|
|
87
|
+
return f"未知工具:{name}"
|
|
88
|
+
try:
|
|
89
|
+
return str(tool_fn.invoke(args))
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return f"工具执行失败:{e}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tool_loop(llm_with_tools, messages: list) -> str:
|
|
95
|
+
"""执行 LLM + 工具调用循环,最多 _MAX_TOOL_ROUNDS 轮,返回最终文本响应"""
|
|
96
|
+
for _ in range(_MAX_TOOL_ROUNDS):
|
|
97
|
+
response = llm_with_tools.invoke(messages)
|
|
98
|
+
tool_calls = getattr(response, "tool_calls", None)
|
|
99
|
+
|
|
100
|
+
if not tool_calls:
|
|
101
|
+
# 无工具调用,直接返回文本
|
|
102
|
+
return response.content
|
|
103
|
+
|
|
104
|
+
# 追加 AI 消息(包含 tool_calls)
|
|
105
|
+
messages.append(response)
|
|
106
|
+
|
|
107
|
+
# 执行所有工具调用,追加 ToolMessage
|
|
108
|
+
for tc in tool_calls:
|
|
109
|
+
result = _run_tool(tc)
|
|
110
|
+
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
|
|
111
|
+
|
|
112
|
+
# 超过最大轮次:用不绑定工具的 LLM 强制生成文字总结,避免返回空内容
|
|
113
|
+
messages.append({
|
|
114
|
+
"role": "user",
|
|
115
|
+
"content": "请根据以上工具调用的结果,给出完整的分析和回答。",
|
|
116
|
+
})
|
|
117
|
+
final = get_llm().invoke(messages)
|
|
118
|
+
return final.content or "(工具调用已达到最大轮次,无法生成完整回答,请尝试更具体的问题)"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def code_agent_node(state: AgentState) -> dict:
|
|
122
|
+
llm = get_llm()
|
|
123
|
+
llm_with_tools = llm.bind_tools(all_tools)
|
|
124
|
+
is_revision = bool(state.get("reflection_feedback"))
|
|
125
|
+
|
|
126
|
+
if is_revision:
|
|
127
|
+
system_content = _REVISION_SYSTEM.format(
|
|
128
|
+
reflection_feedback=state.get("reflection_feedback", ""),
|
|
129
|
+
previous_response=state.get("final_response", ""),
|
|
130
|
+
)
|
|
131
|
+
messages = [
|
|
132
|
+
{"role": "system", "content": system_content},
|
|
133
|
+
{"role": "user", "content": f"原始需求:{state['user_input']}"},
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
system_content = _CODE_SYSTEM.format(
|
|
137
|
+
long_term_summary=state.get("long_term_summary") or "(无)",
|
|
138
|
+
retrieved_memory=state.get("retrieved_memory") or "(无)",
|
|
139
|
+
context=_fmt_context(state.get("short_term", [])),
|
|
140
|
+
)
|
|
141
|
+
messages = [
|
|
142
|
+
{"role": "system", "content": system_content},
|
|
143
|
+
{"role": "user", "content": state["user_input"]},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
final_text = _tool_loop(llm_with_tools, messages)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"final_response": final_text,
|
|
150
|
+
"response_type": "markdown",
|
|
151
|
+
"reflection_feedback": "", # 清空,避免下轮误用
|
|
152
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
context_loader —— 上下文加载节点(Phase 2 升级版)
|
|
3
|
+
|
|
4
|
+
每次对话开始时执行:
|
|
5
|
+
1. 确保 SQLite 已初始化
|
|
6
|
+
2. 加载该 thread_id 的最新 long_term_summary
|
|
7
|
+
3. 语义检索 ChromaDB,获取 retrieved_memory(Top-3)
|
|
8
|
+
4. 若 short_term 为空,从 SQLite 恢复最近 N 条消息
|
|
9
|
+
"""
|
|
10
|
+
from langchain_core.runnables import RunnableConfig
|
|
11
|
+
|
|
12
|
+
from codeagent.graph.state import AgentState
|
|
13
|
+
from codeagent.memory.long_term import init_db, upsert_session, get_latest_summary, get_recent_messages
|
|
14
|
+
from codeagent.memory.vector_store import retrieve_memories
|
|
15
|
+
from codeagent.config import settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def context_loader_node(state: AgentState, config: RunnableConfig) -> dict:
|
|
19
|
+
thread_id: str = config.get("configurable", {}).get("thread_id", "default")
|
|
20
|
+
|
|
21
|
+
# 确保数据库表存在
|
|
22
|
+
init_db()
|
|
23
|
+
upsert_session(thread_id)
|
|
24
|
+
|
|
25
|
+
updates: dict = {}
|
|
26
|
+
|
|
27
|
+
# ── 1. 加载长期摘要 ────────────────────────────
|
|
28
|
+
if not state.get("long_term_summary"):
|
|
29
|
+
summary = get_latest_summary(thread_id)
|
|
30
|
+
if summary:
|
|
31
|
+
updates["long_term_summary"] = summary
|
|
32
|
+
|
|
33
|
+
# ── 2. 语义检索相关记忆 ────────────────────────
|
|
34
|
+
user_input = state.get("user_input", "")
|
|
35
|
+
if user_input:
|
|
36
|
+
retrieved = retrieve_memories(user_input, top_k=3)
|
|
37
|
+
updates["retrieved_memory"] = retrieved
|
|
38
|
+
|
|
39
|
+
# ── 3. 恢复 short_term(会话重启时) ──────────
|
|
40
|
+
if not state.get("short_term"):
|
|
41
|
+
recent = get_recent_messages(thread_id, limit=settings.WINDOW_SIZE * 2)
|
|
42
|
+
if recent:
|
|
43
|
+
updates["short_term"] = recent
|
|
44
|
+
|
|
45
|
+
return updates
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
context_qa —— 上下文问答节点(Phase 2 升级版)
|
|
3
|
+
|
|
4
|
+
system prompt 新增三层记忆注入(同 code_agent)。
|
|
5
|
+
memory 意图直接返回当前 long_term_summary 内容,让用户查看历史。
|
|
6
|
+
"""
|
|
7
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
8
|
+
from codeagent.graph.state import AgentState
|
|
9
|
+
from codeagent.config import get_llm
|
|
10
|
+
|
|
11
|
+
_QA_SYSTEM = """\
|
|
12
|
+
你是一个专业的编程助手,擅长解释概念、回答技术问题、提供建议。
|
|
13
|
+
|
|
14
|
+
回答要求:
|
|
15
|
+
1. 准确清晰,避免冗余
|
|
16
|
+
2. 必要时给出代码示例
|
|
17
|
+
3. 复杂概念分点说明
|
|
18
|
+
|
|
19
|
+
── 历史摘要 ──────────────────────
|
|
20
|
+
{long_term_summary}
|
|
21
|
+
|
|
22
|
+
── 相关记忆 ──────────────────────
|
|
23
|
+
{retrieved_memory}
|
|
24
|
+
|
|
25
|
+
── 近期对话 ──────────────────────
|
|
26
|
+
{context}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_PROMPT = ChatPromptTemplate.from_messages([
|
|
30
|
+
("system", _QA_SYSTEM),
|
|
31
|
+
("human", "{user_input}"),
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fmt_context(short_term: list) -> str:
|
|
36
|
+
if not short_term:
|
|
37
|
+
return "(无)"
|
|
38
|
+
return "\n".join(
|
|
39
|
+
f"{m['role'].upper()}: {m['content'][:300]}"
|
|
40
|
+
for m in short_term[-6:]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def context_qa_node(state: AgentState) -> dict:
|
|
45
|
+
# memory 意图:展示当前长期摘要 + 相关检索片段
|
|
46
|
+
if state.get("intent") == "memory":
|
|
47
|
+
parts = []
|
|
48
|
+
summary = state.get("long_term_summary")
|
|
49
|
+
if summary:
|
|
50
|
+
parts.append(f"**长期记忆摘要:**\n\n{summary}")
|
|
51
|
+
else:
|
|
52
|
+
parts.append("**长期记忆摘要:** 暂无(对话超过 10 轮后自动生成)")
|
|
53
|
+
|
|
54
|
+
retrieved = state.get("retrieved_memory")
|
|
55
|
+
if retrieved:
|
|
56
|
+
parts.append(f"**语义检索到的相关历史:**\n\n{retrieved[:600]}")
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"final_response": "\n\n---\n\n".join(parts),
|
|
60
|
+
"response_type": "markdown",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
llm = get_llm(streaming=True)
|
|
64
|
+
chain = _PROMPT | llm
|
|
65
|
+
result = chain.invoke({
|
|
66
|
+
"user_input": state["user_input"],
|
|
67
|
+
"long_term_summary": state.get("long_term_summary") or "(无)",
|
|
68
|
+
"retrieved_memory": state.get("retrieved_memory") or "(无)",
|
|
69
|
+
"context": _fmt_context(state.get("short_term", [])),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {"final_response": result.content, "response_type": "markdown"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
intent_router —— 意图路由节点
|
|
3
|
+
|
|
4
|
+
将用户输入分类为:code | decompose | qa | memory
|
|
5
|
+
使用 few-shot prompt 提高分类准确性。
|
|
6
|
+
"""
|
|
7
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
8
|
+
from codeagent.graph.state import AgentState
|
|
9
|
+
from codeagent.config import get_llm
|
|
10
|
+
|
|
11
|
+
_INTENT_SYSTEM = """\
|
|
12
|
+
你是一个编程助手的意图分类器。将用户输入分类为以下四类之一,只输出类别名称,不要其他内容。
|
|
13
|
+
|
|
14
|
+
类别定义:
|
|
15
|
+
- code :用户想生成、解释、调试、重构具体代码片段,或需要读取/操作文件
|
|
16
|
+
- decompose :用户有复杂的编程任务(需要多步骤才能完成的项目/功能)
|
|
17
|
+
- qa :用户在问编程概念、原理、工具用法、项目分析或寻求技术建议
|
|
18
|
+
- memory :用户明确询问"你之前帮我做过什么"、"我们之前聊了什么"等对话历史,或要求清空记忆
|
|
19
|
+
|
|
20
|
+
重要规则:
|
|
21
|
+
- "查看项目/分析项目/项目功能" → code(需要读取文件)
|
|
22
|
+
- "分析/解释/介绍某个概念" → qa
|
|
23
|
+
- 只有用户明确问历史对话记录时才选 memory
|
|
24
|
+
|
|
25
|
+
示例:
|
|
26
|
+
用户:帮我写一个二分查找 → code
|
|
27
|
+
用户:查看一下我的项目,分析功能 → code
|
|
28
|
+
用户:帮我从零搭建一个 FastAPI + SQLAlchemy 的用户管理系统 → decompose
|
|
29
|
+
用户:什么是装饰器?Python 的 GIL 是什么? → qa
|
|
30
|
+
用户:你之前帮我写过什么?上次我们聊了什么? → memory
|
|
31
|
+
用户:解释一下这段代码 → code
|
|
32
|
+
用户:帮我实现一个完整的博客系统 → decompose
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_PROMPT = ChatPromptTemplate.from_messages([
|
|
36
|
+
("system", _INTENT_SYSTEM),
|
|
37
|
+
("human", "{user_input}"),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
_VALID_INTENTS = {"code", "decompose", "qa", "memory"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def intent_router_node(state: AgentState) -> dict:
|
|
44
|
+
llm = get_llm()
|
|
45
|
+
chain = _PROMPT | llm
|
|
46
|
+
result = chain.invoke({"user_input": state["user_input"]})
|
|
47
|
+
content = result.content.strip().lower()
|
|
48
|
+
|
|
49
|
+
# 健壮解析:从响应中找到第一个合法意图词
|
|
50
|
+
intent = "qa" # 默认兜底
|
|
51
|
+
for token in content.split():
|
|
52
|
+
if token in _VALID_INTENTS:
|
|
53
|
+
intent = token
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
return {"intent": intent}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def route_by_intent(state: AgentState) -> str:
|
|
60
|
+
"""条件边路由函数,返回下一个节点名称"""
|
|
61
|
+
intent = state.get("intent", "qa")
|
|
62
|
+
if intent == "code":
|
|
63
|
+
return "code_agent"
|
|
64
|
+
if intent == "decompose":
|
|
65
|
+
return "task_decomposer"
|
|
66
|
+
return "context_qa"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory_writer —— 记忆写入节点(Phase 2 升级版)
|
|
3
|
+
|
|
4
|
+
每轮对话结束后执行:
|
|
5
|
+
1. 更新 short_term 滑动窗口
|
|
6
|
+
2. 将本轮消息持久化到 SQLite
|
|
7
|
+
3. 将摘要对存入 ChromaDB(用于语义检索)
|
|
8
|
+
4. 判断是否需要压缩:若 short_term 超出阈值,执行压缩
|
|
9
|
+
- 压缩结果存入 SQLite summaries 表
|
|
10
|
+
- 压缩摘要存入 ChromaDB
|
|
11
|
+
- short_term 截断到最近 3 轮(6 条)
|
|
12
|
+
"""
|
|
13
|
+
from langchain_core.runnables import RunnableConfig
|
|
14
|
+
|
|
15
|
+
from codeagent.graph.state import AgentState
|
|
16
|
+
from codeagent.config import settings
|
|
17
|
+
from codeagent.memory.long_term import save_messages, save_summary, upsert_session
|
|
18
|
+
from codeagent.memory.compressor import should_compress, split_for_compression, compress
|
|
19
|
+
from codeagent.memory.vector_store import store_memory
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def memory_writer_node(state: AgentState, config: RunnableConfig) -> dict:
|
|
23
|
+
thread_id: str = config.get("configurable", {}).get("thread_id", "default")
|
|
24
|
+
user_input = state.get("user_input", "")
|
|
25
|
+
final_response = state.get("final_response", "")
|
|
26
|
+
|
|
27
|
+
# ── 1. 更新 short_term ────────────────────────
|
|
28
|
+
short_term = list(state.get("short_term", []))
|
|
29
|
+
short_term.append({"role": "user", "content": user_input})
|
|
30
|
+
short_term.append({"role": "assistant", "content": final_response})
|
|
31
|
+
|
|
32
|
+
# ── 2. 持久化到 SQLite ────────────────────────
|
|
33
|
+
try:
|
|
34
|
+
upsert_session(thread_id)
|
|
35
|
+
save_messages(thread_id, user_input, final_response)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# ── 3. 存入向量库(本轮摘要对)─────────────
|
|
40
|
+
# 跳过 memory 意图的回复,避免把记忆摘要本身再存入向量库造成"套娃"
|
|
41
|
+
intent = state.get("intent", "")
|
|
42
|
+
if intent != "memory":
|
|
43
|
+
try:
|
|
44
|
+
turn_text = f"Q: {user_input[:300]}\nA: {final_response[:300]}"
|
|
45
|
+
store_memory(thread_id, turn_text, doc_type="turn")
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# ── 4. 判断是否压缩 ────────────────────────────
|
|
50
|
+
long_term_summary = state.get("long_term_summary", "")
|
|
51
|
+
needs_compression = False
|
|
52
|
+
|
|
53
|
+
if should_compress(short_term):
|
|
54
|
+
old_messages, short_term = split_for_compression(short_term)
|
|
55
|
+
new_summary = compress(old_messages, long_term_summary)
|
|
56
|
+
|
|
57
|
+
long_term_summary = new_summary
|
|
58
|
+
needs_compression = True
|
|
59
|
+
|
|
60
|
+
# 持久化摘要
|
|
61
|
+
try:
|
|
62
|
+
save_summary(thread_id, new_summary)
|
|
63
|
+
store_memory(thread_id, new_summary, doc_type="summary")
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"short_term": short_term,
|
|
69
|
+
"long_term_summary": long_term_summary,
|
|
70
|
+
"needs_compression": needs_compression,
|
|
71
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
reflection.py —— Reflection 节点(Phase 3)
|
|
3
|
+
|
|
4
|
+
触发条件:intent 为 code 或 decompose,且 revision_count < 1。
|
|
5
|
+
判断逻辑:
|
|
6
|
+
- 调用 LLM 审查 final_response
|
|
7
|
+
- 响应以 PASS 开头 → 通过,needs_revision=False
|
|
8
|
+
- 响应以 NEEDS_REVISION 开头 → 设置反馈,needs_revision=True
|
|
9
|
+
|
|
10
|
+
路由函数 route_after_reflection:
|
|
11
|
+
- needs_revision=True → 回到 code_agent 或 task_decomposer(仅一次)
|
|
12
|
+
- needs_revision=False → 前往 memory_writer
|
|
13
|
+
"""
|
|
14
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
15
|
+
from codeagent.graph.state import AgentState
|
|
16
|
+
from codeagent.config import get_llm
|
|
17
|
+
|
|
18
|
+
_REFLECTION_SYSTEM = """\
|
|
19
|
+
你是一名严格的代码审查员,负责快速审查编程助手的输出质量。
|
|
20
|
+
|
|
21
|
+
审查维度(仅关注明显问题,不要过度挑剔):
|
|
22
|
+
1. 代码是否有语法错误或明显的逻辑缺陷?
|
|
23
|
+
2. 是否遗漏了用户明确要求的关键功能?
|
|
24
|
+
3. 回答是否与用户问题严重不符?
|
|
25
|
+
|
|
26
|
+
输出格式(严格遵守,只能选其一):
|
|
27
|
+
|
|
28
|
+
若输出质量合格:
|
|
29
|
+
PASS
|
|
30
|
+
|
|
31
|
+
若存在明显问题(不超过 3 条):
|
|
32
|
+
NEEDS_REVISION
|
|
33
|
+
- 问题 1
|
|
34
|
+
- 问题 2
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_PROMPT = ChatPromptTemplate.from_messages([
|
|
38
|
+
("system", _REFLECTION_SYSTEM),
|
|
39
|
+
("human", "用户需求:{user_input}\n\n助手输出:\n{response}"),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
_REVIEWED_INTENTS = {"code", "decompose"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def reflection_node(state: AgentState) -> dict:
|
|
46
|
+
intent = state.get("intent", "qa")
|
|
47
|
+
revision_count = state.get("revision_count", 0)
|
|
48
|
+
|
|
49
|
+
# 不需要 reflection 的情况:直接通过
|
|
50
|
+
if intent not in _REVIEWED_INTENTS or revision_count >= 1:
|
|
51
|
+
return {"needs_revision": False, "reflection_feedback": ""}
|
|
52
|
+
|
|
53
|
+
llm = get_llm()
|
|
54
|
+
chain = _PROMPT | llm
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = chain.invoke({
|
|
58
|
+
"user_input": state.get("user_input", ""),
|
|
59
|
+
"response": state.get("final_response", "")[:2000], # 截断防 token 超出
|
|
60
|
+
})
|
|
61
|
+
content = result.content.strip()
|
|
62
|
+
except Exception:
|
|
63
|
+
# reflection 失败,不阻断主流程
|
|
64
|
+
return {"needs_revision": False, "reflection_feedback": ""}
|
|
65
|
+
|
|
66
|
+
if content.upper().startswith("PASS"):
|
|
67
|
+
return {"needs_revision": False, "reflection_feedback": ""}
|
|
68
|
+
|
|
69
|
+
# 提取 NEEDS_REVISION 后面的具体反馈
|
|
70
|
+
feedback = content.replace("NEEDS_REVISION", "").strip()
|
|
71
|
+
return {
|
|
72
|
+
"needs_revision": True,
|
|
73
|
+
"reflection_feedback": feedback,
|
|
74
|
+
"revision_count": revision_count + 1,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def route_after_reflection(state: AgentState) -> str:
|
|
79
|
+
"""Reflection 后的路由:需要修正 → 回原节点;否则 → memory_writer"""
|
|
80
|
+
if state.get("needs_revision"):
|
|
81
|
+
return "task_decomposer" if state.get("intent") == "decompose" else "code_agent"
|
|
82
|
+
return "memory_writer"
|