Update environment configuration and enhance API endpoints
- Changed ANALYSIS_SERVICE_URL to localhost for local development. - Updated ANALYSIS_AUTH_TOKEN and APP_ID for improved security. - Added new functions in endpoints.py for form extraction and stage code normalization. - Enhanced chat handling to support form updates and improved event streaming. - Updated models to include new fields for form update handling.
This commit is contained in:
9
.env
9
.env
@@ -2,11 +2,8 @@ DATABASE_URL=sqlite:///./test.db
|
||||
SECRET_KEY=your_secret_key
|
||||
DEBUG=True
|
||||
|
||||
ANALYSIS_SERVICE_URL=http://101.89.151.141:3000/api/v1/chat/completions
|
||||
ANALYSIS_AUTH_TOKEN=fastgpt-hSPnXMoBNGVAEpTLkQT3YfAnN26gQSyvLd4ABL1MRDoh68nL4RDlopFHXqmH8
|
||||
APP_ID=683ea1bc86197e19f71fc1ae
|
||||
DELETE_SESSION_URL=http://101.89.151.141:3000/api/core/chat/delHistory?chatId={chatId}&appId={appId}
|
||||
DELETE_CHAT_URL=http://101.89.151.141:3000/api/core/chat/item/delete?contentId={contentId}&chatId={chatId}&appId={appId}
|
||||
GET_CHAT_RECORDS_URL=http://101.89.151.141:3000/api/core/chat/getPaginationRecords
|
||||
ANALYSIS_SERVICE_URL=http://127.0.0.1:3030
|
||||
ANALYSIS_AUTH_TOKEN=fastgpt-r13smJwPgXfGj1HDfc4SWAvIoNrL5Wc6o0BYnezqBs7hgzPdQ7Q34hVl2FJc0R
|
||||
APP_ID=6a310def7132e9f7d592dabb
|
||||
|
||||
VOICE_CONFIG=config/voice-fastgpt-state-xfyunSuperTTS.json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from ..schemas.models import ProcessRequest_chat, ProcessResponse_chat, ProcessRequest_get, ProcessResponse_get, ProcessRequest_set, ProcessResponse_set, ProcessResponse_delete_session, ProcessRequest_delete_session
|
||||
from fastgpt_client import AsyncChatClient
|
||||
from fastgpt_client import AsyncChatClient, aiter_stream_events
|
||||
from fastgpt_client.exceptions import (
|
||||
APIError, AuthenticationError, RateLimitError, ValidationError
|
||||
)
|
||||
@@ -12,6 +12,7 @@ import json
|
||||
import re
|
||||
|
||||
router = APIRouter()
|
||||
FORM_EXTRACT_MODULE_NAME = "文本内容提取事故信息"
|
||||
STATUS_CODE_MAP = {
|
||||
'0000': '结束通话',
|
||||
'0001': '转接人工',
|
||||
@@ -34,6 +35,19 @@ STATUS_CODE_MAP = {
|
||||
'2016': '确认双车中的车牌'
|
||||
}
|
||||
|
||||
def normalize_stage_code(stage_code: str) -> str:
|
||||
"""Normalize FastGPT stage codes to external API stage codes."""
|
||||
if stage_code in ['3001', '3002', '1002']:
|
||||
return '1002'
|
||||
if stage_code == '2006':
|
||||
return '2004'
|
||||
if stage_code == '2017':
|
||||
return '2016'
|
||||
if stage_code == '2020':
|
||||
return '0002'
|
||||
return stage_code
|
||||
|
||||
|
||||
def extract_state_and_content(data1: str) -> dict | None:
|
||||
"""
|
||||
Extracts the state and content from a string in the format <state>STATE</state>content.
|
||||
@@ -47,7 +61,7 @@ def extract_state_and_content(data1: str) -> dict | None:
|
||||
"""
|
||||
data1 = data1.strip()
|
||||
regex = r"<state>(.*?)</state>(.*)"
|
||||
match = re.search(regex, data1)
|
||||
match = re.search(regex, data1, flags=re.DOTALL)
|
||||
|
||||
if match:
|
||||
return {
|
||||
@@ -56,6 +70,38 @@ def extract_state_and_content(data1: str) -> dict | None:
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def extract_form_update_from_flow_nodes(nodes) -> str:
|
||||
"""Extract form update data from the configured FastGPT content-extract node."""
|
||||
if not isinstance(nodes, list):
|
||||
return ""
|
||||
|
||||
for node in nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
if node.get("moduleName") != FORM_EXTRACT_MODULE_NAME:
|
||||
continue
|
||||
|
||||
extract_result = node.get("extractResult", {})
|
||||
if not isinstance(extract_result, dict):
|
||||
return ""
|
||||
|
||||
form_update = extract_result.get("formUpdate", "")
|
||||
if isinstance(form_update, str):
|
||||
return form_update
|
||||
if form_update:
|
||||
return json.dumps(form_update, ensure_ascii=False)
|
||||
return ""
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def format_set_info_input(payload: dict, include_input_info: bool) -> str:
|
||||
"""Build optional setInfo input for FastGPT helper calls."""
|
||||
if not include_input_info:
|
||||
return ""
|
||||
return f"<setInfo>{json.dumps(payload, ensure_ascii=False)}</setInfo>"
|
||||
|
||||
async def delete_last_two_chat_records(
|
||||
client: AsyncChatClient,
|
||||
session_id: str
|
||||
@@ -112,6 +158,8 @@ async def chat(
|
||||
"""Handle chat completion request."""
|
||||
json_data = request.model_dump()
|
||||
logger.info(f"用户请求信息ProcessRequest_chat: {json_data}, stream={stream}")
|
||||
need_form_update = json_data.get('needFormUpdate', False)
|
||||
chat_variables = {'needFormUpdate': need_form_update}
|
||||
|
||||
if stream:
|
||||
async def event_generator():
|
||||
@@ -121,73 +169,80 @@ async def chat(
|
||||
messages=[{"role": "user", "content": json_data['text']}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=True,
|
||||
detail=True
|
||||
detail=True,
|
||||
variables=chat_variables
|
||||
)
|
||||
|
||||
buffer = ""
|
||||
state_code_found = False
|
||||
module_form_sent = False
|
||||
|
||||
def flush_text_delta(text: str):
|
||||
return create_sse_event("text_delta", {"text": text})
|
||||
|
||||
def flush_form_update(form_update: str):
|
||||
return create_sse_event("formUpdate", {"formUpdate": form_update})
|
||||
|
||||
async for chunk in response.aiter_lines():
|
||||
if chunk.startswith('data: '):
|
||||
data_str = chunk[6:].strip()
|
||||
if data_str == '[DONE]':
|
||||
break
|
||||
async for event in aiter_stream_events(response):
|
||||
try:
|
||||
if event.kind == "flowResponses" and not module_form_sent:
|
||||
form_update = extract_form_update_from_flow_nodes(event.data)
|
||||
if form_update:
|
||||
yield flush_form_update(form_update)
|
||||
module_form_sent = True
|
||||
continue
|
||||
|
||||
if event.kind not in {"answer", "fastAnswer", "data"}:
|
||||
continue
|
||||
|
||||
data = event.data
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
try:
|
||||
delta_content = data['choices'][0]['delta'].get('content', '')
|
||||
except (KeyError, IndexError):
|
||||
delta_content = ''
|
||||
if delta_content:
|
||||
buffer += delta_content
|
||||
|
||||
if not state_code_found:
|
||||
# Check for <state>XXXX</state> pattern
|
||||
match = re.search(r"<state>(.*?)</state>", buffer)
|
||||
if match:
|
||||
state_code = match.group(1)
|
||||
|
||||
# Apply logic to map/adjust state code
|
||||
nextStageCode = state_code
|
||||
if nextStageCode in ['3001', '3002', '1002']:
|
||||
nextStageCode = '1002'
|
||||
elif nextStageCode == '2006':
|
||||
nextStageCode = '2004'
|
||||
elif nextStageCode == '2017':
|
||||
nextStageCode = '2016'
|
||||
elif nextStageCode == '2020':
|
||||
nextStageCode = '0002'
|
||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
||||
|
||||
# Send stage code event
|
||||
yield create_sse_event("stage_code", {
|
||||
"nextStageCode": nextStageCode,
|
||||
"nextStage": nextStage
|
||||
})
|
||||
|
||||
state_code_found = True
|
||||
|
||||
# Send remaining content as text_delta
|
||||
remaining_content = buffer[match.end():]
|
||||
if remaining_content:
|
||||
yield create_sse_event("text_delta", {"text": remaining_content})
|
||||
buffer = "" # Clear buffer after extracting state
|
||||
else:
|
||||
# State code already found, just stream text
|
||||
yield create_sse_event("text_delta", {"text": delta_content})
|
||||
buffer = "" # Do not buffer text after state found
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(data)
|
||||
logger.error(f"Error processing chunk: {e}")
|
||||
delta_content = data['choices'][0]['delta'].get('content', '')
|
||||
except (KeyError, IndexError):
|
||||
delta_content = ''
|
||||
if not delta_content:
|
||||
continue
|
||||
|
||||
buffer += delta_content
|
||||
|
||||
if not state_code_found:
|
||||
# Check for <state>XXXX</state> pattern
|
||||
match = re.search(r"<state>(.*?)</state>", buffer, flags=re.DOTALL)
|
||||
if match:
|
||||
state_code = match.group(1)
|
||||
|
||||
# Apply logic to map/adjust state code
|
||||
nextStageCode = normalize_stage_code(state_code)
|
||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
||||
|
||||
# Send stage code event
|
||||
yield create_sse_event("stage_code", {
|
||||
"nextStageCode": nextStageCode,
|
||||
"nextStage": nextStage
|
||||
})
|
||||
|
||||
state_code_found = True
|
||||
|
||||
# Send remaining content as text_delta
|
||||
remaining_content = buffer[match.end():]
|
||||
if remaining_content:
|
||||
yield flush_text_delta(remaining_content)
|
||||
buffer = "" # Clear buffer after extracting state
|
||||
else:
|
||||
yield flush_text_delta(delta_content)
|
||||
buffer = ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing stream event: {e}")
|
||||
continue
|
||||
|
||||
# If stream ends and no state code found (unlikely if format is strict),
|
||||
# we might want to send what we have
|
||||
if not state_code_found and buffer:
|
||||
yield create_sse_event("text_delta", {"text": buffer})
|
||||
yield create_sse_event("text_delta", {"text": buffer})
|
||||
|
||||
yield create_sse_event("done", {"status": "completed"})
|
||||
|
||||
@@ -203,7 +258,8 @@ async def chat(
|
||||
messages=[{"role": "user", "content": json_data['text']}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=False,
|
||||
detail=True
|
||||
detail=True,
|
||||
variables=chat_variables
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
@@ -281,28 +337,18 @@ async def chat(
|
||||
|
||||
logger.debug(f"State variables: {data.get('newVariables', {})}")
|
||||
|
||||
nextStageCode = data['newVariables']['status_code']
|
||||
|
||||
# 有一些情况需要调整nextStageCode
|
||||
if nextStageCode in ['3001', '3002', '1002']:
|
||||
nextStageCode = '1002'
|
||||
elif nextStageCode == '2006':
|
||||
nextStageCode = '2004'
|
||||
elif nextStageCode == '2017':
|
||||
nextStageCode = '2016'
|
||||
elif nextStageCode == '2020':
|
||||
nextStageCode = '0002'
|
||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
||||
|
||||
# Parse content - sometimes content is a string, sometimes it is a list
|
||||
content_stage_code = None
|
||||
if isinstance(content, list):
|
||||
logger.debug("content是一个list")
|
||||
content = content[0]['text']['content']
|
||||
elif isinstance(content, str):
|
||||
|
||||
if isinstance(content, str):
|
||||
logger.debug("content是一个str")
|
||||
state_and_content = extract_state_and_content(content)
|
||||
if state_and_content:
|
||||
logger.debug(f"解析后的state和content为: {state_and_content}")
|
||||
content_stage_code = state_and_content['state']
|
||||
content = state_and_content['content']
|
||||
else:
|
||||
raise ValueError("大模型回复中的state解析失败")
|
||||
@@ -310,10 +356,16 @@ async def chat(
|
||||
logger.error(f"content既不是list也不是str, type: {type(content)}")
|
||||
raise ValueError("大模型回复不是list也不是str")
|
||||
|
||||
nextStageCode = content_stage_code or data['newVariables']['status_code']
|
||||
nextStageCode = normalize_stage_code(nextStageCode)
|
||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
||||
form_update = extract_form_update_from_flow_nodes(data.get("responseData", []))
|
||||
|
||||
return ProcessResponse_chat(
|
||||
sessionId=json_data['sessionId'],
|
||||
timeStamp=json_data['timeStamp'],
|
||||
outputText=content,
|
||||
formUpdate=form_update,
|
||||
nextStage=nextStage,
|
||||
nextStageCode=nextStageCode,
|
||||
code="200",
|
||||
@@ -340,11 +392,16 @@ async def set_info(
|
||||
):
|
||||
"""Set information in chat state."""
|
||||
json_data = request.model_dump()
|
||||
set_info_payload = {'key': json_data['key'], 'value': json_data['value']}
|
||||
set_info_input = format_set_info_input(
|
||||
set_info_payload,
|
||||
json_data.get('includeInputInfo', False)
|
||||
)
|
||||
|
||||
try:
|
||||
# Get current state
|
||||
response = await client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": ""}],
|
||||
messages=[{"role": "user", "content": set_info_input}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=False,
|
||||
detail=True
|
||||
@@ -387,7 +444,7 @@ async def set_info(
|
||||
|
||||
# Update state using SDK
|
||||
response = await client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": ""}],
|
||||
messages=[{"role": "user", "content": set_info_input}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=False,
|
||||
detail=True,
|
||||
@@ -421,11 +478,16 @@ async def get_info(
|
||||
):
|
||||
"""Get information from chat state."""
|
||||
json_data = request.model_dump()
|
||||
get_info_payload = {'key': json_data['key']}
|
||||
get_info_input = format_set_info_input(
|
||||
get_info_payload,
|
||||
json_data.get('includeInputInfo', False)
|
||||
)
|
||||
|
||||
try:
|
||||
# Get current state
|
||||
response = await client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": ""}],
|
||||
messages=[{"role": "user", "content": get_info_input}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=False,
|
||||
detail=True
|
||||
|
||||
@@ -5,11 +5,13 @@ class ProcessRequest_chat(BaseModel):
|
||||
sessionId: str = Field(..., max_length=64)
|
||||
timeStamp: str = Field(..., max_length=32)
|
||||
text: str = Field(...)
|
||||
needFormUpdate: bool = False
|
||||
|
||||
class ProcessResponse_chat(BaseModel):
|
||||
sessionId: str = Field(..., max_length=64)
|
||||
timeStamp: str = Field(..., max_length=32)
|
||||
outputText: str = Field(...)
|
||||
formUpdate: str = Field(default="")
|
||||
nextStage: str = Field(..., max_length=32)
|
||||
nextStageCode: str = Field(..., max_length=4)
|
||||
code: str = Field(..., max_length=4)
|
||||
@@ -19,6 +21,7 @@ class ProcessRequest_get(BaseModel):
|
||||
sessionId: str = Field(..., max_length=64)
|
||||
timeStamp: str = Field(..., max_length=32)
|
||||
key: str = Field(...)
|
||||
includeInputInfo: bool = False
|
||||
|
||||
class ProcessResponse_get(BaseModel):
|
||||
sessionId: str = Field(..., max_length=64)
|
||||
@@ -32,6 +35,7 @@ class ProcessRequest_set(BaseModel):
|
||||
timeStamp: str = Field(..., max_length=32)
|
||||
key: str = Field(...)
|
||||
value: str = Field(...)
|
||||
includeInputInfo: bool = False
|
||||
|
||||
class ProcessResponse_set(BaseModel):
|
||||
sessionId: str = Field(..., max_length=64)
|
||||
|
||||
@@ -3,27 +3,28 @@
|
||||
GET http://127.0.0.1:8080
|
||||
|
||||
HTTP/1.1 200 - OK
|
||||
connection: close
|
||||
date: Wed, 17 Jun 2026 00:37:02 GMT
|
||||
server: uvicorn
|
||||
content-length: 32
|
||||
content-type: application/json
|
||||
date: Thu, 08 Jan 2026 08:58:09 GMT
|
||||
server: uvicorn
|
||||
connection: close
|
||||
###
|
||||
POST http://127.0.0.1:8080/chat
|
||||
content-type: application/json
|
||||
|
||||
{
|
||||
"sessionId": "a1002",
|
||||
"sessionId": "a1100",
|
||||
"timeStamp": "202503310303",
|
||||
"text": "【拍摄完成】"
|
||||
"text": "继续",
|
||||
"needFormTags": true
|
||||
}
|
||||
|
||||
HTTP/1.1 200 - OK
|
||||
connection: close
|
||||
content-length: 205
|
||||
content-type: application/json
|
||||
date: Thu, 08 Jan 2026 08:59:37 GMT
|
||||
date: Wed, 17 Jun 2026 00:37:26 GMT
|
||||
server: uvicorn
|
||||
content-length: 274
|
||||
content-type: application/json
|
||||
connection: close
|
||||
###
|
||||
POST http://127.0.0.1:8080/get_info
|
||||
content-type: application/json
|
||||
@@ -35,11 +36,11 @@ content-type: application/json
|
||||
}
|
||||
|
||||
HTTP/1.1 200 - OK
|
||||
connection: close
|
||||
content-length: 97
|
||||
content-type: application/json
|
||||
date: Thu, 08 Jan 2026 09:27:05 GMT
|
||||
date: Wed, 17 Jun 2026 00:27:12 GMT
|
||||
server: uvicorn
|
||||
content-length: 108
|
||||
content-type: application/json
|
||||
connection: close
|
||||
###
|
||||
POST http://127.0.0.1:8080/set_info
|
||||
content-type: application/json
|
||||
|
||||
Reference in New Issue
Block a user