初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
"""随应用部署的受信任 Python Skills。
新增 Skill 时创建同级模块并导出 ``setup(registry)``。目录仅接收经过代码审查的模块;
数据库 Skill 与用户上传内容不得写入这里或在 FastAPI 进程内动态执行。
"""
+39
View File
@@ -0,0 +1,39 @@
"""工具参数守卫:拒绝参数过大的工具调用,防止模型生成超大输入。"""
import json
from app.application.agent.extensions import (
AgentRunContext,
BeforeToolCallDecision,
ExtensionRegistry,
ToolCallContext,
)
from app.application.agent.python_skills import PythonSkillDefinition
# 单次工具调用参数序列化后的字符上限;超出则拒绝并要求模型精简。
_MAX_ARG_CHARS = 8000
async def _reject_oversized_args(
call: ToolCallContext, _run: AgentRunContext
) -> BeforeToolCallDecision | None:
size = len(json.dumps(call.input, ensure_ascii=False))
if size <= _MAX_ARG_CHARS:
return None
return BeforeToolCallDecision(
allowed=False,
reason=f"工具 {call.name} 的参数过大({size} 字符超过 {_MAX_ARG_CHARS}),请精简后重试。",
)
def _setup(registry: ExtensionRegistry) -> None:
registry.add_before_tool_hook(_reject_oversized_args)
SKILL = PythonSkillDefinition(
name="arg_guard",
description="拒绝参数过大的工具调用,避免超大输入挤占上下文或触发限制。",
triggers=("大参数", "arg guard"),
instructions="发起工具调用时保持参数精简;超大参数会被拒绝。",
setup=_setup,
)
+34
View File
@@ -0,0 +1,34 @@
"""Token 预算守卫:累计消耗超阈值时优雅收尾。"""
from app.application.agent.extensions import (
AgentRunContext,
ExtensionRegistry,
TurnSnapshot,
)
from app.application.agent.python_skills import PythonSkillDefinition
# 累计 total_tokens 软上限。达到后 TurnPolicy 要求停止,让 Agent 用当前内容自然
# 收尾——区别于 max_iterations 的硬截断(truncated=True,回复可能只说了一半)。
_BUDGET_TOTAL_TOKENS = 20000
async def _stop_when_budget_exhausted(
turn: TurnSnapshot, _run: AgentRunContext
) -> bool:
return turn.usage.total_tokens >= _BUDGET_TOTAL_TOKENS
def _setup(registry: ExtensionRegistry) -> None:
registry.add_turn_policy(_stop_when_budget_exhausted)
SKILL = PythonSkillDefinition(
name="budget_guard",
description="累计 token 消耗超预算时优雅停止 Agent 循环,避免成本失控。",
triggers=("预算", "budget", "长任务"),
instructions=(
"本轮受 token 预算约束。若已接近上限,请用当前已有信息给出最终回复,"
"不要发起新的工具调用或长篇推理。"
),
setup=_setup,
)
+47
View File
@@ -0,0 +1,47 @@
"""上下文预算守卫:历史过长时折叠中间消息,保护 prompt 预算。"""
from app.ai_provider.types.messages import BaseMessage
from app.application.agent.extensions import (
AgentContext,
AgentRunContext,
ExtensionRegistry,
)
from app.application.agent.python_skills import PythonSkillDefinition
# messages 总字符数软上限;超出则保留首条与末尾 _TAIL_KEEP 条,中间折叠。
_MAX_CONTEXT_CHARS = 12000
_TAIL_KEEP = 8
async def _trim_history(context: AgentContext, _run: AgentRunContext) -> AgentContext:
messages = context.messages
total = sum(len(message.content) for message in messages)
if total <= _MAX_CONTEXT_CHARS or len(messages) <= _TAIL_KEEP + 1:
return context
first = messages[0]
tail = messages[-_TAIL_KEEP:]
dropped = len(messages) - 1 - _TAIL_KEEP
# 在首条追加折叠提示(新建对象避免污染原 message);不在历史中间插入新消息,
# 以免破坏 provider 对 system 位置或多轮 tool 消息配对的预期。
folded = BaseMessage(
content=f"{first.content}\n\n[已省略 {dropped} 条较早历史]",
role=first.role,
)
return AgentContext(
messages=(folded, *tail),
tools=context.tools,
model_hint=context.model_hint,
)
def _setup(registry: ExtensionRegistry) -> None:
registry.add_context_transformer(_trim_history)
SKILL = PythonSkillDefinition(
name="context_budget",
description="对话历史过长时折叠中间消息,控制 prompt 预算。",
triggers=("长对话", "context", "历史"),
instructions="历史较长时优先依赖近期消息,不必回溯很早的内容。",
setup=_setup,
)
+44
View File
@@ -0,0 +1,44 @@
"""工具输出防护示例 Skill。"""
from dataclasses import replace
from app.application.agent.extensions import (
AfterToolCallDecision,
AgentRunContext,
ExtensionRegistry,
ToolCallContext,
)
from app.application.agent.python_skills import PythonSkillDefinition
from app.modules.tool_runtime.definition import ToolResult
_MAX_OUTPUT_CHARS = 4000
async def _limit_tool_output(
_call: ToolCallContext,
result: ToolResult,
_run: AgentRunContext,
) -> AfterToolCallDecision | None:
if len(result.stdout) <= _MAX_OUTPUT_CHARS and len(result.stderr) <= _MAX_OUTPUT_CHARS:
return None
return AfterToolCallDecision(
result=replace(
result,
stdout=result.stdout[:_MAX_OUTPUT_CHARS],
stderr=result.stderr[:_MAX_OUTPUT_CHARS],
truncated=True,
)
)
def _setup(registry: ExtensionRegistry) -> None:
registry.add_after_tool_hook(_limit_tool_output)
SKILL = PythonSkillDefinition(
name="response_guard",
description="限制过长工具输出,避免无关内容挤占模型上下文。",
triggers=("长输出", "大量输出", "response guard"),
instructions="工具输出可能很长;优先分段读取,只保留完成当前任务所需的内容。",
setup=_setup,
)
+53
View File
@@ -0,0 +1,53 @@
"""运行指标采集:监听 Agent 生命周期事件,结束时记录摘要日志。"""
from loguru import logger
from app.application.agent.extensions import (
AgentEvent,
AgentRunContext,
ExtensionRegistry,
)
from app.application.agent.python_skills import PythonSkillDefinition
def _setup(registry: ExtensionRegistry) -> None:
turns = 0
tool_calls = 0
tool_errors = 0
tool_denied = 0
async def listen(event: AgentEvent, run: AgentRunContext) -> None:
nonlocal turns, tool_calls, tool_errors, tool_denied
# 工具事件经 Agent._on_tool_event 桥接进扩展事件流,故此处可直接观测
# tool.started / tool.completed(status: success / error / denied)。
if event.type == "agent.turn.started":
turns += 1
elif event.type == "tool.started":
tool_calls += 1
elif event.type == "tool.completed":
status = event.payload.get("status")
if status == "error":
tool_errors += 1
elif status == "denied":
tool_denied += 1
elif event.type == "agent.settled":
logger.info(
"agent_run_metrics",
run_id=run.run_id,
turns=turns,
tool_calls=tool_calls,
tool_errors=tool_errors,
tool_denied=tool_denied,
status=event.payload.get("status"),
)
registry.add_event_listener(listen)
SKILL = PythonSkillDefinition(
name="run_metrics",
description="采集 Agent 轮次指标并在运行结束时记录摘要日志。",
triggers=("指标", "metrics", "观测"),
instructions="",
setup=_setup,
)