Add fastgpt as seperate assistant mode
This commit is contained in:
411
engine/tests/test_fastgpt_provider.py
Normal file
411
engine/tests/test_fastgpt_provider.py
Normal file
@@ -0,0 +1,411 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from providers.common.base import LLMMessage
|
||||
from providers.llm.fastgpt import FastGPTLLMService
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, events: List[Any]):
|
||||
self.events = events
|
||||
self.closed = False
|
||||
|
||||
async def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class _FakeJSONResponse:
|
||||
def __init__(self, payload: Dict[str, Any], status_code: int = 200):
|
||||
self._payload = payload
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return dict(self._payload)
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
if self.status_code >= 400:
|
||||
raise RuntimeError(f"HTTP {self.status_code}")
|
||||
|
||||
|
||||
class _FakeAsyncStreamResponse(_FakeResponse):
|
||||
def __init__(self, events: List[Any]):
|
||||
super().__init__(events)
|
||||
self.aclosed = False
|
||||
|
||||
def close(self) -> None:
|
||||
raise AssertionError("sync close should not be used for async stream responses")
|
||||
|
||||
async def aclose(self) -> None:
|
||||
self.aclosed = True
|
||||
|
||||
|
||||
class _FakeAsyncChatClient:
|
||||
responses: List[_FakeResponse] = []
|
||||
init_payload: Dict[str, Any] | None = None
|
||||
|
||||
def __init__(self, api_key: str, base_url: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
self.requests: List[Dict[str, Any]] = []
|
||||
self.init_requests: List[Dict[str, Any]] = []
|
||||
|
||||
async def create_chat_completion(self, **kwargs):
|
||||
self.requests.append(dict(kwargs))
|
||||
if not self.responses:
|
||||
raise AssertionError("No fake FastGPT response queued")
|
||||
return self.responses.pop(0)
|
||||
|
||||
async def get_chat_init(self, **kwargs):
|
||||
self.init_requests.append(dict(kwargs))
|
||||
return _FakeJSONResponse(
|
||||
self.init_payload or {"data": {"app": {"chatConfig": {"welcomeText": ""}}}},
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
async def _fake_aiter_stream_events(response: _FakeResponse):
|
||||
for event in response.events:
|
||||
yield event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_streams_text_from_data_event(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="data",
|
||||
data={"choices": [{"delta": {"content": "Hello from FastGPT."}}]},
|
||||
),
|
||||
SimpleNamespace(kind="done", data={}),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Hi")])]
|
||||
|
||||
assert [event.type for event in events] == ["text_delta", "done"]
|
||||
assert events[0].text == "Hello from FastGPT."
|
||||
assert service.client.requests[0]["messages"] == [{"role": "user", "content": "Hi"}]
|
||||
assert service.client.requests[0]["chatId"] == service._state.chat_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_streams_text_from_answer_delta_event(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="answer",
|
||||
data={"choices": [{"delta": {"content": "Hello from answer delta."}}]},
|
||||
),
|
||||
SimpleNamespace(kind="done", data={}),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Hi")])]
|
||||
|
||||
assert [event.type for event in events] == ["text_delta", "done"]
|
||||
assert events[0].text == "Hello from answer delta."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_uses_async_close_for_stream_responses(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
response = _FakeAsyncStreamResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="data",
|
||||
data={"choices": [{"delta": {"content": "Hello from FastGPT."}}]},
|
||||
),
|
||||
SimpleNamespace(kind="done", data={}),
|
||||
]
|
||||
)
|
||||
_FakeAsyncChatClient.responses = [response]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Hi")])]
|
||||
|
||||
assert [event.type for event in events] == ["text_delta", "done"]
|
||||
assert response.aclosed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_loads_initial_greeting_from_chat_init(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.init_payload = {
|
||||
"data": {
|
||||
"app": {
|
||||
"chatConfig": {
|
||||
"welcomeText": "Hello from FastGPT init.",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service = FastGPTLLMService(
|
||||
api_key="key",
|
||||
base_url="https://fastgpt.example",
|
||||
app_id="app-123",
|
||||
)
|
||||
await service.connect()
|
||||
|
||||
greeting = await service.get_initial_greeting()
|
||||
|
||||
assert greeting == "Hello from FastGPT init."
|
||||
assert service.client.init_requests[0] == {
|
||||
"appId": "app-123",
|
||||
"chatId": service._state.chat_id,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_maps_interactive_event_to_client_tool(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="interactive",
|
||||
data={
|
||||
"type": "userSelect",
|
||||
"title": "Choose a plan",
|
||||
"params": {
|
||||
"description": "Pick the best plan for your team.",
|
||||
"userSelectOptions": [
|
||||
{"id": "basic", "label": "Basic", "value": "basic", "desc": "Starter tier"},
|
||||
{"id": "pro", "label": "Pro", "value": "pro", "description": "Advanced tier"},
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Start")])]
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "tool_call"
|
||||
tool_call = events[0].tool_call
|
||||
assert tool_call["executor"] == "client"
|
||||
assert tool_call["wait_for_response"] is True
|
||||
assert tool_call["timeout_ms"] == 300000
|
||||
assert tool_call["function"]["name"] == "fastgpt.interactive"
|
||||
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
assert arguments["provider"] == "fastgpt"
|
||||
assert arguments["version"] == "fastgpt_interactive_v1"
|
||||
assert arguments["interaction"]["type"] == "userSelect"
|
||||
assert arguments["interaction"]["description"] == "Pick the best plan for your team."
|
||||
assert arguments["interaction"]["options"][0]["description"] == "Starter tier"
|
||||
assert arguments["interaction"]["options"][1]["value"] == "pro"
|
||||
assert arguments["interaction"]["options"][1]["description"] == "Advanced tier"
|
||||
assert arguments["context"]["chat_id"] == service._state.chat_id
|
||||
assert service._state.pending_interaction is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_unwraps_nested_tool_children_interactive(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="interactive",
|
||||
data={
|
||||
"interactive": {
|
||||
"type": "toolChildrenInteractive",
|
||||
"params": {
|
||||
"childrenResponse": {
|
||||
"type": "userSelect",
|
||||
"params": {
|
||||
"description": "Please choose a workflow branch.",
|
||||
"userSelectOptions": [
|
||||
{"value": "A", "description": "Branch A"},
|
||||
{"value": "B", "description": "Branch B"},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Start")])]
|
||||
|
||||
assert len(events) == 1
|
||||
arguments = json.loads(events[0].tool_call["function"]["arguments"])
|
||||
assert arguments["interaction"]["type"] == "userSelect"
|
||||
assert arguments["interaction"]["description"] == "Please choose a workflow branch."
|
||||
assert arguments["interaction"]["options"][0]["description"] == "Branch A"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_uses_opener_for_interactive_prompt_when_prompt_missing(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="interactive",
|
||||
data={
|
||||
"type": "userSelect",
|
||||
"opener": "请确认您是否满意本次服务。",
|
||||
"params": {
|
||||
"userSelectOptions": [
|
||||
{"value": "是"},
|
||||
{"value": "否"},
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Start")])]
|
||||
|
||||
assert len(events) == 1
|
||||
tool_call = events[0].tool_call
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
assert tool_call["display_name"] == "请确认您是否满意本次服务。"
|
||||
assert arguments["interaction"]["prompt"] == "请确认您是否满意本次服务。"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_resumes_same_chat_after_client_result(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="interactive",
|
||||
data={
|
||||
"type": "userSelect",
|
||||
"params": {"userSelectOptions": [{"label": "Pro", "value": "pro"}]},
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(kind="answer", data={"text": "Resumed answer."}),
|
||||
SimpleNamespace(kind="done", data={}),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
initial_events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Start")])]
|
||||
call_id = initial_events[0].tool_call["id"]
|
||||
|
||||
resumed_events = [
|
||||
event
|
||||
async for event in service.resume_after_client_tool_result(
|
||||
call_id,
|
||||
{
|
||||
"tool_call_id": call_id,
|
||||
"name": "fastgpt.interactive",
|
||||
"output": {
|
||||
"action": "submit",
|
||||
"result": {"type": "userSelect", "selected": "pro"},
|
||||
},
|
||||
"status": {"code": 200, "message": "ok"},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
assert [event.type for event in resumed_events] == ["text_delta", "done"]
|
||||
assert resumed_events[0].text == "Resumed answer."
|
||||
assert service.client.requests[1]["chatId"] == service.client.requests[0]["chatId"]
|
||||
assert service.client.requests[1]["messages"] == [{"role": "user", "content": "pro"}]
|
||||
assert service._state.pending_interaction is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fastgpt_provider_cancel_result_clears_pending_interaction(monkeypatch):
|
||||
monkeypatch.setattr("providers.llm.fastgpt.AsyncChatClient", _FakeAsyncChatClient)
|
||||
monkeypatch.setattr("providers.llm.fastgpt.aiter_stream_events", _fake_aiter_stream_events)
|
||||
|
||||
_FakeAsyncChatClient.responses = [
|
||||
_FakeResponse(
|
||||
[
|
||||
SimpleNamespace(
|
||||
kind="interactive",
|
||||
data={
|
||||
"type": "userInput",
|
||||
"params": {"inputForm": [{"name": "name", "label": "Name"}]},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
service = FastGPTLLMService(api_key="key", base_url="https://fastgpt.example")
|
||||
await service.connect()
|
||||
|
||||
initial_events = [event async for event in service.generate_stream([LLMMessage(role="user", content="Start")])]
|
||||
call_id = initial_events[0].tool_call["id"]
|
||||
|
||||
resumed_events = [
|
||||
event
|
||||
async for event in service.resume_after_client_tool_result(
|
||||
call_id,
|
||||
{
|
||||
"tool_call_id": call_id,
|
||||
"name": "fastgpt.interactive",
|
||||
"output": {"action": "cancel", "result": {}},
|
||||
"status": {"code": 499, "message": "user_cancelled"},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
assert [event.type for event in resumed_events] == ["done"]
|
||||
assert service._state.pending_interaction is None
|
||||
Reference in New Issue
Block a user