feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
@ -22,6 +22,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
@ -224,8 +225,29 @@ class ContextBuilder:
|
||||
clean = {key: value for key, value in message.items() if key in allowed}
|
||||
if "name" not in clean and message.get("tool_name"):
|
||||
clean["name"] = message.get("tool_name")
|
||||
if isinstance(clean.get("tool_calls"), list):
|
||||
clean["tool_calls"] = ContextBuilder._provider_tool_calls(clean["tool_calls"])
|
||||
return clean
|
||||
|
||||
@staticmethod
|
||||
def _provider_tool_calls(tool_calls: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Normalize persisted tool calls to OpenAI-compatible provider payloads."""
|
||||
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for tool_call in tool_calls:
|
||||
if not isinstance(tool_call, dict):
|
||||
continue
|
||||
clean = dict(tool_call)
|
||||
function = clean.get("function")
|
||||
if isinstance(function, dict):
|
||||
clean_function = dict(function)
|
||||
arguments = clean_function.get("arguments")
|
||||
if not isinstance(arguments, str):
|
||||
clean_function["arguments"] = json.dumps(arguments or {}, ensure_ascii=False, default=str)
|
||||
clean["function"] = clean_function
|
||||
normalized.append(clean)
|
||||
return normalized
|
||||
|
||||
def add_tool_result(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
@ -278,7 +300,7 @@ class ContextBuilder:
|
||||
"content": content,
|
||||
}
|
||||
if tool_calls:
|
||||
message["tool_calls"] = tool_calls
|
||||
message["tool_calls"] = self._provider_tool_calls(tool_calls)
|
||||
if reasoning_content is not None:
|
||||
message["reasoning_content"] = reasoning_content
|
||||
messages.append(message)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@ -11,6 +12,7 @@ from beaver.coordinator.registry import AgentRegistry
|
||||
from beaver.engine.context import ContextBuilder
|
||||
from beaver.engine.session import SessionManager
|
||||
from beaver.foundation.config import BeaverConfig, load_config
|
||||
from beaver.integrations.mcp import MCPConnectionManager
|
||||
from beaver.memory.curated.store import MemoryStore
|
||||
from beaver.memory.runs import RunMemoryStore
|
||||
from beaver.memory.skills import SkillLearningStore
|
||||
@ -27,13 +29,27 @@ from beaver.tasks.skill_resolver import TaskSkillResolver
|
||||
from beaver.skills import SkillAssembler, SkillsLoader
|
||||
from beaver.tools import ObjectBackedTool, ToolAssembler, ToolExecutor, ToolRegistry
|
||||
from beaver.tools.builtins import (
|
||||
ClarifyTool,
|
||||
CronTool,
|
||||
DelegateTool,
|
||||
EchoTool,
|
||||
ExecuteCodeTool,
|
||||
ListDirectoryTool,
|
||||
MemoryTool,
|
||||
PatchFileTool,
|
||||
ProcessTool,
|
||||
ReadFileTool,
|
||||
SearchFilesTool,
|
||||
SendMessageTool,
|
||||
SpawnTool,
|
||||
SessionSearchTool,
|
||||
SkillViewTool,
|
||||
SkillManageTool,
|
||||
SkillsListTool,
|
||||
TerminalTool,
|
||||
TodoTool,
|
||||
WebFetchTool,
|
||||
WebSearchTool,
|
||||
WriteFileTool,
|
||||
)
|
||||
|
||||
|
||||
@ -76,6 +92,8 @@ class EngineLoadResult:
|
||||
task_service: TaskService | None = None
|
||||
task_execution_planner: TaskExecutionPlanner | None = None
|
||||
validation_service: ValidationService | None = None
|
||||
mcp_manager: MCPConnectionManager | None = None
|
||||
mcp_report: dict[str, dict] = field(default_factory=dict)
|
||||
closeables: list[tuple[str, Callable[[], None]]] = field(default_factory=list, repr=False)
|
||||
closed: bool = False
|
||||
|
||||
@ -198,11 +216,25 @@ class EngineLoader:
|
||||
[
|
||||
ObjectBackedTool(EchoTool()),
|
||||
ObjectBackedTool(MemoryTool(store=memory_service.get_store())),
|
||||
ObjectBackedTool(SkillViewTool(loader=skills_loader)),
|
||||
ObjectBackedTool(SessionSearchTool(db=session_manager)),
|
||||
ObjectBackedTool(ListDirectoryTool()),
|
||||
ObjectBackedTool(ReadFileTool()),
|
||||
ObjectBackedTool(SearchFilesTool()),
|
||||
ObjectBackedTool(WriteFileTool()),
|
||||
ObjectBackedTool(PatchFileTool()),
|
||||
ObjectBackedTool(WebFetchTool()),
|
||||
ObjectBackedTool(WebSearchTool()),
|
||||
ObjectBackedTool(TerminalTool()),
|
||||
ObjectBackedTool(ProcessTool()),
|
||||
ObjectBackedTool(ExecuteCodeTool()),
|
||||
ObjectBackedTool(TodoTool()),
|
||||
ObjectBackedTool(ClarifyTool()),
|
||||
ObjectBackedTool(SendMessageTool()),
|
||||
ObjectBackedTool(DelegateTool()),
|
||||
ObjectBackedTool(SpawnTool()),
|
||||
SkillsListTool(),
|
||||
SkillManageTool(),
|
||||
CronTool(),
|
||||
]
|
||||
)
|
||||
|
||||
@ -240,6 +272,11 @@ class EngineLoader:
|
||||
task_service = self._task_service or TaskService(workspace / "tasks")
|
||||
task_execution_planner = self._task_execution_planner or TaskExecutionPlanner(task_skill_resolver=task_skill_resolver)
|
||||
validation_service = self._validation_service or ValidationService()
|
||||
mcp_manager = MCPConnectionManager(
|
||||
self.config.tools.mcp_servers,
|
||||
authz_config=self.config.authz,
|
||||
backend_identity=self.config.backend_identity,
|
||||
)
|
||||
|
||||
result = EngineLoadResult(
|
||||
workspace=workspace,
|
||||
@ -270,7 +307,18 @@ class EngineLoader:
|
||||
task_service=task_service,
|
||||
task_execution_planner=task_execution_planner,
|
||||
validation_service=validation_service,
|
||||
mcp_manager=mcp_manager,
|
||||
)
|
||||
if self._session_manager is None:
|
||||
result.register_closeable("session_manager", session_manager.close)
|
||||
result.register_closeable("mcp_manager", lambda: _close_mcp_manager(mcp_manager))
|
||||
return result
|
||||
|
||||
|
||||
def _close_mcp_manager(manager: MCPConnectionManager) -> None:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
asyncio.run(manager.close())
|
||||
return
|
||||
loop.create_task(manager.close())
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
@ -64,6 +65,7 @@ class AgentLoop:
|
||||
self.profile = profile or AgentProfile()
|
||||
self.loader = loader or EngineLoader()
|
||||
self.loaded: EngineLoadResult | None = None
|
||||
self.runtime_services: dict[str, Any] = {}
|
||||
self._run_queue: asyncio.Queue[_DirectRunRequest | None] | None = None
|
||||
self._running = False
|
||||
self._stop_requested = False
|
||||
@ -190,6 +192,7 @@ class AgentLoop:
|
||||
user_id: str | None = None,
|
||||
title: str | None = None,
|
||||
execution_context: str | None = None,
|
||||
skill_selection_context: str | None = None,
|
||||
model: str | None = None,
|
||||
provider_name: str | None = None,
|
||||
api_key: str | None = None,
|
||||
@ -202,6 +205,9 @@ class AgentLoop:
|
||||
embedding_model: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
thinking_enabled: bool | None = None,
|
||||
include_skill_assembly: bool = True,
|
||||
include_tools: bool = True,
|
||||
max_tool_iterations: int | None = None,
|
||||
provider_bundle: ProviderBundle | None = None,
|
||||
parent_session_id: str | None = None,
|
||||
@ -210,7 +216,7 @@ class AgentLoop:
|
||||
attempt_index: int | None = None,
|
||||
pinned_skill_names: list[str] | None = None,
|
||||
pinned_skill_contexts: list[SkillContext] | None = None,
|
||||
learning_candidate_enabled: bool = False,
|
||||
allow_candidate_generation: bool = False,
|
||||
) -> AgentRunResult:
|
||||
"""跑通最小 direct run 主链。
|
||||
|
||||
@ -234,6 +240,7 @@ class AgentLoop:
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
execution_context=execution_context,
|
||||
skill_selection_context=skill_selection_context,
|
||||
model=model,
|
||||
provider_name=provider_name,
|
||||
api_key=api_key,
|
||||
@ -246,6 +253,9 @@ class AgentLoop:
|
||||
embedding_model=embedding_model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
thinking_enabled=thinking_enabled,
|
||||
include_skill_assembly=include_skill_assembly,
|
||||
include_tools=include_tools,
|
||||
max_tool_iterations=max_tool_iterations,
|
||||
provider_bundle=provider_bundle,
|
||||
parent_session_id=parent_session_id,
|
||||
@ -254,7 +264,7 @@ class AgentLoop:
|
||||
attempt_index=attempt_index,
|
||||
pinned_skill_names=pinned_skill_names,
|
||||
pinned_skill_contexts=pinned_skill_contexts,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
allow_candidate_generation=allow_candidate_generation,
|
||||
)
|
||||
|
||||
async def _process_direct_impl(
|
||||
@ -266,6 +276,7 @@ class AgentLoop:
|
||||
user_id: str | None = None,
|
||||
title: str | None = None,
|
||||
execution_context: str | None = None,
|
||||
skill_selection_context: str | None = None,
|
||||
model: str | None = None,
|
||||
provider_name: str | None = None,
|
||||
api_key: str | None = None,
|
||||
@ -278,6 +289,9 @@ class AgentLoop:
|
||||
embedding_model: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
thinking_enabled: bool | None = None,
|
||||
include_skill_assembly: bool = True,
|
||||
include_tools: bool = True,
|
||||
max_tool_iterations: int | None = None,
|
||||
provider_bundle: ProviderBundle | None = None,
|
||||
parent_session_id: str | None = None,
|
||||
@ -286,7 +300,7 @@ class AgentLoop:
|
||||
attempt_index: int | None = None,
|
||||
pinned_skill_names: list[str] | None = None,
|
||||
pinned_skill_contexts: list[SkillContext] | None = None,
|
||||
learning_candidate_enabled: bool = False,
|
||||
allow_candidate_generation: bool = False,
|
||||
) -> AgentRunResult:
|
||||
"""真正执行一轮 direct run 的内部实现。
|
||||
|
||||
@ -306,6 +320,10 @@ class AgentLoop:
|
||||
skills_loader = self._require_loaded("skills_loader")
|
||||
skill_assembler = self._require_loaded("skill_assembler")
|
||||
skill_learning_service = self._require_loaded("skill_learning_service")
|
||||
mcp_manager = getattr(loaded, "mcp_manager", None)
|
||||
if mcp_manager is not None:
|
||||
loaded.mcp_report = await mcp_manager.connect_all(tool_registry)
|
||||
loaded.tools = [spec.name for spec in tool_registry.list_specs()]
|
||||
|
||||
config = loaded.config
|
||||
configured_provider = config.resolve_provider_target(model=model, provider_name=provider_name)
|
||||
@ -357,6 +375,9 @@ class AgentLoop:
|
||||
"task_id": task_id,
|
||||
"task_mode": task_mode,
|
||||
"attempt_index": attempt_index,
|
||||
"thinking_enabled": thinking_enabled,
|
||||
"include_skill_assembly": include_skill_assembly,
|
||||
"skill_selection_context_present": bool(skill_selection_context),
|
||||
"parent_session_id": parent_session_id,
|
||||
"pinned_skill_names": list(pinned_skill_names or []),
|
||||
"pinned_skill_context_names": [skill.name for skill in pinned_skill_contexts or []],
|
||||
@ -396,19 +417,39 @@ class AgentLoop:
|
||||
if bundle.auxiliary_runtime is not None
|
||||
else bundle.main_runtime.model
|
||||
)
|
||||
assembled_skills = await skill_assembler.assemble(
|
||||
task_description=task,
|
||||
provider=skill_selector_provider,
|
||||
model=skill_selector_model,
|
||||
embedding_runtime=bundle.embedding_runtime,
|
||||
)
|
||||
activated_skills = self._merge_skill_contexts(
|
||||
[
|
||||
*(pinned_skill_contexts or []),
|
||||
*self._load_pinned_skill_contexts(skills_loader, pinned_skill_names or []),
|
||||
],
|
||||
assembled_skills.activated_skills,
|
||||
)
|
||||
pinned_skills = [
|
||||
*(pinned_skill_contexts or []),
|
||||
*self._load_pinned_skill_contexts(skills_loader, pinned_skill_names or []),
|
||||
]
|
||||
if not include_skill_assembly or thinking_enabled is False:
|
||||
activated_skills = self._merge_skill_contexts(pinned_skills, [])
|
||||
else:
|
||||
skill_query = skill_selection_context or task
|
||||
assembled_skills = await skill_assembler.assemble(
|
||||
task_description=skill_query,
|
||||
provider=skill_selector_provider,
|
||||
model=skill_selector_model,
|
||||
embedding_runtime=bundle.embedding_runtime,
|
||||
thinking_enabled=thinking_enabled,
|
||||
)
|
||||
for interaction in getattr(assembled_skills, "llm_interactions", []) or []:
|
||||
session_manager.append_message(
|
||||
resolved_session_id,
|
||||
run_id=resolved_run_id,
|
||||
role="system",
|
||||
event_type="skill_assembler_llm_interaction_snapshotted",
|
||||
event_payload=interaction,
|
||||
content=json.dumps(interaction, ensure_ascii=False, default=str),
|
||||
context_visible=False,
|
||||
source=source,
|
||||
title=title,
|
||||
model=skill_selector_model,
|
||||
user_id=user_id,
|
||||
)
|
||||
activated_skills = self._merge_skill_contexts(
|
||||
pinned_skills,
|
||||
assembled_skills.activated_skills,
|
||||
)
|
||||
skill_activation_messages = context_builder.build_skill_activation_messages(
|
||||
activated_skills
|
||||
)
|
||||
@ -444,14 +485,19 @@ class AgentLoop:
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
selected_tool_specs = await tool_assembler.assemble(
|
||||
task_description=task,
|
||||
registry=tool_registry,
|
||||
skills_loader=skills_loader,
|
||||
activated_skills=activated_skills,
|
||||
embedding_runtime=bundle.embedding_runtime,
|
||||
top_k=10,
|
||||
)
|
||||
if not include_tools:
|
||||
selected_tool_specs = []
|
||||
elif thinking_enabled is False:
|
||||
selected_tool_specs = tool_registry.list_specs()
|
||||
else:
|
||||
selected_tool_specs = await tool_assembler.assemble(
|
||||
task_description=task,
|
||||
registry=tool_registry,
|
||||
skills_loader=skills_loader,
|
||||
activated_skills=activated_skills,
|
||||
embedding_runtime=bundle.embedding_runtime,
|
||||
top_k=10,
|
||||
)
|
||||
tool_schemas = tool_registry.export_selected_provider_schemas(selected_tool_specs)
|
||||
session_manager.append_message(
|
||||
resolved_session_id,
|
||||
@ -486,6 +532,25 @@ class AgentLoop:
|
||||
execution_context=execution_context,
|
||||
)
|
||||
context_result = context_builder.build_messages(build_input)
|
||||
if skill_selection_context:
|
||||
session_manager.append_message(
|
||||
resolved_session_id,
|
||||
run_id=resolved_run_id,
|
||||
role="system",
|
||||
event_type="skill_selection_context_snapshotted",
|
||||
event_payload={
|
||||
"skill_selection_context": skill_selection_context,
|
||||
"task_id": task_id,
|
||||
"task_mode": task_mode,
|
||||
"attempt_index": attempt_index,
|
||||
},
|
||||
content=skill_selection_context,
|
||||
context_visible=False,
|
||||
source=source,
|
||||
title=title,
|
||||
model=resolved_model,
|
||||
user_id=user_id,
|
||||
)
|
||||
session_manager.update_system_prompt(resolved_session_id, context_result.system_prompt)
|
||||
session_manager.append_message(
|
||||
resolved_session_id,
|
||||
@ -528,6 +593,9 @@ class AgentLoop:
|
||||
"memory_service": memory_service,
|
||||
"memory_store": memory_service.get_store(),
|
||||
"tool_registry": tool_registry,
|
||||
"skills_loader": skills_loader,
|
||||
"draft_service": getattr(loaded, "draft_service", None),
|
||||
**self.runtime_services,
|
||||
},
|
||||
metadata={
|
||||
"source": source,
|
||||
@ -541,13 +609,45 @@ class AgentLoop:
|
||||
final_model = bundle.main_runtime.model
|
||||
|
||||
while True:
|
||||
response = await provider.chat(
|
||||
messages=messages,
|
||||
tools=tool_schemas,
|
||||
chat_kwargs: dict[str, Any] = {
|
||||
"messages": messages,
|
||||
"tools": tool_schemas,
|
||||
"model": final_model,
|
||||
"max_tokens": resolved_max_tokens,
|
||||
"temperature": resolved_temperature,
|
||||
}
|
||||
if thinking_enabled is not None:
|
||||
chat_kwargs["thinking_enabled"] = thinking_enabled
|
||||
session_manager.append_message(
|
||||
resolved_session_id,
|
||||
run_id=resolved_run_id,
|
||||
role="system",
|
||||
event_type="llm_request_snapshotted",
|
||||
event_payload={
|
||||
"iteration": iterations,
|
||||
"provider_name": final_provider_name,
|
||||
"model": final_model,
|
||||
"messages": messages,
|
||||
"tools": tool_schemas,
|
||||
"max_tokens": resolved_max_tokens,
|
||||
"temperature": resolved_temperature,
|
||||
"thinking_enabled": thinking_enabled,
|
||||
},
|
||||
content=json.dumps(
|
||||
{
|
||||
"messages": messages,
|
||||
"tools": tool_schemas,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
context_visible=False,
|
||||
source=source,
|
||||
title=title,
|
||||
model=final_model,
|
||||
max_tokens=resolved_max_tokens,
|
||||
temperature=resolved_temperature,
|
||||
user_id=user_id,
|
||||
)
|
||||
response = await provider.chat(**chat_kwargs)
|
||||
final_provider_name = response.provider_name or final_provider_name
|
||||
final_model = response.model or final_model
|
||||
final_usage = self._merge_usage(final_usage, response.usage or {})
|
||||
@ -650,7 +750,7 @@ class AgentLoop:
|
||||
model=final_model,
|
||||
user_id=user_id,
|
||||
)
|
||||
self._record_skill_learning(
|
||||
self._record_run_receipts(
|
||||
skill_learning_service=skill_learning_service,
|
||||
session_manager=session_manager,
|
||||
session_id=resolved_session_id,
|
||||
@ -663,7 +763,7 @@ class AgentLoop:
|
||||
success=(final_finish_reason == "stop"),
|
||||
task_id=task_id,
|
||||
attempt_index=attempt_index,
|
||||
generate_candidates=learning_candidate_enabled,
|
||||
allow_candidate_generation=False,
|
||||
)
|
||||
return AgentRunResult(
|
||||
session_id=resolved_session_id,
|
||||
@ -703,7 +803,7 @@ class AgentLoop:
|
||||
usage=final_usage,
|
||||
task_id=task_id,
|
||||
)
|
||||
self._record_skill_learning(
|
||||
self._record_run_receipts(
|
||||
skill_learning_service=skill_learning_service,
|
||||
session_manager=session_manager,
|
||||
session_id=resolved_session_id,
|
||||
@ -716,7 +816,7 @@ class AgentLoop:
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
attempt_index=attempt_index,
|
||||
generate_candidates=learning_candidate_enabled,
|
||||
allow_candidate_generation=False,
|
||||
)
|
||||
return result
|
||||
|
||||
@ -771,13 +871,16 @@ class AgentLoop:
|
||||
def _serialize_tool_calls(tool_calls: list[Any]) -> list[dict[str, Any]]:
|
||||
payload: list[dict[str, Any]] = []
|
||||
for tool_call in tool_calls:
|
||||
arguments = tool_call.arguments
|
||||
if not isinstance(arguments, str):
|
||||
arguments = json.dumps(arguments or {}, ensure_ascii=False, default=str)
|
||||
payload.append(
|
||||
{
|
||||
"id": tool_call.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call.name,
|
||||
"arguments": tool_call.arguments,
|
||||
"arguments": arguments,
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -877,7 +980,7 @@ class AgentLoop:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _record_skill_learning(
|
||||
def _record_run_receipts(
|
||||
*,
|
||||
skill_learning_service: Any,
|
||||
session_manager: Any,
|
||||
@ -891,7 +994,7 @@ class AgentLoop:
|
||||
success: bool,
|
||||
task_id: str | None = None,
|
||||
attempt_index: int | None = None,
|
||||
generate_candidates: bool = False,
|
||||
allow_candidate_generation: bool = False,
|
||||
) -> None:
|
||||
run_record = RunRecord(
|
||||
run_id=run_id,
|
||||
@ -921,7 +1024,7 @@ class AgentLoop:
|
||||
try:
|
||||
candidates = skill_learning_service.collect_run_receipts(
|
||||
RunReceiptContext(run_record=run_record, effect_records=effect_records),
|
||||
generate_candidates=generate_candidates,
|
||||
generate_candidates=allow_candidate_generation,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - defensive hot-path guard
|
||||
session_manager.append_message(
|
||||
@ -948,7 +1051,7 @@ class AgentLoop:
|
||||
"run_record": run_record.to_dict(),
|
||||
"skill_effects": [item.to_dict() for item in effect_records],
|
||||
"learning_candidates": [candidate.to_dict() for candidate in candidates],
|
||||
"learning_candidate_enabled": generate_candidates,
|
||||
"candidate_generation_allowed": allow_candidate_generation,
|
||||
},
|
||||
content=f"Recorded {len(effect_records)} skill effect record(s).",
|
||||
context_visible=False,
|
||||
|
||||
@ -45,6 +45,7 @@ class AnthropicProvider(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
try:
|
||||
client = self._client_or_raise()
|
||||
|
||||
@ -90,6 +90,7 @@ class LLMProvider(ABC):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
"""统一聊天接口。"""
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ class FallbackProviderChain(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
self._last_provider = self.primary_provider
|
||||
self._last_runtime = self.primary_runtime
|
||||
@ -71,6 +72,7 @@ class FallbackProviderChain(LLMProvider):
|
||||
model=model or self.primary_runtime.model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
thinking_enabled=thinking_enabled,
|
||||
)
|
||||
response = self._decorate_response(response, self.primary_runtime)
|
||||
if not self._should_activate_fallback(response):
|
||||
@ -91,6 +93,7 @@ class FallbackProviderChain(LLMProvider):
|
||||
model=self.fallback_runtime.model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
thinking_enabled=thinking_enabled,
|
||||
)
|
||||
return self._decorate_response(response, self.fallback_runtime)
|
||||
|
||||
@ -114,6 +117,7 @@ class FallbackProviderChain(LLMProvider):
|
||||
model: str,
|
||||
max_tokens: int,
|
||||
temperature: float,
|
||||
thinking_enabled: bool | None,
|
||||
) -> LLMResponse:
|
||||
"""把 provider 抛出的异常也收敛成统一 error response。
|
||||
|
||||
@ -121,13 +125,16 @@ class FallbackProviderChain(LLMProvider):
|
||||
"""
|
||||
|
||||
try:
|
||||
return await provider.chat(
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
)
|
||||
kwargs = {
|
||||
"messages": messages,
|
||||
"tools": tools,
|
||||
"model": model,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
}
|
||||
if thinking_enabled is not None:
|
||||
kwargs["thinking_enabled"] = thinking_enabled
|
||||
return await provider.chat(**kwargs)
|
||||
except Exception as exc:
|
||||
return LLMResponse(
|
||||
content=f"Error: {exc}",
|
||||
|
||||
@ -41,6 +41,7 @@ class OpenAICodexProvider(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
if httpx is None or get_codex_token is None:
|
||||
return LLMResponse(content="Error: codex dependencies are not installed", finish_reason="error", provider_name="openai_codex")
|
||||
|
||||
@ -49,6 +49,7 @@ class CustomProvider(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
client = self._client_or_raise()
|
||||
kwargs: dict[str, Any] = {
|
||||
|
||||
@ -123,6 +123,25 @@ class LiteLLMProvider(LLMProvider):
|
||||
clean = {key: value for key, value in message.items() if key in _ALLOWED_MSG_KEYS}
|
||||
if clean.get("role") == "assistant" and "content" not in clean:
|
||||
clean["content"] = None
|
||||
if isinstance(clean.get("tool_calls"), list):
|
||||
clean["tool_calls"] = LiteLLMProvider._sanitize_tool_calls(clean["tool_calls"])
|
||||
sanitized.append(clean)
|
||||
return sanitized
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_tool_calls(tool_calls: list[Any]) -> list[dict[str, Any]]:
|
||||
sanitized: list[dict[str, Any]] = []
|
||||
for tool_call in tool_calls:
|
||||
if not isinstance(tool_call, dict):
|
||||
continue
|
||||
clean = dict(tool_call)
|
||||
function = clean.get("function")
|
||||
if isinstance(function, dict):
|
||||
clean_function = dict(function)
|
||||
arguments = clean_function.get("arguments")
|
||||
if not isinstance(arguments, str):
|
||||
clean_function["arguments"] = json.dumps(arguments or {}, ensure_ascii=False, default=str)
|
||||
clean["function"] = clean_function
|
||||
sanitized.append(clean)
|
||||
return sanitized
|
||||
|
||||
@ -155,6 +174,18 @@ class LiteLLMProvider(LLMProvider):
|
||||
if provider_payload:
|
||||
kwargs["provider"] = provider_payload
|
||||
|
||||
def _apply_thinking_mode(self, original_model: str, resolved_model: str, kwargs: dict[str, Any], enabled: bool | None) -> None:
|
||||
if enabled is None:
|
||||
return
|
||||
model_key = f"{original_model} {resolved_model}".lower()
|
||||
if "qwen" not in model_key:
|
||||
return
|
||||
extra_body = dict(kwargs.get("extra_body") or {})
|
||||
chat_template_kwargs = dict(extra_body.get("chat_template_kwargs") or {})
|
||||
chat_template_kwargs["enable_thinking"] = bool(enabled)
|
||||
extra_body["chat_template_kwargs"] = chat_template_kwargs
|
||||
kwargs["extra_body"] = extra_body
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
@ -162,6 +193,7 @@ class LiteLLMProvider(LLMProvider):
|
||||
model: str | None = None,
|
||||
max_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
thinking_enabled: bool | None = None,
|
||||
) -> LLMResponse:
|
||||
if acompletion is None:
|
||||
return LLMResponse(content="Error: litellm is not installed", finish_reason="error", provider_name=self.provider_name)
|
||||
@ -174,6 +206,7 @@ class LiteLLMProvider(LLMProvider):
|
||||
"messages": sanitized_messages,
|
||||
"max_tokens": max(1, max_tokens),
|
||||
"temperature": temperature,
|
||||
"timeout": self.request_timeout_seconds or 45.0,
|
||||
}
|
||||
if self.api_key:
|
||||
kwargs["api_key"] = self.api_key
|
||||
@ -186,6 +219,7 @@ class LiteLLMProvider(LLMProvider):
|
||||
kwargs["tool_choice"] = "auto"
|
||||
self._apply_model_overrides(original_model, kwargs)
|
||||
self._apply_openrouter_routing(kwargs)
|
||||
self._apply_thinking_mode(original_model, resolved_model, kwargs, thinking_enabled)
|
||||
env_overrides = self._build_env_overrides(self.api_key, self.api_base, original_model)
|
||||
|
||||
try:
|
||||
|
||||
@ -121,7 +121,37 @@ class SessionManager:
|
||||
3. 让 `ContextBuilder` 明确消费的是“上游裁剪后的可见片段”
|
||||
"""
|
||||
|
||||
history = self.get_messages_as_conversation(session_id)
|
||||
records = self.get_event_records(session_id)
|
||||
completed_run_ids = {
|
||||
record.run_id
|
||||
for record in records
|
||||
if record.run_id and record.event_type == "run_completed"
|
||||
}
|
||||
failed_run_ids = {
|
||||
record.run_id
|
||||
for record in records
|
||||
if record.run_id
|
||||
and record.event_type == "run_completed"
|
||||
and (
|
||||
record.finish_reason == "error"
|
||||
or (record.event_payload or {}).get("finish_reason") == "error"
|
||||
)
|
||||
}
|
||||
history = []
|
||||
for record in records:
|
||||
if not record.context_visible or record.role == "system":
|
||||
continue
|
||||
if record.role == "tool":
|
||||
continue
|
||||
if record.role == "assistant" and record.tool_calls:
|
||||
continue
|
||||
if record.run_id and record.run_id not in completed_run_ids:
|
||||
continue
|
||||
if record.run_id and record.run_id in failed_run_ids:
|
||||
continue
|
||||
if record.role == "assistant" and record.finish_reason == "error":
|
||||
continue
|
||||
history.append(record.to_conversation_message())
|
||||
sliced = history[-max_messages:]
|
||||
for index, message in enumerate(sliced):
|
||||
if message.get("role") == "user":
|
||||
|
||||
@ -88,6 +88,15 @@ class MessageRecord:
|
||||
payload["feedback_state"] = self.event_payload.get("feedback_state")
|
||||
if self.event_payload.get("feedback_error"):
|
||||
payload["feedback_error"] = self.event_payload.get("feedback_error")
|
||||
for key in (
|
||||
"message_type",
|
||||
"scheduled_job_id",
|
||||
"scheduled_run_id",
|
||||
"cron_job_name",
|
||||
"mode",
|
||||
):
|
||||
if self.event_payload.get(key):
|
||||
payload[key] = self.event_payload.get(key)
|
||||
if self.tool_name:
|
||||
payload["tool_name"] = self.tool_name
|
||||
if self.tool_calls:
|
||||
|
||||
@ -70,6 +70,7 @@ class SessionSearchService:
|
||||
include_children: bool = False,
|
||||
source: str | None = None,
|
||||
exclude_sources: list[str] | None = None,
|
||||
exclude_end_reasons: list[str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""列出最近活跃的 session 及其摘要元数据。"""
|
||||
|
||||
@ -85,6 +86,10 @@ class SessionSearchService:
|
||||
placeholders = ",".join("?" for _ in exclude_sources)
|
||||
clauses.append(f"source NOT IN ({placeholders})")
|
||||
params.extend(exclude_sources)
|
||||
if exclude_end_reasons:
|
||||
placeholders = ",".join("?" for _ in exclude_end_reasons)
|
||||
clauses.append(f"(end_reason IS NULL OR end_reason NOT IN ({placeholders}))")
|
||||
params.extend(exclude_end_reasons)
|
||||
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
params.extend([limit, offset])
|
||||
|
||||
@ -128,19 +128,46 @@ class SessionStore:
|
||||
self._conn.executescript(SCHEMA_SQL)
|
||||
try:
|
||||
self._conn.execute("SELECT * FROM messages_fts LIMIT 0")
|
||||
except sqlite3.OperationalError:
|
||||
self._conn.executescript(FTS_TABLE_SQL)
|
||||
self._conn.executescript(FTS_TRIGGER_SQL)
|
||||
self._conn.executescript(FTS_TRIGGER_SQL)
|
||||
except sqlite3.Error:
|
||||
self._rebuild_fts_index()
|
||||
return
|
||||
# 旧版本可能把 hidden 事件也写进了 FTS;初始化时顺手清掉这些噪声项。
|
||||
self._conn.execute(
|
||||
"""
|
||||
INSERT INTO messages_fts(messages_fts, rowid, content)
|
||||
SELECT 'delete', id, content
|
||||
FROM messages
|
||||
WHERE context_visible = 0 AND content IS NOT NULL
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
try:
|
||||
self._conn.execute(
|
||||
"""
|
||||
INSERT INTO messages_fts(messages_fts, rowid, content)
|
||||
SELECT 'delete', id, content
|
||||
FROM messages
|
||||
WHERE context_visible = 0 AND content IS NOT NULL
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
except sqlite3.Error:
|
||||
self._rebuild_fts_index()
|
||||
|
||||
def _rebuild_fts_index(self) -> None:
|
||||
"""Recreate the derived FTS index without touching canonical session rows."""
|
||||
|
||||
self._conn.executescript(
|
||||
"""
|
||||
DROP TRIGGER IF EXISTS messages_fts_insert;
|
||||
DROP TRIGGER IF EXISTS messages_fts_delete;
|
||||
DROP TRIGGER IF EXISTS messages_fts_update;
|
||||
DROP TABLE IF EXISTS messages_fts;
|
||||
"""
|
||||
)
|
||||
self._conn.executescript(FTS_TABLE_SQL)
|
||||
self._conn.executescript(FTS_TRIGGER_SQL)
|
||||
self._conn.execute(
|
||||
"""
|
||||
INSERT INTO messages_fts(rowid, content)
|
||||
SELECT id, content
|
||||
FROM messages
|
||||
WHERE context_visible = 1 AND content IS NOT NULL
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def close(self) -> None:
|
||||
with self._lock:
|
||||
|
||||
Reference in New Issue
Block a user