feat: 重新打开 FC 开关,AI 可在纯 JSON 与 FC 之间自选(混合模式)

This commit is contained in:
shuiyu67 committed 2026-08-06 23:30:53 +08:00
1 parent a70e4b1819
commit 46376aa23e
3 files changed
+189 -11

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
"""
FC 混合模式(AI 自选)集成测试
验证「让 AI 自己选纯 JSON 或 FC」的接线:
1. 支持 FC 的引擎会收到下发的 tools schema(tool_choice=auto,模型自选)
2. 引擎返回 _fc JSON 时,code_editor 被按名分发执行(走 _handle_fc_response 路径)
3. 不支持 FC 的引擎(无 _fc_supported)则不收 tools,纯 JSON 照常工作
"""
import os
import sys
import json
import tempfile
import pytest
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
class FakeFcEngine:
"""模拟一个支持 FC 的引擎:首轮返回 _fc 调用 code_editor,次轮收尾。"""
def __init__(self):
self.calls = 0
self.tools_seen = []
@property
def name(self):
return "fakefc"
def _fc_supported(self):
return True
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
self.calls += 1
self.tools_seen.append(tools)
if self.calls == 1:
return json.dumps({
"_fc": True,
"tool_calls": [{
"id": "c1",
"name": "code_editor",
"tool": "code_editor",
"args": "create %s print(1)" % self._fp,
}],
}, ensure_ascii=False)
return "已完成。"
class FakeNoFcEngine:
"""模拟一个不支持 FC 的引擎(无 _fc_supported):应降级为纯 JSON。"""
def __init__(self):
self.calls = 0
self.tools_seen = []
@property
def name(self):
return "fakenofc"
def generate_response(self, user_input, tool_results=None, system_prompt=None, tools=None):
self.calls += 1
self.tools_seen.append(tools)
assert tools is None, f"不支持 FC 的引擎不应收到 tools,实际 {tools!r}"
if self.calls == 1:
return ('好的,我来创建文件。\n'
'{"action": "use_tool", "tool": "code_editor", '
'"args": "create %s print(1)"}' % self._fp)
return "已完成。"
def _run(engine):
from xcli_core.cli_core import AICLI
from xcli_core.safety import get_safety, MODE_UNRESTRICTED
cli = AICLI()
cli.current_engine = engine
cli.tui_output_callback = lambda *a, **k: None # 同步 TUI 分支,免动画线程
get_safety().set_mode(MODE_UNRESTRICTED)
d = tempfile.mkdtemp(prefix="pj_")
fp = os.path.join(d, "demo.py")
engine._fp = fp
cli.process_conversation("帮我创建一个 python 文件")
return engine, fp
def test_fc_engine_receives_tools_and_dispatches():
engine, fp = _run(FakeFcEngine())
# 首轮应向支持 FC 的引擎下发 tools schema
assert engine.tools_seen[0] is not None, "FC 引擎应收到 tools schema"
assert isinstance(engine.tools_seen[0], list) and len(engine.tools_seen[0]) > 0
assert any(
t.get("function", {}).get("name") == "code_editor" for t in engine.tools_seen[0]
), "code_editor 应在下发的 FC 工具集中"
# _fc 调用应被按名分发执行并真实创建文件
assert os.path.exists(fp), "FC 路径应执行 code_editor 并创建文件"
assert "print(1)" in open(fp, encoding="utf-8").read()
assert engine.calls >= 2
def test_no_fc_engine_stays_pure_json():
engine, fp = _run(FakeNoFcEngine())
# 不支持 FC 的引擎不应收到 tools,纯 JSON 照常分发
assert all(t is None for t in engine.tools_seen), "不支持 FC 的引擎不应收到 tools"
assert os.path.exists(fp), "纯 JSON 路径仍应创建文件"
def test_build_fc_tools_returns_none_for_unsupported_engine():
from xcli_core.cli_core import AICLI
cli = AICLI()
cli.current_engine = FakeNoFcEngine()
assert cli._build_fc_tools("帮我创建文件") is None
def test_build_fc_tools_returns_list_for_supported_engine():
from xcli_core.cli_core import AICLI
cli = AICLI()
cli.current_engine = FakeFcEngine()
tools = cli._build_fc_tools("帮我创建 python 文件")
assert isinstance(tools, list) and len(tools) > 0
assert any(t.get("function", {}).get("name") == "code_editor" for t in tools)