| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- """示例工具:演示三种注册形态。
- 骨架刻意只放三个工具,每个演示一件不同的事,业务工具照这个模子加:
- 1. :func:`get_current_time` —— 最简形态:无参、返回字符串
- 2. :func:`search_demo_knowledge` —— 带来源标注:返回 :func:`tools.registry.sourced`
- 包裹的结构,SSE 层会自动把它变成 ``citations`` 事件
- 3. :func:`calculate` —— 确定性计算:把「不该交给模型算」的事放到工具侧,
- 顺带演示参数校验与错误返回
- 写工具的三条纪律:
- - **docstring 是给模型看的**——第一行说清「做什么」,正文说清「什么时候用、
- 参数含义、返回什么」。模型选错工具多半是因为 docstring 写得含糊。
- - **参数要写类型注解**——工具 schema 由签名推导,缺注解会导致参数变成
- 「任意字符串」。
- - **失败要返回可读的原因**,不要抛裸异常:抛异常会中断这一轮工具调用,
- 返回错误说明能让模型自己纠正(比如改个参数重试)。
- """
- from __future__ import annotations
- import ast
- import datetime
- import operator
- import re
- from typing import Any
- from tools.registry import register, source, sourced
- # ============================================================
- # 1. 最简形态
- # ============================================================
- @register(group="系统", label="获取当前时间", summary="返回服务器当前本地时间")
- def get_current_time() -> str:
- """返回服务器当前的本地日期时间。
- 当用户问「现在几点」「今天几号」,或任务需要按当前日期做推算时使用。
- 不要凭记忆回答时间——你的训练数据里没有「现在」。
- """
- now = datetime.datetime.now()
- weekday = "一二三四五六日"[now.weekday()]
- return f"{now.strftime('%Y-%m-%d %H:%M:%S')}(周{weekday})"
- # ============================================================
- # 2. 带来源标注
- # ============================================================
- # 演示用的假知识库。真实项目里这里会替换成 MCP / HTTP 调用。
- _DEMO_KB: list[dict[str, Any]] = [
- {
- "title": "矿井通风基础知识",
- "url": "https://example.invalid/kb/ventilation-basics",
- "text": "矿井通风的任务是向井下各用风地点连续输送足够的新鲜空气,"
- "稀释并排出瓦斯、粉尘和有害气体,同时为作业人员提供适宜的气候条件。",
- "keywords": ["通风", "矿井", "新鲜空气", "瓦斯"],
- },
- {
- "title": "需风量计算方法",
- "url": "https://example.invalid/kb/air-volume",
- "text": "采煤工作面需风量按瓦斯涌出量、同时作业人数、气象条件、风速上限"
- "四项分别计算,取其中最大值。",
- "keywords": ["需风量", "采煤", "瓦斯", "风速"],
- },
- {
- "title": "deepagents 框架简介",
- "url": "https://example.invalid/kb/deepagents",
- "text": "deepagents 是构建深度智能体的框架,提供待办清单、文件系统、"
- "子智能体、技能与上下文压缩等开箱能力。",
- "keywords": ["deepagents", "智能体", "子智能体", "技能"],
- },
- ]
- @register(
- group="示例检索",
- label="检索演示知识库",
- summary="在内置演示知识库里按关键词检索,返回命中片段与来源",
- )
- def search_demo_knowledge(query: str, limit: int = 3) -> dict[str, Any]:
- """在内置演示知识库中检索。
- Args:
- query: 检索关键词,支持空格分隔多个词(命中任意一个即算命中)。
- limit: 最多返回几条,默认 3。
- Returns:
- 含 ``data``(命中片段列表)与 ``sources``(来源列表)的结构。
- ``sources`` 会被上层自动渲染成引用标注,因此必须如实填写,
- 不要编造 url。
- """
- terms = [t for t in re.split(r"\s+", (query or "").strip()) if t]
- if not terms:
- # 返回可读原因而非抛异常:模型看到原因后可以补上关键词重试
- return sourced(data=[], sources=[], error="query 为空,请提供至少一个关键词")
- hits: list[dict[str, Any]] = []
- for item in _DEMO_KB:
- haystack = f"{item['title']} {' '.join(item['keywords'])} {item['text']}"
- matched = [t for t in terms if t.lower() in haystack.lower()]
- if matched:
- # 命中词数 / 总词数,作为粗糙的相关度打分
- hits.append(
- {
- "title": item["title"],
- "text": item["text"],
- "matched": matched,
- "score": round(len(matched) / len(terms), 4),
- }
- )
- hits.sort(key=lambda h: h["score"], reverse=True)
- top = hits[: max(1, int(limit or 3))]
- return sourced(
- data=top,
- sources=[
- source(
- title=hit["title"],
- url=next(i["url"] for i in _DEMO_KB if i["title"] == hit["title"]),
- snippet=hit["text"][:80],
- score=hit["score"],
- )
- for hit in top
- ],
- matched_count=len(hits),
- )
- # ============================================================
- # 3. 确定性计算
- # ============================================================
- # 白名单运算符:只允许算术与幂,杜绝 eval 带来的代码执行面
- _BIN_OPS = {
- ast.Add: operator.add,
- ast.Sub: operator.sub,
- ast.Mult: operator.mul,
- ast.Div: operator.truediv,
- ast.FloorDiv: operator.floordiv,
- ast.Mod: operator.mod,
- ast.Pow: operator.pow,
- }
- _UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg}
- _MAX_POW_EXPONENT = 1000
- def _eval_node(node: ast.AST) -> float:
- """递归求值 AST 节点。只放行算术表达式,其余一律拒绝。"""
- if isinstance(node, ast.Expression):
- return _eval_node(node.body)
- if isinstance(node, ast.Constant):
- if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
- raise ValueError(f"不支持的字面量:{node.value!r}")
- return float(node.value)
- if isinstance(node, ast.BinOp):
- op_type = type(node.op)
- if op_type not in _BIN_OPS:
- raise ValueError(f"不支持的运算符:{op_type.__name__}")
- left, right = _eval_node(node.left), _eval_node(node.right)
- if op_type is ast.Pow and abs(right) > _MAX_POW_EXPONENT:
- # 挡住 9**9**9 这类会把进程算死的表达式
- raise ValueError(f"幂指数过大(上限 {_MAX_POW_EXPONENT})")
- return _BIN_OPS[op_type](left, right)
- if isinstance(node, ast.UnaryOp):
- op_type = type(node.op)
- if op_type not in _UNARY_OPS:
- raise ValueError(f"不支持的一元运算符:{op_type.__name__}")
- return _UNARY_OPS[op_type](_eval_node(node.operand))
- raise ValueError(f"不支持的表达式元素:{type(node).__name__}")
- @register(
- group="示例计算",
- label="安全算术计算",
- summary="对纯算术表达式做精确求值,避免心算出错",
- )
- def calculate(expression: str) -> dict[str, Any]:
- """对纯算术表达式求值,支持 + - * / // % ** 与括号。
- Args:
- expression: 算术表达式,例如 ``"(1200 + 300) * 0.85"``。
- Returns:
- ``{"expression": ..., "result": ...}``;表达式非法时返回 ``error`` 字段。
- 需要精确数值(配风量核算、成本换算、单位折算)时使用本工具,
- 不要自己心算——多位数乘除很容易出错,而且错了没人能发现。
- """
- expr = (expression or "").strip()
- if not expr:
- return {"error": "expression 为空"}
- if len(expr) > 200:
- return {"error": "表达式过长(上限 200 字符)"}
- try:
- tree = ast.parse(expr, mode="eval")
- result = _eval_node(tree)
- except SyntaxError:
- return {"error": f"表达式语法错误:{expr!r}"}
- except ZeroDivisionError:
- return {"error": "除数为零"}
- except (ValueError, OverflowError) as exc:
- return {"error": str(exc)}
- except Exception as exc: # pragma: no cover - 兜底,避免工具异常打断整轮
- return {"error": f"计算失败:{type(exc).__name__}: {exc}"}
- # 整数结果去掉多余小数位,便于阅读与后续引用
- if result == int(result) and abs(result) < 1e15:
- result = int(result)
- return {"expression": expr, "result": result}
- # ============================================================
- # 4. 人工审批(HITL 管道)
- # ============================================================
- # 这个工具的名字会被 agents/agent_factory 放进 interrupt_on:
- # 调用它时官方 HumanInTheLoopMiddleware 会在**执行前**拦下,向用户抛出
- # 审批请求;用户批准后才真正执行函数体。
- PLAN_APPROVAL_TOOL = "request_plan_approval"
- @register(
- group="人机协同",
- label="请求计划审批",
- summary="把待执行的计划提交给用户确认,用户批准后才继续",
- )
- def request_plan_approval(plan: str) -> str:
- """把计划提交给用户审批,等待其批准后再继续执行。
- Args:
- plan: 待审批的计划全文。要写清步骤、涉及的操作、预期产出。
- 不要只写一句「我将开始处理」——用户没有信息就无法判断该不该批准。
- 当执行模式为 plan、或任务包含不可逆操作(删除、覆盖、对外发送、
- 消耗额度)需要用户点头时调用。普通问答与只读查询不要调用,
- 无谓的确认会打断用户。
- Returns:
- 用户批准后的确认文本;用户拒绝时本函数不会被执行。
- """
- # 正常情况下走不到这里:调用被 interrupt_on 拦下,用户批准后才会执行函数体。
- # 这条 return 是「批准」路径的返回值。
- return f"用户已批准以下计划,请按计划执行:\n{plan}"
|