refactor(beaver): 移除Hermes相关引用和迁移代码,完善Beaver后端主线实现
移除了所有Hermes相关的命名引用,包括: - 从.gitignore中清理相关构建缓存文件 - 将README中的beaver-home路径配置更新 - 完善backend/README.md文档说明Beaver后端主线实现 - 移除Hermes风格的相关注释和兼容性代码 - 清理nanobot环境变量兼容性处理 - 删除技能迁移和服务迁移相关功能代码 - 更新测试用例中相关命名和函数名 BREAKING CHANGE: 移除了Hermes迁移相关API和CLI命令,不再支持nanobot环境变量兼容性
This commit is contained in:
@ -1,8 +1,6 @@
|
||||
# Beaver Backend
|
||||
|
||||
这是新的 `Beaver` 后端。
|
||||
|
||||
旧实现已保留在 [backend-old](/home/ivan/xuan/nano_project/app-instance/backend-old),新目录用于按 [change.md](/home/ivan/xuan/nano_project/app-instance/backend/change.md) 的蓝图逐步重建后端。
|
||||
这是 `Beaver` 后端。
|
||||
|
||||
当前已经落地的主线:
|
||||
|
||||
@ -28,11 +26,4 @@
|
||||
|
||||
## 说明
|
||||
|
||||
这个目录已经不是空骨架,但仍不等于完成迁移。
|
||||
|
||||
后续迁移原则:
|
||||
|
||||
1. 不再新增 `nanobot` 命名。
|
||||
2. 不在新目录中保留 `third_party/`。
|
||||
3. 所有 agent 最终都复用 `beaver.engine`。
|
||||
4. 高级 team 策略先编译成 Beaver 自有 `ExecutionGraph`,不直接暴露 swarms runtime。
|
||||
后端已切到 Beaver 主线,不再保留旧实现、vendored 第三方 runtime 或迁移期旧命名兼容入口。所有 agent 运行都复用 `beaver.engine`,多 agent 协调通过 Beaver 自有 coordinator 和 `ExecutionGraph` 表达。
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
1. 先服务单 agent 主链
|
||||
2. 先支持 frozen curated memory,而不是 live memory
|
||||
3. skills 按 Hermes 风格支持“显式激活消息注入”,不在这里做磁盘扫描
|
||||
3. skills 通过显式激活消息注入,不在这里做磁盘扫描
|
||||
4. 为后续 channel / gateway / team metadata 预留注入位,但不提前做复杂逻辑
|
||||
"""
|
||||
|
||||
@ -45,7 +45,7 @@ class SkillContext:
|
||||
- `name`:用于生成激活提示
|
||||
- `content`:skill 的完整正文
|
||||
|
||||
注意:按当前 Hermes 风格实现,skill 正文不再塞进 system prompt,而是转成显式消息注入。
|
||||
注意:skill 正文不再塞进 system prompt,而是转成显式消息注入。
|
||||
"""
|
||||
|
||||
name: str
|
||||
@ -151,7 +151,7 @@ class ContextBuilder:
|
||||
- 身份与总规则要最靠前
|
||||
- session/execution 是本轮运行语境,优先级高于长期记忆
|
||||
- memory 必须是 frozen snapshot,避免中途写 memory 后 prompt 失真
|
||||
- activated skill 正文按 Hermes 风格放到显式消息里,避免 system prompt 持续膨胀
|
||||
- activated skill 正文放到显式消息里,避免 system prompt 持续膨胀
|
||||
"""
|
||||
|
||||
sections: list[str] = [BEAVER_USER_ASSISTANT_IDENTITY_PROMPT]
|
||||
@ -190,7 +190,7 @@ class ContextBuilder:
|
||||
|
||||
这里做三件事:
|
||||
1. 先生成最终 system prompt
|
||||
2. 按 Hermes 风格,把已激活 skill 的完整正文作为显式消息注入
|
||||
2. 把已激活 skill 的完整正文作为显式消息注入
|
||||
3. 把历史消息按原顺序接到后面
|
||||
4. 如果存在当前用户输入,则把本轮输入追加为最后一条 user message
|
||||
|
||||
@ -348,7 +348,7 @@ class ContextBuilder:
|
||||
return "# Current Session\n\n" + "\n".join(rows)
|
||||
|
||||
def build_skill_activation_messages(self, activated_skills: list[SkillContext]) -> list[dict[str, str]]:
|
||||
"""按 Hermes 风格把已激活 skill 转成显式消息。
|
||||
"""把已激活 skill 转成显式消息。
|
||||
|
||||
关键区别:
|
||||
- system prompt 只保留轻量 skills index
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Hermes 风格的 provider runtime resolution。"""
|
||||
"""Provider runtime resolution for Beaver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -165,7 +165,7 @@ def resolve_fallback_runtime(
|
||||
) -> ProviderRuntime | None:
|
||||
"""把 fallback 配置解析成独立 runtime。
|
||||
|
||||
Hermes 的 fallback 是“主 provider 失败后切换到另一个 provider:model”。
|
||||
fallback 的语义是“主 provider 失败后切换到另一个 provider:model”。
|
||||
这里先把 fallback 解析独立出来,具体何时激活交给上层 chain/factory。
|
||||
"""
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver session 子系统的 SQLite 存储实现。
|
||||
|
||||
设计来源主要参考 Hermes-agent:
|
||||
设计目标:
|
||||
1. SQLite 作为统一 session/transcript backend
|
||||
2. WAL 模式支持多读单写
|
||||
3. FTS5 支持跨 session 文本检索
|
||||
|
||||
@ -35,14 +35,12 @@ def default_config_path(*, workspace: str | Path | None = None) -> Path:
|
||||
|
||||
Priority:
|
||||
1. `BEAVER_CONFIG_PATH`
|
||||
2. `NANOBOT_CONFIG_PATH` for compatibility during migration
|
||||
3. `BEAVER_HOME/config.json`
|
||||
4. `NANOBOT_HOME/config.json` for migration compatibility
|
||||
5. `<workspace>/.beaver/config.json`
|
||||
6. `./.beaver/config.json`
|
||||
2. `BEAVER_HOME/config.json`
|
||||
3. `<workspace>/.beaver/config.json`
|
||||
4. `./.beaver/config.json`
|
||||
"""
|
||||
|
||||
explicit = os.getenv("BEAVER_CONFIG_PATH") or os.getenv("NANOBOT_CONFIG_PATH")
|
||||
explicit = os.getenv("BEAVER_CONFIG_PATH")
|
||||
if explicit:
|
||||
return Path(explicit).expanduser()
|
||||
|
||||
@ -50,10 +48,6 @@ def default_config_path(*, workspace: str | Path | None = None) -> Path:
|
||||
if beaver_home:
|
||||
return Path(beaver_home).expanduser() / "config.json"
|
||||
|
||||
nanobot_home = os.getenv("NANOBOT_HOME")
|
||||
if nanobot_home:
|
||||
return Path(nanobot_home).expanduser() / "config.json"
|
||||
|
||||
root = Path(workspace).expanduser() if workspace is not None else Path.cwd()
|
||||
return root / ".beaver" / "config.json"
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
"""Scheduled task models for Beaver cron.
|
||||
|
||||
The scheduler borrows Hermes' durable JSON + explicit schedule parsing shape,
|
||||
but the execution target is Beaver Task mode: every trigger creates a normal
|
||||
Task run instead of a detached agent turn.
|
||||
Every trigger targets Beaver Task mode so scheduled work remains visible as a
|
||||
normal Task instead of a detached agent turn.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -19,7 +19,7 @@ from beaver.foundation.config import BeaverConfig
|
||||
from beaver.integrations.authz import AuthzClient
|
||||
|
||||
|
||||
OUTLOOK_SERVER_ID = os.getenv("BEAVER_OUTLOOK_MCP_SERVER_ID") or os.getenv("NANOBOT_OUTLOOK_MCP_SERVER_ID", "outlook_mcp")
|
||||
OUTLOOK_SERVER_ID = os.getenv("BEAVER_OUTLOOK_MCP_SERVER_ID", "outlook_mcp")
|
||||
OUTLOOK_OVERVIEW_MESSAGE_LIMIT = 8
|
||||
OUTLOOK_OVERVIEW_EVENT_LIMIT = 20
|
||||
OUTLOOK_MAX_PAGE_SIZE = 100
|
||||
@ -31,11 +31,11 @@ class OutlookIntegrationError(RuntimeError):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutlookDefaults:
|
||||
domain: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_DOMAIN", "")
|
||||
service_endpoint: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_EWS_URL", "")
|
||||
server: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_EWS_SERVER", "")
|
||||
default_timezone: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_TIMEZONE", "Asia/Shanghai")
|
||||
autodiscover: bool = os.getenv("NANOBOT_OUTLOOK_DEFAULT_AUTODISCOVER", "0") == "1"
|
||||
domain: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_DOMAIN", "")
|
||||
service_endpoint: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_URL", "")
|
||||
server: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_SERVER", "")
|
||||
default_timezone: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_TIMEZONE", "Asia/Shanghai")
|
||||
autodiscover: bool = os.getenv("BEAVER_OUTLOOK_DEFAULT_AUTODISCOVER", "0") == "1"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -71,7 +71,7 @@ OUTLOOK_TOOL_NAMES = [
|
||||
|
||||
|
||||
def _call_timeout_seconds() -> float:
|
||||
raw = os.getenv("NANOBOT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "").strip()
|
||||
raw = os.getenv("BEAVER_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "").strip()
|
||||
try:
|
||||
return max(1.0, float(raw)) if raw else 10.0
|
||||
except ValueError:
|
||||
@ -108,8 +108,8 @@ def outlook_defaults() -> dict[str, Any]:
|
||||
return {
|
||||
"provider": "ews",
|
||||
"server_id": OUTLOOK_SERVER_ID,
|
||||
"mcp_command": os.getenv("NANOBOT_OUTLOOK_MCP_COMMAND", "bw-outlook-mcp"),
|
||||
"mcp_extra_args": shlex.split(os.getenv("NANOBOT_OUTLOOK_MCP_EXTRA_ARGS", "").strip()),
|
||||
"mcp_command": os.getenv("BEAVER_OUTLOOK_MCP_COMMAND", "bw-outlook-mcp"),
|
||||
"mcp_extra_args": shlex.split(os.getenv("BEAVER_OUTLOOK_MCP_EXTRA_ARGS", "").strip()),
|
||||
"fields": asdict(OutlookDefaults()),
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
"""CLI entry for Beaver."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import typer
|
||||
except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only environments
|
||||
@ -29,8 +27,6 @@ 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
|
||||
|
||||
@ -59,26 +55,6 @@ 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()
|
||||
|
||||
@ -58,7 +58,7 @@ LOCAL_TOOL_CATEGORIES = {
|
||||
|
||||
|
||||
def _workspace_path(value: str | None = None) -> Path:
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE") or os.getenv("NANOBOT_WORKSPACE")
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE")
|
||||
if raw:
|
||||
return Path(raw).expanduser().resolve()
|
||||
return Path.cwd()
|
||||
|
||||
@ -23,7 +23,6 @@ from beaver.foundation.models import CronExecutionResult, CronRunRecord
|
||||
from beaver.integrations.mcp import MCPConnectionManager
|
||||
from beaver.services.agent_service import NOTIFICATION_SESSION_ID, AgentService
|
||||
from beaver.services.cron_service import CronService, schedule_from_api
|
||||
from beaver.services.skill_migration import SkillMigrationService
|
||||
from beaver.services.skillhub_service import SkillHubService
|
||||
from beaver.skills.learning import SkillLearningWorker, SkillLearningWorkerConfig
|
||||
from beaver.skills.catalog.utils import parse_frontmatter
|
||||
@ -305,7 +304,7 @@ def create_app(
|
||||
)
|
||||
app.state.auth_tokens = {}
|
||||
app.state.handoff_codes = {}
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or os.getenv("NANOBOT_AUTH_FILE") or "")
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or "")
|
||||
max_file_size = 50 * 1024 * 1024
|
||||
|
||||
@app.get("/api/ping", response_model=WebStatusResponse)
|
||||
@ -427,7 +426,6 @@ def create_app(
|
||||
_clean_text(payload.get("base_url"))
|
||||
or config.backend_identity.public_base_url
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL")
|
||||
or str(request.base_url).rstrip("/")
|
||||
)
|
||||
frontend_base_url = _clean_text(payload.get("frontend_base_url")) or public_base_url
|
||||
@ -526,7 +524,7 @@ def create_app(
|
||||
return {
|
||||
"id": username,
|
||||
"username": username,
|
||||
"email": os.getenv("BEAVER_BACKEND_IDENTITY__EMAIL") or os.getenv("NANOBOT_BACKEND_IDENTITY__EMAIL", ""),
|
||||
"email": os.getenv("BEAVER_BACKEND_IDENTITY__EMAIL", ""),
|
||||
"role": "owner",
|
||||
"quota_tier": "single-user",
|
||||
}
|
||||
@ -1184,30 +1182,6 @@ def create_app(
|
||||
)
|
||||
return result
|
||||
|
||||
@app.get("/api/tools/servers")
|
||||
async def list_tool_servers(request: Request) -> list[dict[str, Any]]:
|
||||
return await list_mcp_servers(request)
|
||||
|
||||
@app.get("/api/tools")
|
||||
async def list_tools(request: Request) -> dict[str, Any]:
|
||||
servers = await list_mcp_servers(request)
|
||||
tool_groups = await list_mcp_tools(request)
|
||||
server_map = {server["id"]: server for server in servers}
|
||||
grouped = {"local": [], "online": []}
|
||||
for group in tool_groups:
|
||||
server = server_map.get(group["server_id"], {})
|
||||
kind = str(server.get("kind") or "online")
|
||||
item = {
|
||||
**group,
|
||||
"server_name": server.get("name") or group["server_id"],
|
||||
"transport": server.get("transport"),
|
||||
"kind": kind,
|
||||
"category": server.get("category") or kind,
|
||||
"status": server.get("status"),
|
||||
}
|
||||
grouped["local" if kind == "local" else "online"].append(item)
|
||||
return {"servers": servers, "groups": grouped}
|
||||
|
||||
@app.get("/api/skills")
|
||||
async def list_skills(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
@ -1301,19 +1275,6 @@ def create_app(
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return draft
|
||||
|
||||
@app.post("/api/skills/migrate")
|
||||
async def migrate_skills(request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
return SkillMigrationService(loaded.skill_spec_store).migrate_all() # type: ignore[arg-type]
|
||||
|
||||
@app.get("/api/skills/migration-manifest")
|
||||
async def get_skill_migration_manifest(request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
path = loaded.workspace / "skill_migration_manifest.json"
|
||||
if not path.exists():
|
||||
return {"included": [], "skipped": []}
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
@app.get("/api/marketplaces/skills/search")
|
||||
async def search_skillhub(
|
||||
request: Request,
|
||||
@ -2482,7 +2443,7 @@ def _provider_enabled(provider_name: str, provider_cfg: Any) -> bool:
|
||||
|
||||
|
||||
def _auth_file_path() -> Path:
|
||||
raw = os.getenv("BEAVER_AUTH_FILE") or os.getenv("NANOBOT_AUTH_FILE")
|
||||
raw = os.getenv("BEAVER_AUTH_FILE")
|
||||
if raw:
|
||||
return Path(raw)
|
||||
return Path.home() / ".beaver" / "web_auth_users.json"
|
||||
@ -2542,7 +2503,7 @@ def _issue_web_token(app: FastAPI, username: str) -> str:
|
||||
|
||||
|
||||
def _handoff_ttl_seconds() -> int:
|
||||
raw = os.getenv("NANOBOT_HANDOFF_CODE_TTL_SECONDS", "90").strip()
|
||||
raw = os.getenv("BEAVER_HANDOFF_CODE_TTL_SECONDS", "90").strip()
|
||||
try:
|
||||
return max(15, int(raw))
|
||||
except ValueError:
|
||||
@ -2550,7 +2511,7 @@ def _handoff_ttl_seconds() -> int:
|
||||
|
||||
|
||||
def _handoff_replay_window_seconds() -> int:
|
||||
raw = os.getenv("NANOBOT_HANDOFF_REPLAY_WINDOW_SECONDS", "15").strip()
|
||||
raw = os.getenv("BEAVER_HANDOFF_REPLAY_WINDOW_SECONDS", "15").strip()
|
||||
try:
|
||||
return max(1, int(raw))
|
||||
except ValueError:
|
||||
@ -2637,22 +2598,18 @@ def _require_web_user(app: FastAPI, authorization: str | None) -> str:
|
||||
def _backend_connection_view(request: Request) -> dict[str, Any]:
|
||||
public_base_url = (
|
||||
os.getenv("BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL")
|
||||
or str(request.base_url).rstrip("/")
|
||||
)
|
||||
backend_id = (
|
||||
os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__BACKEND_ID")
|
||||
or os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID")
|
||||
)
|
||||
client_id = os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID") or backend_id
|
||||
client_id = os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or backend_id
|
||||
return {
|
||||
"backend_id": backend_id,
|
||||
"client_id": client_id,
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or os.getenv("NANOBOT_BACKEND_IDENTITY__NAME") or backend_id,
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or backend_id,
|
||||
"public_base_url": public_base_url,
|
||||
"api_base_url": public_base_url,
|
||||
"frontend_base_url": public_base_url,
|
||||
@ -2663,16 +2620,14 @@ def _backend_connection_view(request: Request) -> dict[str, Any]:
|
||||
|
||||
def _local_backend_view() -> dict[str, Any]:
|
||||
return {
|
||||
"backend_id": os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__BACKEND_ID"),
|
||||
"client_id": os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID"),
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or os.getenv("NANOBOT_BACKEND_IDENTITY__NAME"),
|
||||
"backend_id": os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID"),
|
||||
"client_id": os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID"),
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME"),
|
||||
"public_base_url": os.getenv("BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL"),
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL"),
|
||||
"authz": {
|
||||
"enabled": (os.getenv("BEAVER_AUTHZ__ENABLED") or os.getenv("NANOBOT_AUTHZ__ENABLED", "")).strip() in {"1", "true", "True"},
|
||||
"base_url": os.getenv("BEAVER_AUTHZ__BASE_URL") or os.getenv("NANOBOT_AUTHZ__BASE_URL"),
|
||||
"enabled": os.getenv("BEAVER_AUTHZ__ENABLED", "").strip() in {"1", "true", "True"},
|
||||
"base_url": os.getenv("BEAVER_AUTHZ__BASE_URL"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 的精炼长期记忆存储层。
|
||||
|
||||
这个文件实现的是以 Hermes-agent 为基线的 curated memory 模型,目标不是
|
||||
这个文件实现的是 Beaver curated memory 模型,目标不是
|
||||
“把所有历史都存下来”,而是只保存跨会话仍然值得保留的稳定事实。
|
||||
|
||||
核心设计:
|
||||
|
||||
@ -38,10 +38,9 @@ _MAX_HISTORY = 20
|
||||
class CronService:
|
||||
"""Persistent single-timer scheduler.
|
||||
|
||||
Hermes' cron implementation stores jobs as JSON and ticks safely in the
|
||||
background. Beaver keeps that shape, but the callback is required to route
|
||||
agent work through Task mode so every scheduled trigger is visible as a
|
||||
normal Task.
|
||||
Jobs are stored as JSON and ticked safely in the background. The callback
|
||||
routes agent work through Task mode so every scheduled trigger is visible as
|
||||
a normal Task.
|
||||
"""
|
||||
|
||||
def __init__(self, store_path: str | Path, *, on_job: CronCallback | None = None) -> None:
|
||||
|
||||
@ -43,7 +43,7 @@ class MemoryService:
|
||||
def reload_for_new_run(self) -> None:
|
||||
"""每次新 run 开始前刷新 live state。
|
||||
|
||||
这是 Hermes 风格 memory policy 的关键点:
|
||||
这是 Beaver memory policy 的关键点:
|
||||
- 上一次会话中通过 tool 写入的持久记忆,下一次运行应该能看到
|
||||
- 但同一次 run 中途写入的新记忆,不应反向修改当前 frozen snapshot
|
||||
"""
|
||||
|
||||
@ -237,7 +237,7 @@ class SkillsLoader:
|
||||
def build_skills_summary(self) -> str:
|
||||
"""构建可注入 system prompt 的 skills index。
|
||||
|
||||
虽然函数名还沿用 `summary`,但当前语义已经更接近 Hermes 的 skills index:
|
||||
虽然函数名还沿用 `summary`,但当前语义是轻量 skills index:
|
||||
- 这里只告诉模型“系统里有哪些 skill 可用”
|
||||
- 不负责把 skill 正文塞进 system prompt
|
||||
- 真正激活的 skill 正文由 resolver/builder 走显式消息注入
|
||||
|
||||
@ -87,9 +87,7 @@ def strip_frontmatter(content: str) -> str:
|
||||
def parse_skill_metadata_blob(raw: str) -> dict[str, Any]:
|
||||
"""解析 metadata 字段里的 JSON 扩展配置。
|
||||
|
||||
为了兼容旧 nanobot 习惯,这里同时支持:
|
||||
- `nanobot`
|
||||
- `openclaw`
|
||||
Supports plain metadata objects and the current `openclaw` namespace.
|
||||
|
||||
第一版主要关心的字段有:
|
||||
- `always`
|
||||
@ -103,7 +101,7 @@ def parse_skill_metadata_blob(raw: str) -> dict[str, Any]:
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
nested = data.get("nanobot", data.get("openclaw", data))
|
||||
nested = data.get("openclaw", data)
|
||||
return nested if isinstance(nested, dict) else {}
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
import inspect
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
@ -181,9 +182,21 @@ class ObjectBackedTool(BaseTool):
|
||||
arguments["current_session_id"] = context.session_id
|
||||
if "workspace" not in arguments and hasattr(self.backend, "workspace"):
|
||||
arguments["workspace"] = context.workspace
|
||||
if "metadata" not in arguments:
|
||||
if "metadata" not in arguments and self._backend_accepts_argument("metadata"):
|
||||
arguments["metadata"] = context.metadata
|
||||
|
||||
def _backend_accepts_argument(self, name: str) -> bool:
|
||||
try:
|
||||
signature = inspect.signature(self.backend.execute)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
for parameter in signature.parameters.values():
|
||||
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
return True
|
||||
if parameter.name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _normalize_output(content: Any) -> dict[str, Any]:
|
||||
"""把后端工具返回值转成统一 success/content/error 语义。
|
||||
|
||||
@ -33,7 +33,7 @@ CRON_TOOL_PARAMETERS: dict[str, Any] = {
|
||||
},
|
||||
"schedule": {
|
||||
"type": "string",
|
||||
"description": "Hermes-style schedule, for example 'every 15m', '0 9 * * *', or an ISO datetime.",
|
||||
"description": "Schedule expression, for example 'every 15m', '0 9 * * *', or an ISO datetime.",
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@ -59,7 +59,7 @@ def memory_tool(
|
||||
old_text: str | None = None,
|
||||
store: MemoryStore | None = None,
|
||||
) -> str:
|
||||
"""分发 Hermes 风格的 CRUD memory API,并返回 JSON 字符串。
|
||||
"""分发 CRUD memory API,并返回 JSON 字符串。
|
||||
|
||||
这里统一采用 JSON 返回,是为了兼容常见 tool-calling 场景:
|
||||
- LLM 更容易消费结构化结果
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 内置 session_search tool。
|
||||
|
||||
这个工具对应 Hermes-agent 的跨会话检索能力,目标不是把所有历史内容塞回主上下文,
|
||||
这个工具提供跨会话检索能力,目标不是把所有历史内容塞回主上下文,
|
||||
而是按需从过去的 session 中找回“之前发生过什么”。
|
||||
|
||||
当前实现保留了几个关键行为:
|
||||
@ -28,7 +28,7 @@ class SessionSearchDB(Protocol):
|
||||
"""session_search 依赖的最小数据库契约。
|
||||
|
||||
这里没有直接绑定某个具体 SQLite 实现,而是先定义行为接口。
|
||||
这样后面无论你接的是 Hermes 风格 state DB、还是 Beaver 自己的 transcript store,
|
||||
这样后面无论你接的是当前 SQLite state DB、还是其他 transcript store,
|
||||
只要满足这些方法就能工作。
|
||||
"""
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 内置 skill_view tool。
|
||||
|
||||
这个工具对应 Hermes 风格的显式 skill loading path:
|
||||
这个工具对应显式 skill loading path:
|
||||
1. skill 正文默认不会长期塞进 system prompt
|
||||
2. 模型若想查看某个 skill 的完整正文或支持文件,必须显式调用 `skill_view`
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from beaver.engine import AgentLoop, EngineLoader
|
||||
from beaver.engine.providers import make_provider_bundle
|
||||
@ -139,9 +138,36 @@ def test_openai_compatible_qwen_config_keeps_openai_provider() -> None:
|
||||
assert bundle.main_provider._resolve_model("qwen-plus") == "openai/qwen-plus"
|
||||
|
||||
|
||||
def test_load_config_reads_stevenli_mcp_authz_identity() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
config_path = repo_root / "app-instance" / "runtime" / "instances" / "stevenli" / "nanobot-home" / "config.json"
|
||||
def test_load_config_reads_mcp_authz_identity(tmp_path) -> None:
|
||||
config_path = tmp_path / "beaver-home" / "config.json"
|
||||
config_path.parent.mkdir()
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"tools": {
|
||||
"mcpServers": {
|
||||
"outlook_mcp": {
|
||||
"url": "http://10.6.80.29:8000/mcp",
|
||||
"authMode": "oauth_backend_token",
|
||||
"authAudience": "mcp:outlook_mcp",
|
||||
"authScopes": ["list_tools", "tool:mail_list_messages"],
|
||||
"toolTimeout": 60,
|
||||
"sensitive": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
"authz": {
|
||||
"enabled": True,
|
||||
"baseUrl": "http://nano-authz-service:19090",
|
||||
},
|
||||
"backend_identity": {
|
||||
"backend_id": "stevenli",
|
||||
"client_id": "stevenli",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = load_config(config_path=config_path)
|
||||
|
||||
server = config.tools.mcp_servers["outlook_mcp"]
|
||||
|
||||
@ -6,7 +6,7 @@ from beaver.tools.builtins import CronTool
|
||||
from beaver.services.cron_service import CronService, compute_next_run, parse_schedule, schedule_from_api
|
||||
|
||||
|
||||
def test_parse_hermes_style_schedules() -> None:
|
||||
def test_parse_schedule_expressions() -> None:
|
||||
interval = parse_schedule("every 15m")
|
||||
assert interval.kind == "every"
|
||||
assert interval.every_ms == 15 * 60 * 1000
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from beaver.interfaces.web.app import _create_skill_upload_draft
|
||||
from beaver.services.hermes_migration import HermesMigrationService
|
||||
from beaver.services.skillhub_service import SkillHubService
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
@ -101,27 +99,6 @@ def test_upload_skill_zip_keeps_supporting_files_on_draft(tmp_path):
|
||||
assert upload_dir.endswith(draft["draft_id"])
|
||||
|
||||
|
||||
def test_hermes_migration_manifest_includes_no_credential_skill_and_skips_api_skill(tmp_path):
|
||||
repo = tmp_path / "hermes"
|
||||
safe = repo / "skills" / "safe"
|
||||
unsafe = repo / "skills" / "unsafe"
|
||||
safe.mkdir(parents=True)
|
||||
unsafe.mkdir(parents=True)
|
||||
safe.joinpath("SKILL.md").write_text("---\nname: safe\n---\nUse local files only.\n", encoding="utf-8")
|
||||
unsafe.joinpath("SKILL.md").write_text("---\nname: unsafe\n---\nRequires API_KEY.\n", encoding="utf-8")
|
||||
|
||||
store = SkillSpecStore(tmp_path / "workspace")
|
||||
manifest = HermesMigrationService(store).migrate(repo)
|
||||
included = {item["skill_name"] for item in manifest["included"]}
|
||||
skipped = {item.get("skill_name"): item["reason"] for item in manifest["skipped"]}
|
||||
|
||||
assert "safe" in included
|
||||
assert skipped["unsafe"] == "requires_external_credentials"
|
||||
assert store.get_skill_spec("safe") is not None
|
||||
manifest_path = tmp_path / "workspace" / "hermes_migration_manifest.json"
|
||||
assert json.loads(manifest_path.read_text(encoding="utf-8"))["source"] == "hermes-agent"
|
||||
|
||||
|
||||
def test_mcp_wrapper_metadata_preserves_server_id_with_underscores():
|
||||
tool_def = SimpleNamespace(name="auth_status", description="Auth", inputSchema={"type": "object", "properties": {}})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user