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:
@ -55,3 +55,37 @@ class MemoryChannelAdapter:
|
||||
await self.bus.publish_inbound(message)
|
||||
return message
|
||||
|
||||
async def publish_external_text(
|
||||
self,
|
||||
content: str,
|
||||
*,
|
||||
chat_id: str,
|
||||
message_id: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
raw_payload: dict[str, Any] | None = None,
|
||||
user_id: str | None = None,
|
||||
title: str | None = None,
|
||||
) -> InboundMessage:
|
||||
"""Publish an old-style channel payload through the new adapter contract.
|
||||
|
||||
Real platform adapters should keep platform-specific fields here, build
|
||||
a stable Beaver session_id, and pass the normalized InboundMessage to
|
||||
the shared gateway bus.
|
||||
"""
|
||||
|
||||
session_parts = [self.name, chat_id]
|
||||
if thread_id:
|
||||
session_parts.append(thread_id)
|
||||
metadata = {
|
||||
"chat_id": chat_id,
|
||||
"message_id": message_id,
|
||||
"thread_id": thread_id,
|
||||
"raw_channel_payload": raw_payload or {},
|
||||
}
|
||||
return await self.publish_text(
|
||||
content,
|
||||
session_id=":".join(str(part) for part in session_parts if str(part)),
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"""CLI entry for Beaver."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import typer
|
||||
except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only environments
|
||||
@ -27,6 +29,8 @@ except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only env
|
||||
typer = _FallbackTyper() # type: ignore[assignment]
|
||||
|
||||
from beaver.services.agent_service import AgentService
|
||||
from beaver.services.hermes_migration import HermesMigrationService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
|
||||
app = typer.Typer(help="Beaver backend CLI") if hasattr(typer, "Typer") else typer
|
||||
|
||||
@ -55,6 +59,26 @@ def run(
|
||||
typer.echo(result.output_text)
|
||||
|
||||
|
||||
@app.command("migrate-hermes")
|
||||
def migrate_hermes(
|
||||
repo: str = typer.Option(..., "--repo", help="Local checkout of https://github.com/NousResearch/hermes-agent."),
|
||||
workspace: str | None = typer.Option(None, "--workspace", help="Workspace root to import skills into."),
|
||||
manifest: str | None = typer.Option(None, "--manifest", help="Path for hermes_migration_manifest.json."),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Only write the manifest without importing skills."),
|
||||
) -> None:
|
||||
"""Import no-credential Hermes Agent skills and write a manifest."""
|
||||
|
||||
service = AgentService(workspace=workspace)
|
||||
loaded = service.create_loop().boot()
|
||||
store = loaded.skill_spec_store or SkillSpecStore(loaded.workspace)
|
||||
migration = HermesMigrationService(store, manifest_path=Path(manifest) if manifest else None)
|
||||
result = migration.migrate(repo, dry_run=dry_run)
|
||||
typer.echo(
|
||||
f"Hermes migration complete: {len(result['included'])} included, "
|
||||
f"{len(result['skipped'])} skipped."
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Project script entrypoint."""
|
||||
app()
|
||||
|
||||
192
app-instance/backend/beaver/interfaces/mcp/tools_server.py
Normal file
192
app-instance/backend/beaver/interfaces/mcp/tools_server.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Beaver local tools as real stdio MCP servers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.server.lowlevel.server import NotificationOptions
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
from beaver.engine.session import SessionManager
|
||||
from beaver.memory.curated.store import MemoryStore
|
||||
from beaver.services.cron_service import CronService
|
||||
from beaver.skills import SkillsLoader
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
from beaver.tools.base import BaseTool, ObjectBackedTool, ToolContext
|
||||
from beaver.tools.builtins import (
|
||||
ClarifyTool,
|
||||
CronTool,
|
||||
DelegateTool,
|
||||
ExecuteCodeTool,
|
||||
ListDirectoryTool,
|
||||
MemoryTool,
|
||||
PatchFileTool,
|
||||
ProcessTool,
|
||||
ReadFileTool,
|
||||
SearchFilesTool,
|
||||
SendMessageTool,
|
||||
SkillManageTool,
|
||||
SkillViewTool,
|
||||
SkillsListTool,
|
||||
SpawnTool,
|
||||
TerminalTool,
|
||||
TodoTool,
|
||||
WebFetchTool,
|
||||
WebSearchTool,
|
||||
WriteFileTool,
|
||||
)
|
||||
|
||||
|
||||
LOCAL_TOOL_CATEGORIES = {
|
||||
"filesystem": "Beaver Local Filesystem Tools",
|
||||
"runtime": "Beaver Local Runtime Tools",
|
||||
"memory": "Beaver Local Memory Tools",
|
||||
"skills": "Beaver Local Skills Tools",
|
||||
"coordination": "Beaver Local Coordination Tools",
|
||||
"scheduler": "Beaver Local Scheduler Tools",
|
||||
"web": "Beaver Local Web Tools",
|
||||
}
|
||||
|
||||
|
||||
def _workspace_path(value: str | None = None) -> Path:
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE") or os.getenv("NANOBOT_WORKSPACE")
|
||||
if raw:
|
||||
return Path(raw).expanduser().resolve()
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def _json_content(value: str) -> dict[str, Any]:
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {"success": True, "result": parsed}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "content": value}
|
||||
|
||||
|
||||
def _category_tools(category: str, workspace: Path) -> tuple[list[BaseTool], ToolContext]:
|
||||
skill_store = SkillSpecStore(workspace)
|
||||
skills_loader = SkillsLoader(workspace, skill_store=skill_store)
|
||||
draft_service = DraftService(skill_store)
|
||||
services = {
|
||||
"skills_loader": skills_loader,
|
||||
"draft_service": draft_service,
|
||||
}
|
||||
context = ToolContext(workspace=str(workspace), services=services)
|
||||
|
||||
if category == "filesystem":
|
||||
tools: list[BaseTool] = [
|
||||
ObjectBackedTool(ListDirectoryTool()),
|
||||
ObjectBackedTool(ReadFileTool()),
|
||||
ObjectBackedTool(SearchFilesTool()),
|
||||
ObjectBackedTool(WriteFileTool()),
|
||||
ObjectBackedTool(PatchFileTool()),
|
||||
]
|
||||
elif category == "runtime":
|
||||
tools = [
|
||||
ObjectBackedTool(TerminalTool()),
|
||||
ObjectBackedTool(ProcessTool()),
|
||||
ObjectBackedTool(ExecuteCodeTool()),
|
||||
]
|
||||
elif category == "memory":
|
||||
session_manager = SessionManager(workspace)
|
||||
memory_store = MemoryStore(workspace / "memory" / "curated")
|
||||
memory_store.load_from_disk()
|
||||
tools = [
|
||||
ObjectBackedTool(MemoryTool(store=memory_store)),
|
||||
ObjectBackedTool(__import__("beaver.tools.builtins.session_search", fromlist=["SessionSearchTool"]).SessionSearchTool(db=session_manager)),
|
||||
]
|
||||
elif category == "skills":
|
||||
tools = [
|
||||
ObjectBackedTool(SkillViewTool(loader=skills_loader)),
|
||||
SkillsListTool(),
|
||||
SkillManageTool(),
|
||||
]
|
||||
elif category == "coordination":
|
||||
tools = [
|
||||
ObjectBackedTool(TodoTool()),
|
||||
ObjectBackedTool(ClarifyTool()),
|
||||
ObjectBackedTool(DelegateTool()),
|
||||
ObjectBackedTool(SpawnTool()),
|
||||
ObjectBackedTool(SendMessageTool()),
|
||||
]
|
||||
elif category == "scheduler":
|
||||
services["cron_service"] = CronService(workspace / "cron" / "jobs.json")
|
||||
tools = [CronTool()]
|
||||
elif category == "web":
|
||||
tools = [
|
||||
ObjectBackedTool(WebFetchTool()),
|
||||
ObjectBackedTool(WebSearchTool()),
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unknown local tool category: {category}")
|
||||
return tools, context
|
||||
|
||||
|
||||
def create_tools_server(*, category: str, workspace: str | None = None) -> Server:
|
||||
workspace_path = _workspace_path(workspace)
|
||||
tools, context = _category_tools(category, workspace_path)
|
||||
tool_map = {tool.spec.name: tool for tool in tools}
|
||||
server = Server(LOCAL_TOOL_CATEGORIES.get(category, f"Beaver Local {category} Tools"))
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
types.Tool(
|
||||
name=tool.spec.name,
|
||||
description=tool.spec.description,
|
||||
inputSchema=tool.spec.input_schema,
|
||||
)
|
||||
for tool in tools
|
||||
]
|
||||
|
||||
@server.call_tool(validate_input=True)
|
||||
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
tool = tool_map.get(name)
|
||||
if tool is None:
|
||||
return {"success": False, "error": f"Unknown tool: {name}"}
|
||||
result = await tool.invoke(arguments or {}, context)
|
||||
if result.raw_output is not None and isinstance(result.raw_output, dict):
|
||||
return result.raw_output
|
||||
payload = _json_content(result.content)
|
||||
if "success" not in payload:
|
||||
payload["success"] = bool(result.success)
|
||||
if result.error and "error" not in payload:
|
||||
payload["error"] = result.error
|
||||
return payload
|
||||
|
||||
return server
|
||||
|
||||
|
||||
async def _run_stdio(category: str, workspace: str | None) -> None:
|
||||
server = create_tools_server(category=category, workspace=workspace)
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name=LOCAL_TOOL_CATEGORIES.get(category, f"beaver-{category}"),
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={}),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run a Beaver local tool category as a stdio MCP server.")
|
||||
parser.add_argument("--category", choices=sorted(LOCAL_TOOL_CATEGORIES), required=True)
|
||||
parser.add_argument("--workspace", default=None)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(_run_stdio(args.category, args.workspace))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@ -60,10 +60,13 @@ class WebChatRequest(BaseModel):
|
||||
embedding_model: str | None = None
|
||||
temperature: float | None = None
|
||||
max_tokens: int | None = None
|
||||
thinking_enabled: bool | None = None
|
||||
max_tool_iterations: int | None = None
|
||||
fallback_target: WebProviderTarget | None = None
|
||||
auxiliary_target: WebProviderTarget | None = None
|
||||
embedding_target: WebProviderTarget | None = None
|
||||
reply_to_scheduled_run_id: str | None = None
|
||||
scheduled_reply_intent: str | None = None
|
||||
|
||||
|
||||
class WebChatResponse(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user