Start LangGraph backend migration foundation
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
GET http://101.89.151.141:3000
|
||||
@fastgptBaseUrl = http://127.0.0.1:3000
|
||||
@fastgptApiKey = replace-with-local-api-key
|
||||
|
||||
GET {{fastgptBaseUrl}}
|
||||
###
|
||||
POST http://101.89.151.141:3000/api/v1/chat/completions
|
||||
POST {{fastgptBaseUrl}}/api/v1/chat/completions
|
||||
content-type: application/json
|
||||
Authorization: Bearer fastgpt-xCH4CaEoNEyVtq7fkBEI5UP3O6sABKdpGszTtSYk4R2TVW5VgrPp1YPfuLX1iH
|
||||
Authorization: Bearer {{fastgptApiKey}}
|
||||
|
||||
{
|
||||
|
||||
@@ -23,4 +26,4 @@ content-type: application/json; charset=utf-8
|
||||
etag: "s14v22uu1g5f"
|
||||
content-length: 219
|
||||
date: Fri, 20 Jun 2025 02:37:16 GMT
|
||||
connection: close
|
||||
connection: close
|
||||
|
||||
116
test/api/test_chat_sse_contract.py
Normal file
116
test/api/test_chat_sse_contract.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api.endpoints import chat
|
||||
from src.backends.chat import ChatInput, ChatResult, FormUpdate, TextDelta
|
||||
from src.schemas.models import ProcessRequest_chat
|
||||
|
||||
|
||||
def make_request(**overrides):
|
||||
payload = {
|
||||
"sessionId": "session-001",
|
||||
"timeStamp": "20260726120000",
|
||||
"text": "发生了交通事故",
|
||||
"needFormUpdate": True,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return ProcessRequest_chat(**payload)
|
||||
|
||||
|
||||
async def response_text(response):
|
||||
chunks = []
|
||||
async for chunk in response.body_iterator:
|
||||
chunks.append(chunk.decode() if isinstance(chunk, bytes) else chunk)
|
||||
return "".join(chunks)
|
||||
|
||||
|
||||
def parse_sse(body):
|
||||
events = []
|
||||
for block in body.strip().split("\n\n"):
|
||||
lines = block.splitlines()
|
||||
event = lines[0].removeprefix("event: ")
|
||||
data = json.loads(lines[1].removeprefix("data: "))
|
||||
events.append((event, data))
|
||||
return events
|
||||
|
||||
|
||||
class OrderedBackend:
|
||||
async def stream(self, chat_input: ChatInput):
|
||||
yield TextDelta("<sta")
|
||||
yield TextDelta("te>1002</state>")
|
||||
yield FormUpdate({"jdcsl": 2})
|
||||
yield TextDelta("第一句。")
|
||||
yield TextDelta("第二句。")
|
||||
|
||||
async def complete(self, chat_input: ChatInput):
|
||||
return ChatResult("<state>1002</state>第一句。第二句。", "1002", {"jdcsl": 2})
|
||||
|
||||
|
||||
class MissingPrefixBackend:
|
||||
async def stream(self, chat_input: ChatInput):
|
||||
yield TextDelta("没有状态前缀")
|
||||
|
||||
async def complete(self, chat_input: ChatInput):
|
||||
return ChatResult("没有状态前缀")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sse_success_event_order_and_cardinality():
|
||||
response = await chat(make_request(), stream=True, backend=OrderedBackend())
|
||||
events = parse_sse(await response_text(response))
|
||||
names = [name for name, _ in events]
|
||||
|
||||
assert names == [
|
||||
"stage_code",
|
||||
"formUpdate",
|
||||
"text_delta",
|
||||
"text_delta",
|
||||
"done",
|
||||
]
|
||||
assert names.count("stage_code") == 1
|
||||
assert names.count("formUpdate") == 1
|
||||
assert names.count("done") == 1
|
||||
assert "error" not in names
|
||||
assert "".join(data["text"] for name, data in events if name == "text_delta") == (
|
||||
"第一句。第二句。"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_use_text_chunk_only_changes_delta_boundaries():
|
||||
request = make_request(useTextChunk=True)
|
||||
response = await chat(request, stream=True, backend=OrderedBackend())
|
||||
events = parse_sse(await response_text(response))
|
||||
|
||||
assert "".join(data["text"] for name, data in events if name == "text_delta") == (
|
||||
"第一句。第二句。"
|
||||
)
|
||||
assert [name for name, _ in events].count("done") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_stream_prefix_characterizes_current_legacy_behavior():
|
||||
response = await chat(
|
||||
make_request(needFormUpdate=False),
|
||||
stream=True,
|
||||
backend=MissingPrefixBackend(),
|
||||
)
|
||||
events = parse_sse(await response_text(response))
|
||||
|
||||
assert [name for name, _ in events] == ["text_delta", "done"]
|
||||
assert events[0][1]["text"] == "没有状态前缀"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_non_stream_prefix_returns_compatible_business_error():
|
||||
response = await chat(
|
||||
make_request(needFormUpdate=False),
|
||||
stream=False,
|
||||
backend=MissingPrefixBackend(),
|
||||
)
|
||||
|
||||
assert response.code == "500"
|
||||
assert response.outputText == ""
|
||||
assert response.nextStageCode == ""
|
||||
assert response.msg == "大模型服务返回消息不完整"
|
||||
145
test/api/test_info_contract.py
Normal file
145
test/api/test_info_contract.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api import endpoints
|
||||
from src.api.endpoints import get_info, set_info
|
||||
from src.schemas.models import ProcessRequest_get, ProcessRequest_set
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeInfoClient:
|
||||
def __init__(self, state):
|
||||
self.state = state
|
||||
self.completion_calls = []
|
||||
|
||||
async def create_chat_completion(self, **kwargs):
|
||||
self.completion_calls.append(kwargs)
|
||||
if "variables" in kwargs:
|
||||
self.state = kwargs["variables"]["state"]
|
||||
return FakeResponse({"newVariables": {"state": self.state}})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def skip_helper_record_deletion(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def fake_delete(client, session_id):
|
||||
calls.append(session_id)
|
||||
|
||||
monkeypatch.setattr(endpoints, "delete_last_two_chat_records", fake_delete)
|
||||
return calls
|
||||
|
||||
|
||||
def make_set_request(**overrides):
|
||||
payload = {
|
||||
"sessionId": "session-001",
|
||||
"timeStamp": "20260726120000",
|
||||
"key": "hphm1",
|
||||
"value": "<PLATE_1>",
|
||||
}
|
||||
payload.update(overrides)
|
||||
return ProcessRequest_set(**payload)
|
||||
|
||||
|
||||
def make_get_request(**overrides):
|
||||
payload = {
|
||||
"sessionId": "session-001",
|
||||
"timeStamp": "20260726120000",
|
||||
"key": "all",
|
||||
}
|
||||
payload.update(overrides)
|
||||
return ProcessRequest_get(**payload)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_info_reads_then_writes_fastgpt_state(
|
||||
skip_helper_record_deletion,
|
||||
):
|
||||
client = FakeInfoClient({"hphm1": "<PLATE_OLD>", "jdcsl": 1})
|
||||
|
||||
response = await set_info(make_set_request(), client=client)
|
||||
|
||||
assert response.code == "200"
|
||||
assert client.state == {"hphm1": "<PLATE_1>", "jdcsl": 1}
|
||||
assert len(client.completion_calls) == 2
|
||||
assert client.completion_calls[1]["variables"]["state"] == client.state
|
||||
assert skip_helper_record_deletion == ["session-001", "session-001"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_info_include_input_info_keeps_legacy_magic_payload(
|
||||
skip_helper_record_deletion,
|
||||
):
|
||||
client = FakeInfoClient({})
|
||||
|
||||
await set_info(make_set_request(includeInputInfo=True), client=client)
|
||||
|
||||
message = client.completion_calls[0]["messages"][0]["content"]
|
||||
assert message == '<setInfo>{"key": "hphm1", "value": "<PLATE_1>"}</setInfo>'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_all_keeps_json_string_and_boolean_encoding(
|
||||
skip_helper_record_deletion,
|
||||
):
|
||||
client = FakeInfoClient(
|
||||
{
|
||||
"ywrysw": False,
|
||||
"ywfjdc": True,
|
||||
"jdcsl": 2,
|
||||
"xm1": "<PERSON_1>",
|
||||
"hphm1": "<PLATE_1>",
|
||||
"xm2": "<PERSON_2>",
|
||||
}
|
||||
)
|
||||
|
||||
response = await get_info(make_get_request(), client=client)
|
||||
value = json.loads(response.value)
|
||||
|
||||
assert response.code == "200"
|
||||
assert isinstance(response.value, str)
|
||||
assert value["acdinfo"]["ywrysw"] == "0"
|
||||
assert value["acdinfo"]["ywfjdc"] == "1"
|
||||
assert value["acdinfo"]["jdcsl"] == 2
|
||||
assert value["acdhuman1"]["xm1"] == "<PERSON_1>"
|
||||
assert value["acdhuman2"]["xm2"] == "<PERSON_2>"
|
||||
assert skip_helper_record_deletion == ["session-001"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_unknown_key_returns_json_encoded_empty_string(
|
||||
skip_helper_record_deletion,
|
||||
):
|
||||
client = FakeInfoClient({})
|
||||
|
||||
response = await get_info(
|
||||
make_get_request(key="unknown_legacy_key"),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert response.code == "200"
|
||||
assert response.value == '""'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_fastgpt_shape_error_keeps_legacy_business_error():
|
||||
class InvalidClient:
|
||||
async def create_chat_completion(self, **kwargs):
|
||||
return FakeResponse({})
|
||||
|
||||
response = await get_info(make_get_request(), client=InvalidClient())
|
||||
|
||||
assert response.code == "500"
|
||||
assert response.value == ""
|
||||
assert response.msg == "大模型服务器无响应"
|
||||
Reference in New Issue
Block a user