feat: 将项目从nano重命名为beaver并更新相关配置
- 将所有环境变量前缀从NANO_改为BEAVER_ - 更新README.md文档内容,包括项目介绍、组件说明和快速开始指南 - 修改.gitignore文件,添加auth-portal运行时路径排除规则 - 更新app-instance镜像标签从nano/app-instance改为beaver/app-instance - 增强技能安全检查器,支持工具前缀白名单功能 - 添加技能草稿重新检查安全性API端点 - 扩展证据选择器,收集工具调用名称用于技能学习 - 改进技能合成器,基于实际调用的工具生成工具提示 - 优化路由超时处理机制,增加重试逻辑 - 更新后端架构文档,添加可视化入口和基础概念说明 - 实现在WebSocket消息中传递工具迭代次数信息
This commit is contained in:
@ -42,6 +42,8 @@ class EvidenceSelector:
|
||||
resolved_session_ids: list[str] = list(dict.fromkeys(session_ids or []))
|
||||
task_summaries: list[str] = []
|
||||
session_excerpts: list[str] = []
|
||||
tool_names: list[str] = []
|
||||
selected_tool_names: list[str] = []
|
||||
for run_id in run_ids:
|
||||
record = runs_by_id.get(run_id)
|
||||
if record is None:
|
||||
@ -56,12 +58,19 @@ class EvidenceSelector:
|
||||
excerpt = self._session_excerpt(record.session_id, run_id)
|
||||
if excerpt:
|
||||
session_excerpts.append(excerpt)
|
||||
run_tool_names, run_selected_tool_names = self._run_tool_names(record.session_id, run_id)
|
||||
tool_names.extend(run_tool_names)
|
||||
selected_tool_names.extend(run_selected_tool_names)
|
||||
return EvidencePacket(
|
||||
run_ids=resolved_run_ids,
|
||||
session_ids=resolved_session_ids,
|
||||
task_summaries=task_summaries[:8],
|
||||
session_excerpts=session_excerpts[:6],
|
||||
metadata={"bounded": True},
|
||||
metadata={
|
||||
"bounded": True,
|
||||
"tool_names": _unique_strings(tool_names),
|
||||
"selected_tool_names": _unique_strings(selected_tool_names),
|
||||
},
|
||||
)
|
||||
|
||||
def _session_excerpt(self, session_id: str, run_id: str) -> str:
|
||||
@ -74,3 +83,37 @@ class EvidenceSelector:
|
||||
continue
|
||||
visible.append(f"{event.role}: {event.content.strip()}")
|
||||
return "\n".join(visible[:12])[:2000]
|
||||
|
||||
def _run_tool_names(self, session_id: str, run_id: str) -> tuple[list[str], list[str]]:
|
||||
if self.session_manager is None:
|
||||
return [], []
|
||||
|
||||
names: list[str] = []
|
||||
selected_names: list[str] = []
|
||||
for event in self.session_manager.get_run_event_records(session_id, run_id):
|
||||
if event.tool_name:
|
||||
names.append(event.tool_name)
|
||||
if event.tool_calls:
|
||||
for call in event.tool_calls:
|
||||
if not isinstance(call, dict):
|
||||
continue
|
||||
name = call.get("name")
|
||||
function = call.get("function")
|
||||
if not name and isinstance(function, dict):
|
||||
name = function.get("name")
|
||||
if name:
|
||||
names.append(str(name))
|
||||
if event.event_type == "tool_selection_snapshotted" and isinstance(event.event_payload, dict):
|
||||
selected = event.event_payload.get("tool_names")
|
||||
if isinstance(selected, list):
|
||||
selected_names.extend(str(item) for item in selected if str(item).strip())
|
||||
return _unique_strings(names), _unique_strings(selected_names)
|
||||
|
||||
|
||||
def _unique_strings(values: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for value in values:
|
||||
cleaned = str(value).strip()
|
||||
if cleaned and cleaned not in result:
|
||||
result.append(cleaned)
|
||||
return result
|
||||
|
||||
@ -32,8 +32,14 @@ class SkillDraftSafetyChecker:
|
||||
"credentials",
|
||||
}
|
||||
|
||||
def __init__(self, *, allowed_tool_names: set[str] | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
allowed_tool_names: set[str] | None = None,
|
||||
allowed_tool_prefixes: set[str] | None = None,
|
||||
) -> None:
|
||||
self.allowed_tool_names = allowed_tool_names
|
||||
self.allowed_tool_prefixes = allowed_tool_prefixes or set()
|
||||
|
||||
def check(self, draft: SkillDraft) -> SkillDraftSafetyReport:
|
||||
issues: list[str] = []
|
||||
@ -50,7 +56,7 @@ class SkillDraftSafetyChecker:
|
||||
|
||||
tool_hints = _tool_hints(frontmatter)
|
||||
if self.allowed_tool_names is not None:
|
||||
unknown = [name for name in tool_hints if name not in self.allowed_tool_names]
|
||||
unknown = [name for name in tool_hints if not self._is_allowed_tool_hint(name)]
|
||||
if unknown:
|
||||
blocked.append(f"unknown tool hints: {', '.join(sorted(unknown))}")
|
||||
dangerous = sorted({name for name in tool_hints if name.lower() in self._DANGEROUS_TOOL_HINTS})
|
||||
@ -80,6 +86,11 @@ class SkillDraftSafetyChecker:
|
||||
created_at=_utc_now(),
|
||||
)
|
||||
|
||||
def _is_allowed_tool_hint(self, name: str) -> bool:
|
||||
if self.allowed_tool_names is not None and name in self.allowed_tool_names:
|
||||
return True
|
||||
return any(name.startswith(prefix) and len(name) > len(prefix) for prefix in self.allowed_tool_prefixes)
|
||||
|
||||
|
||||
def _tool_hints(frontmatter: dict) -> list[str]:
|
||||
raw = frontmatter.get("tools")
|
||||
|
||||
@ -65,19 +65,29 @@ class SkillDraftSynthesizer:
|
||||
)
|
||||
payload = self._parse_payload(response.content or "")
|
||||
if payload:
|
||||
return payload
|
||||
return self._normalize_payload(payload, evidence_packet)
|
||||
return self._fallback_payload(candidate, evidence_packet, action)
|
||||
|
||||
@staticmethod
|
||||
def _build_prompt(candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, action: str) -> str:
|
||||
tool_names = _coerce_string_list(evidence_packet.metadata.get("tool_names"))
|
||||
tool_section = ", ".join(tool_names) if tool_names else "none observed"
|
||||
selected_tool_names = _coerce_string_list(evidence_packet.metadata.get("selected_tool_names"))
|
||||
selected_tool_section = ", ".join(selected_tool_names) if selected_tool_names else "none recorded"
|
||||
return (
|
||||
f"Action: {action}\n"
|
||||
f"Candidate kind: {candidate.kind}\n"
|
||||
f"Reason: {candidate.reason}\n"
|
||||
f"Related skills: {candidate.related_skill_names}\n"
|
||||
f"Called tool names: {tool_section}\n"
|
||||
f"Run-selected tool names: {selected_tool_section}\n"
|
||||
f"Task summaries:\n- " + "\n- ".join(evidence_packet.task_summaries)
|
||||
+ "\n\nSession excerpts:\n" + "\n\n".join(evidence_packet.session_excerpts)
|
||||
+ "\n\nReturn JSON only."
|
||||
+ "\n\nReturn JSON only. The frontmatter object must include:"
|
||||
+ "\n- description: a concise skill description"
|
||||
+ "\n- tools: an explicit JSON array of exact tool names this skill needs. "
|
||||
+ "Prefer called tool names when the workflow depends on them; use run-selected tool names only when clearly required. "
|
||||
+ "Use [] only when no tool is required."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -103,6 +113,19 @@ class SkillDraftSynthesizer:
|
||||
"change_reason": str(payload.get("change_reason") or ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_payload(payload: dict[str, Any], evidence_packet: EvidencePacket) -> dict[str, Any]:
|
||||
frontmatter = dict(payload.get("frontmatter") or {})
|
||||
tool_hints = _coerce_string_list(frontmatter.get("tools"))
|
||||
if not tool_hints:
|
||||
tool_hints = _coerce_string_list(evidence_packet.metadata.get("tool_names"))
|
||||
frontmatter["tools"] = tool_hints
|
||||
return {
|
||||
"frontmatter": frontmatter,
|
||||
"content": str(payload.get("content") or "").strip(),
|
||||
"change_reason": str(payload.get("change_reason") or ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _fallback_payload(candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, action: str) -> dict[str, Any]:
|
||||
related = candidate.related_skill_names[0] if candidate.related_skill_names else "generated-skill"
|
||||
@ -111,8 +134,25 @@ class SkillDraftSynthesizer:
|
||||
return {
|
||||
"frontmatter": {
|
||||
"description": candidate.reason or f"Auto-generated {action} draft for {title}.",
|
||||
"tools": [],
|
||||
"tools": _coerce_string_list(evidence_packet.metadata.get("tool_names")),
|
||||
},
|
||||
"content": f"# {title}\n\n## Evidence\n\n{content}\n",
|
||||
"change_reason": candidate.reason or f"Fallback {action} synthesis.",
|
||||
}
|
||||
|
||||
|
||||
def _coerce_string_list(value: Any) -> list[str]:
|
||||
raw_items: list[Any]
|
||||
if isinstance(value, list):
|
||||
raw_items = value
|
||||
elif isinstance(value, str):
|
||||
raw_items = value.split(",")
|
||||
else:
|
||||
raw_items = []
|
||||
|
||||
result: list[str] = []
|
||||
for item in raw_items:
|
||||
cleaned = str(item).strip()
|
||||
if cleaned and cleaned not in result:
|
||||
result.append(cleaned)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user