|
@@ -775,6 +775,71 @@ def _tools_json_schema(cfg: Dict) -> list:
|
|
|
return out
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+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 同形。"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ from lambdagent.agentruntime.react_engine import (
|
|
|
|
|
+ StepEvent, STEP_THINK, STEP_TOOL_CALL, STEP_TOOL_RESULT)
|
|
|
|
|
+ except ImportError:
|
|
|
|
|
+ StepEvent = None
|
|
|
|
|
+
|
|
|
|
|
+ def _emit(etype, step, content, tool=""):
|
|
|
|
|
+ if on_step and StepEvent is not None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ on_step(StepEvent(type=etype, step=step, content=str(content)[:2000], tool=tool))
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ messages = [{"role": "system", "content": system_prompt},
|
|
|
|
|
+ {"role": "user", "content": user_input}]
|
|
|
|
|
+ final = ""
|
|
|
|
|
+ for step in range(max_steps):
|
|
|
|
|
+ if cancel is not None and cancel.is_cancelled():
|
|
|
|
|
+ raise cancel.CancelledRun()
|
|
|
|
|
+ resp = provider.chat_with_tools(messages, tools_schema)
|
|
|
|
|
+ content = resp.get("content")
|
|
|
|
|
+ tcs = resp.get("tool_calls") or []
|
|
|
|
|
+ if content:
|
|
|
|
|
+ _emit(STEP_THINK, step, content)
|
|
|
|
|
+ if not tcs:
|
|
|
|
|
+ final = content or final
|
|
|
|
|
+ break # 模型给了最终答案、无工具调用 → 结束
|
|
|
|
|
+ # 回放 assistant 的 tool_calls(OpenAI 协议要求)
|
|
|
|
|
+ messages.append({"role": "assistant", "content": content or "",
|
|
|
|
|
+ "tool_calls": [{"id": tc["id"] or f"call_{step}_{i}", "type": "function",
|
|
|
|
|
+ "function": {"name": tc["name"],
|
|
|
|
|
+ "arguments": json.dumps(tc["arguments"], ensure_ascii=False)}}
|
|
|
|
|
+ for i, tc in enumerate(tcs)]})
|
|
|
|
|
+ terminated = False
|
|
|
|
|
+ for i, tc in enumerate(tcs):
|
|
|
|
|
+ name, args = tc["name"], (tc["arguments"] if isinstance(tc["arguments"], dict) else {})
|
|
|
|
|
+ tcid = tc["id"] or f"call_{step}_{i}"
|
|
|
|
|
+ if name == "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)
|
|
|
|
|
+ tool = tools.get(name)
|
|
|
|
|
+ if tool is None:
|
|
|
|
|
+ obs = f"[TOOL_ERROR] unknown tool: {name}"
|
|
|
|
|
+ else:
|
|
|
|
|
+ try:
|
|
|
|
|
+ obs = str(_timeout_call(tool, args, tool_timeout))
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ obs = f"[TOOL_ERROR] {e}"
|
|
|
|
|
+ _emit(STEP_TOOL_RESULT, step, obs, tool=name)
|
|
|
|
|
+ messages.append({"role": "tool", "tool_call_id": tcid,
|
|
|
|
|
+ "content": obs[:_MAX_OBS_LENGTH]})
|
|
|
|
|
+ if terminated:
|
|
|
|
|
+ break
|
|
|
|
|
+ return final
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _compile_react(cfg: Dict, overrides: Dict) -> Term:
|
|
def _compile_react(cfg: Dict, overrides: Dict) -> Term:
|
|
|
"""
|
|
"""
|
|
|
type: react -> Loop(react_step, condition, max_steps)
|
|
type: react -> Loop(react_step, condition, max_steps)
|
|
@@ -816,6 +881,33 @@ def _compile_react(cfg: Dict, overrides: Dict) -> Term:
|
|
|
verbose = react_cfg.get("verbose", False)
|
|
verbose = react_cfg.get("verbose", False)
|
|
|
agent_name = cfg.get("name", cfg.get("agentId", "agent"))
|
|
agent_name = cfg.get("name", cfg.get("agentId", "agent"))
|
|
|
|
|
|
|
|
|
|
+ # ── 原生 function-calling 车道(react.nativeToolCalls,阶段 2)──
|
|
|
|
|
+ # FC-capable provider(OpenAI 兼容:qwen/openai/deepseek…)+ 开关 → 用结构化
|
|
|
|
|
+ # tool_calls 直接执行,根治"文本求 JSON、正则抠"那一类 0 执行/格式飘 bug。
|
|
|
|
|
+ # 旧文本路径(下方)保留作 fallback:provider 不支持 FC 或开关 off 时走它。
|
|
|
|
|
+ # 见 docs/NATIVE_FUNCTION_CALLING_DESIGN.md。
|
|
|
|
|
+ if react_cfg.get("nativeToolCalls"):
|
|
|
|
|
+ _prov = getattr(think, "provider", None)
|
|
|
|
|
+ if (_prov is not None and hasattr(_prov, "chat_with_tools")
|
|
|
|
|
+ and getattr(_prov, "supports_function_calling", lambda: False)()):
|
|
|
|
|
+ try:
|
|
|
|
|
+ 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)
|
|
|
|
|
+ fc_term._think_ref = think # token/usage 会计沿用 _find_usage_source
|
|
|
|
|
+ return fc_term
|
|
|
|
|
+
|
|
|
# enforceLoop: hard floor on tool execution before terminate is allowed.
|
|
# enforceLoop: hard floor on tool execution before terminate is allowed.
|
|
|
# Two modes:
|
|
# Two modes:
|
|
|
#
|
|
#
|