Add fastgpt as seperate assistant mode
This commit is contained in:
@@ -109,6 +109,22 @@ class _CaptureGenerateLLM:
|
||||
yield LLMStreamEvent(type="done")
|
||||
|
||||
|
||||
class _InitGreetingLLM:
|
||||
def __init__(self, greeting: str):
|
||||
self.greeting = greeting
|
||||
self.init_calls = 0
|
||||
|
||||
async def generate(self, _messages, temperature=0.7, max_tokens=None):
|
||||
return ""
|
||||
|
||||
async def generate_stream(self, _messages, temperature=0.7, max_tokens=None):
|
||||
yield LLMStreamEvent(type="done")
|
||||
|
||||
async def get_initial_greeting(self):
|
||||
self.init_calls += 1
|
||||
return self.greeting
|
||||
|
||||
|
||||
def _build_pipeline(monkeypatch, llm_rounds: List[List[LLMStreamEvent]]) -> tuple[DuplexPipeline, List[Dict[str, Any]]]:
|
||||
monkeypatch.setattr("runtime.pipeline.duplex.SileroVAD", _DummySileroVAD)
|
||||
monkeypatch.setattr("runtime.pipeline.duplex.VADProcessor", _DummyVADProcessor)
|
||||
@@ -306,6 +322,21 @@ async def test_generated_opener_uses_tool_capable_turn_when_tools_available(monk
|
||||
assert called.get("user_text") == ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provider_initial_greeting_takes_precedence_over_local_opener(monkeypatch):
|
||||
llm = _InitGreetingLLM("FastGPT init greeting")
|
||||
pipeline, events = _build_pipeline_with_custom_llm(monkeypatch, llm)
|
||||
pipeline.apply_runtime_overrides({"output": {"mode": "text"}})
|
||||
pipeline.conversation.greeting = "local fallback greeting"
|
||||
|
||||
await pipeline.emit_initial_greeting()
|
||||
|
||||
finals = [event for event in events if event.get("type") == "assistant.response.final"]
|
||||
assert finals
|
||||
assert finals[-1]["text"] == "FastGPT init greeting"
|
||||
assert llm.init_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_opener_tool_calls_emit_assistant_tool_call(monkeypatch):
|
||||
pipeline, events = _build_pipeline(monkeypatch, [[LLMStreamEvent(type="done")]])
|
||||
@@ -736,3 +767,139 @@ async def test_eou_early_return_clears_stale_asr_capture(monkeypatch):
|
||||
assert pipeline._asr_capture_active is False
|
||||
assert pipeline._asr_capture_started_ms == 0.0
|
||||
assert pipeline._pending_speech_audio == b""
|
||||
|
||||
class _FakeResumableLLM:
|
||||
def __init__(self, *, timeout_ms: int = 300000):
|
||||
self.timeout_ms = timeout_ms
|
||||
self.generate_stream_calls = 0
|
||||
self.resumed_results: List[Dict[str, Any]] = []
|
||||
|
||||
async def generate(self, _messages, temperature=0.7, max_tokens=None):
|
||||
return ""
|
||||
|
||||
async def generate_stream(self, _messages, temperature=0.7, max_tokens=None):
|
||||
self.generate_stream_calls += 1
|
||||
yield LLMStreamEvent(
|
||||
type="tool_call",
|
||||
tool_call={
|
||||
"id": "call_fastgpt_1",
|
||||
"executor": "client",
|
||||
"wait_for_response": True,
|
||||
"timeout_ms": self.timeout_ms,
|
||||
"display_name": "Choose a plan",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "fastgpt.interactive",
|
||||
"arguments": json.dumps(
|
||||
{
|
||||
"provider": "fastgpt",
|
||||
"version": "fastgpt_interactive_v1",
|
||||
"interaction": {
|
||||
"type": "userSelect",
|
||||
"title": "Choose a plan",
|
||||
"options": [
|
||||
{"id": "basic", "label": "Basic", "value": "basic"},
|
||||
{"id": "pro", "label": "Pro", "value": "pro"},
|
||||
],
|
||||
"form": [],
|
||||
},
|
||||
"context": {"chat_id": "fastgpt_chat_1"},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
yield LLMStreamEvent(type="done")
|
||||
|
||||
def handles_client_tool(self, tool_name: str) -> bool:
|
||||
return tool_name == "fastgpt.interactive"
|
||||
|
||||
async def resume_after_client_tool_result(self, tool_call_id: str, result: Dict[str, Any]):
|
||||
self.resumed_results.append({"tool_call_id": tool_call_id, "result": dict(result)})
|
||||
yield LLMStreamEvent(type="text_delta", text="provider resumed answer.")
|
||||
yield LLMStreamEvent(type="done")
|
||||
|
||||
|
||||
def _build_pipeline_with_custom_llm(monkeypatch, llm_service) -> tuple[DuplexPipeline, List[Dict[str, Any]]]:
|
||||
monkeypatch.setattr("runtime.pipeline.duplex.SileroVAD", _DummySileroVAD)
|
||||
monkeypatch.setattr("runtime.pipeline.duplex.VADProcessor", _DummyVADProcessor)
|
||||
monkeypatch.setattr("runtime.pipeline.duplex.EouDetector", _DummyEouDetector)
|
||||
|
||||
pipeline = DuplexPipeline(
|
||||
transport=_FakeTransport(),
|
||||
session_id="s_fastgpt",
|
||||
llm_service=llm_service,
|
||||
tts_service=_FakeTTS(),
|
||||
asr_service=_FakeASR(),
|
||||
)
|
||||
events: List[Dict[str, Any]] = []
|
||||
|
||||
async def _capture_event(event: Dict[str, Any], priority: int = 20):
|
||||
events.append(event)
|
||||
|
||||
async def _noop_speak(_text: str, *args, **kwargs):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(pipeline, "_send_event", _capture_event)
|
||||
monkeypatch.setattr(pipeline, "_speak_sentence", _noop_speak)
|
||||
return pipeline, events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_managed_tool_resumes_provider_stream(monkeypatch):
|
||||
llm = _FakeResumableLLM(timeout_ms=300000)
|
||||
pipeline, events = _build_pipeline_with_custom_llm(monkeypatch, llm)
|
||||
pipeline.apply_runtime_overrides({"output": {"mode": "text"}})
|
||||
|
||||
task = asyncio.create_task(pipeline._handle_turn("start fastgpt"))
|
||||
for _ in range(200):
|
||||
if any(event.get("type") == "assistant.tool_call" for event in events):
|
||||
break
|
||||
await asyncio.sleep(0.005)
|
||||
|
||||
tool_event = next(event for event in events if event.get("type") == "assistant.tool_call")
|
||||
assert tool_event.get("executor") == "client"
|
||||
assert tool_event.get("tool_name") == "fastgpt.interactive"
|
||||
assert tool_event.get("timeout_ms") == 300000
|
||||
assert tool_event.get("arguments", {}).get("context", {}).get("turn_id")
|
||||
assert tool_event.get("arguments", {}).get("context", {}).get("response_id")
|
||||
|
||||
await pipeline.handle_tool_call_results(
|
||||
[
|
||||
{
|
||||
"tool_call_id": "call_fastgpt_1",
|
||||
"name": "fastgpt.interactive",
|
||||
"output": {
|
||||
"action": "submit",
|
||||
"result": {"type": "userSelect", "selected": "pro"},
|
||||
},
|
||||
"status": {"code": 200, "message": "ok"},
|
||||
}
|
||||
]
|
||||
)
|
||||
await task
|
||||
|
||||
finals = [event for event in events if event.get("type") == "assistant.response.final"]
|
||||
assert finals
|
||||
assert "provider resumed answer" in finals[-1].get("text", "")
|
||||
assert llm.generate_stream_calls == 1
|
||||
assert len(llm.resumed_results) == 1
|
||||
assert llm.resumed_results[0]["tool_call_id"] == "call_fastgpt_1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_managed_tool_timeout_stops_without_generic_tool_prompt(monkeypatch):
|
||||
llm = _FakeResumableLLM(timeout_ms=10)
|
||||
pipeline, events = _build_pipeline_with_custom_llm(monkeypatch, llm)
|
||||
pipeline.apply_runtime_overrides({"output": {"mode": "text"}})
|
||||
|
||||
await pipeline._handle_turn("start fastgpt")
|
||||
|
||||
tool_results = [event for event in events if event.get("type") == "assistant.tool_result"]
|
||||
assert tool_results
|
||||
assert tool_results[-1].get("result", {}).get("status", {}).get("code") == 504
|
||||
finals = [event for event in events if event.get("type") == "assistant.response.final"]
|
||||
assert not finals
|
||||
assert llm.generate_stream_calls == 1
|
||||
assert llm.resumed_results == []
|
||||
|
||||
Reference in New Issue
Block a user