|
|
@@ -777,10 +777,12 @@ def _tools_json_schema(cfg: Dict) -> list:
|
|
|
|
|
|
def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
tools_schema: list, *, max_steps: int, tool_timeout: int,
|
|
|
- on_step=None, cancel=None) -> str:
|
|
|
- """原生 function-calling 的 ReAct 循环(阶段 2)。用结构化 tool_calls 直接执行,
|
|
|
- 不解析文本——根治 0 执行/格式飘/路径别名类 bug。见 docs/NATIVE_FUNCTION_CALLING_DESIGN.md。
|
|
|
- 复用现有工具执行(_timeout_call)+ on_step 事件(StepEvent),与文本 react 同形。"""
|
|
|
+ on_step=None, cancel=None, ctx=None,
|
|
|
+ enforce_tool: str = "", enforce_min: int = 0) -> str:
|
|
|
+ """原生 function-calling 的 ReAct 循环。用结构化 tool_calls 直接执行,不解析文本——
|
|
|
+ 根治 0 执行/格式飘/路径别名类 bug。见 docs/NATIVE_FUNCTION_CALLING_DESIGN.md。
|
|
|
+ 复用 _timeout_call 工具执行 + on_step(StepEvent);per-tool 写 ctx.trace(可观测);
|
|
|
+ 支持 enforceLoop 计数模式(terminate 前 enforce_tool 须调够 enforce_min 次)。"""
|
|
|
try:
|
|
|
from lambdagent.agentruntime.react_engine import (
|
|
|
StepEvent, STEP_THINK, STEP_TOOL_CALL, STEP_TOOL_RESULT)
|
|
|
@@ -794,8 +796,10 @@ def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
+ import time as _time
|
|
|
messages = [{"role": "system", "content": system_prompt},
|
|
|
{"role": "user", "content": user_input}]
|
|
|
+ tool_counts: Dict[str, int] = {}
|
|
|
final = ""
|
|
|
for step in range(max_steps):
|
|
|
if cancel is not None and cancel.is_cancelled():
|
|
|
@@ -807,8 +811,7 @@ def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
_emit(STEP_THINK, step, content)
|
|
|
if not tcs:
|
|
|
final = content or final
|
|
|
- break # 模型给了最终答案、无工具调用 → 结束
|
|
|
- # 回放 assistant 的 tool_calls(OpenAI 协议要求)
|
|
|
+ break
|
|
|
messages.append({"role": "assistant", "content": content or "",
|
|
|
"tool_calls": [{"id": tc["id"] or f"call_{step}_{i}", "type": "function",
|
|
|
"function": {"name": tc["name"],
|
|
|
@@ -819,11 +822,20 @@ def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
name, args = tc["name"], (tc["arguments"] if isinstance(tc["arguments"], dict) else {})
|
|
|
tcid = tc["id"] or f"call_{step}_{i}"
|
|
|
if name == "terminate":
|
|
|
+ # enforceLoop 计数模式:enforce_tool 没调够 enforce_min 次 → 禁止结束。
|
|
|
+ got = tool_counts.get(enforce_tool, 0)
|
|
|
+ if enforce_tool and enforce_min > 0 and got < enforce_min:
|
|
|
+ msg = (f"[SYSTEM] 还不能结束:{enforce_tool} 已调用 {got}/{enforce_min} 次,"
|
|
|
+ f"还需 {enforce_min - got} 次。请继续完成后再 terminate。")
|
|
|
+ messages.append({"role": "tool", "tool_call_id": tcid, "content": msg})
|
|
|
+ _emit(STEP_TOOL_RESULT, step, msg, tool="terminate")
|
|
|
+ continue # 不 terminate,循环继续
|
|
|
final = (args.get("summary") or "").strip() or content or final
|
|
|
terminated = True
|
|
|
messages.append({"role": "tool", "tool_call_id": tcid, "content": "ok"})
|
|
|
continue
|
|
|
_emit(STEP_TOOL_CALL, step, json.dumps(args, ensure_ascii=False), tool=name)
|
|
|
+ _t0 = _time.time()
|
|
|
tool = tools.get(name)
|
|
|
if tool is None:
|
|
|
obs = f"[TOOL_ERROR] unknown tool: {name}"
|
|
|
@@ -832,7 +844,13 @@ def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
obs = str(_timeout_call(tool, args, tool_timeout))
|
|
|
except Exception as e:
|
|
|
obs = f"[TOOL_ERROR] {e}"
|
|
|
+ tool_counts[name] = tool_counts.get(name, 0) + 1
|
|
|
_emit(STEP_TOOL_RESULT, step, obs, tool=name)
|
|
|
+ if ctx is not None: # per-tool 写 trace → 前端步数/trace 面板可见 FC 过程
|
|
|
+ try:
|
|
|
+ ctx.log(f"fc:{name}", "", args, obs[:500], (_time.time() - _t0) * 1000)
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
messages.append({"role": "tool", "tool_call_id": tcid,
|
|
|
"content": obs[:_MAX_OBS_LENGTH]})
|
|
|
if terminated:
|
|
|
@@ -840,6 +858,23 @@ def _run_fc_loop(provider, system_prompt: str, user_input: str, tools: Dict,
|
|
|
return final
|
|
|
|
|
|
|
|
|
+class _FCReactTerm(Term):
|
|
|
+ """FC react 车道的 Term(apply 收 ctx → 能写 trace)。比 Tool 多带 ctx 与 enforceLoop。"""
|
|
|
+ def __init__(self, name, *, provider, system_prompt, tools, tools_schema,
|
|
|
+ max_steps, tool_timeout, on_step, cancel, enforce_tool, enforce_min):
|
|
|
+ super().__init__(name)
|
|
|
+ self._p = provider; self._sys = system_prompt; self._tools = tools
|
|
|
+ self._schema = tools_schema; self._ms = max_steps; self._tt = tool_timeout
|
|
|
+ self._os = on_step; self._c = cancel
|
|
|
+ self._etool = enforce_tool; self._emin = enforce_min
|
|
|
+
|
|
|
+ def apply(self, input, ctx=None):
|
|
|
+ return _run_fc_loop(self._p, self._sys, str(input), self._tools, self._schema,
|
|
|
+ max_steps=self._ms, tool_timeout=self._tt, on_step=self._os,
|
|
|
+ cancel=self._c, ctx=ctx, enforce_tool=self._etool,
|
|
|
+ enforce_min=self._emin)
|
|
|
+
|
|
|
+
|
|
|
def _compile_react(cfg: Dict, overrides: Dict) -> Term:
|
|
|
"""
|
|
|
type: react -> Loop(react_step, condition, max_steps)
|
|
|
@@ -894,17 +929,15 @@ def _compile_react(cfg: Dict, overrides: Dict) -> Term:
|
|
|
from lambdagent.agentruntime import cancel as _fc_cancel
|
|
|
except Exception:
|
|
|
_fc_cancel = None
|
|
|
- _fc_schema = _tools_json_schema(cfg)
|
|
|
- _fc_sys = cfg.get("systemPrompt", "")
|
|
|
- _fc_on_step = overrides.get("on_step")
|
|
|
-
|
|
|
- def _fc_apply(user_input, _p=_prov, _sys=_fc_sys, _tools=tools,
|
|
|
- _schema=_fc_schema, _ms=max_steps, _tt=tool_timeout,
|
|
|
- _os=_fc_on_step, _c=_fc_cancel):
|
|
|
- return _run_fc_loop(_p, _sys, str(user_input), _tools, _schema,
|
|
|
- max_steps=_ms, tool_timeout=_tt, on_step=_os, cancel=_c)
|
|
|
-
|
|
|
- fc_term = Tool(f"{agent_name}.fc_react", _fc_apply)
|
|
|
+ # enforceLoop 计数模式(单体 agent 用):terminate 前 tool 须调够 minCount 次。
|
|
|
+ # 序列模式(orchestrator)FC 暂不支持——那些 agent 不设 nativeToolCalls。
|
|
|
+ _el = react_cfg.get("enforceLoop", {}) or {}
|
|
|
+ fc_term = _FCReactTerm(
|
|
|
+ f"{agent_name}.fc_react", provider=_prov, system_prompt=cfg.get("systemPrompt", ""),
|
|
|
+ tools=tools, tools_schema=_tools_json_schema(cfg),
|
|
|
+ max_steps=max_steps, tool_timeout=tool_timeout,
|
|
|
+ on_step=overrides.get("on_step"), cancel=_fc_cancel,
|
|
|
+ enforce_tool=_el.get("tool", ""), enforce_min=int(_el.get("minCount", 0) or 0))
|
|
|
fc_term._think_ref = think # token/usage 会计沿用 _find_usage_source
|
|
|
return fc_term
|
|
|
|