Start LangGraph backend migration foundation

This commit is contained in:
Xin Wang
2026-07-27 17:21:29 +08:00
parent 5c719ed2ea
commit 1c8e9da486
33 changed files with 1859 additions and 86 deletions

View File

@@ -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

View 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 == "大模型服务返回消息不完整"

View 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 == "大模型服务器无响应"