66- GET /chat/messages/{article_doc_id}: Query chat history
77- DELETE /chat/messages/{article_doc_id}: Clear chat content
88"""
9+
910import json
1011import logging
1112from datetime import datetime
12- from typing import List
1313
1414from fastapi import APIRouter , Depends
1515from fastapi .responses import StreamingResponse
1616from langchain_core .documents import Document
1717from langchain_core .messages import HumanMessage , SystemMessage
1818from sse_starlette import EventSourceResponse
1919
20- from bisheng .api .services .workstation import (
21- WorkstationConversation , WorkstationMessage
22- )
23- from bisheng .api .v1 .schemas import resp_200 , ChatResponse
20+ from bisheng .api .services .workstation import WorkstationConversation , WorkstationMessage
21+ from bisheng .api .v1 .schemas import ChatResponse , resp_200
2422from bisheng .channel .domain .schemas .channel_chat_schema import ChannelArticleChatRequest
2523from bisheng .channel .domain .services .article_es_service import ArticleEsService
2624from bisheng .channel .domain .services .channel_chat_service import ChannelChatService
2927from bisheng .common .errcode import BaseErrorCode
3028from bisheng .common .errcode .channel import ChannelChatConversationNotFoundError
3129from bisheng .common .errcode .http_error import ServerError , UnAuthorizedError
32- from bisheng .common .schemas .api import resp_500 , SSEResponse
33- from bisheng .llm .domain .utils import extract_reasoning_content
30+ from bisheng .common .schemas .api import SSEResponse , resp_500
3431from bisheng .database .constants import MessageCategory
3532from bisheng .database .models .message import ChatMessage , ChatMessageDao
3633from bisheng .database .models .session import MessageSession
34+ from bisheng .llm .domain .utils import extract_reasoning_content
3735
3836logger = logging .getLogger (__name__ )
3937
40- router = APIRouter (prefix = ' /chat' , tags = [' Channel Article Chat' ])
38+ router = APIRouter (prefix = " /chat" , tags = [" Channel Article Chat" ])
4139
4240
4341def custom_json_serializer (obj ):
4442 if isinstance (obj , datetime ):
4543 return obj .isoformat ()
46- raise TypeError (f' Type { type (obj )} not serializable' )
44+ raise TypeError (f" Type { type (obj )} not serializable" )
4745
4846
4947def user_message (msgId , conversationId , sender , text ):
50- msg = json .dumps ({
51- 'message' : {
52- 'messageId' : msgId ,
53- 'conversationId' : conversationId ,
54- 'sender' : sender ,
55- 'text' : text
56- },
57- 'created' : True
58- })
59- return f'event: message\n data: { msg } \n \n '
48+ msg = json .dumps (
49+ {
50+ "message" : {"messageId" : msgId , "conversationId" : conversationId , "sender" : sender , "text" : text },
51+ "created" : True ,
52+ }
53+ )
54+ return f"event: message\n data: { msg } \n \n "
6055
6156
6257def step_message (stepId , runId , index , msgId ):
63- msg = json .dumps ({
64- 'event' : 'on_run_step' ,
65- 'data' : {
66- 'id' : stepId ,
67- 'runId' : runId ,
68- 'type' : 'message_creation' ,
69- 'index' : index ,
70- 'stepDetails' : {
71- 'type' : 'message_creation' ,
72- 'message_creation' : {
73- 'message_id' : msgId
74- }
75- }
58+ msg = json .dumps (
59+ {
60+ "event" : "on_run_step" ,
61+ "data" : {
62+ "id" : stepId ,
63+ "runId" : runId ,
64+ "type" : "message_creation" ,
65+ "index" : index ,
66+ "stepDetails" : {"type" : "message_creation" , "message_creation" : {"message_id" : msgId }},
67+ },
7668 }
77- } )
78- return f' event: message\n data: { msg } \n \n '
69+ )
70+ return f" event: message\n data: { msg } \n \n "
7971
8072
8173def delta (id , delta ):
82- return {'id' : id , ' delta' : delta }
74+ return {"id" : id , " delta" : delta }
8375
8476
85- async def final_message (conversation : MessageSession , title : str , requestMessage : ChatMessage ,
86- text : str , error : bool , modelName : str ,
87- source_document : List [Document ] = None ):
77+ async def final_message (
78+ conversation : MessageSession ,
79+ title : str ,
80+ requestMessage : ChatMessage ,
81+ text : str ,
82+ error : bool ,
83+ modelName : str ,
84+ source_document : list [Document ] = None ,
85+ ):
8886 responseMessage = await ChatMessageDao .ainsert_one (
8987 ChatMessage (
9088 user_id = conversation .user_id ,
9189 chat_id = conversation .chat_id ,
9290 flow_id = conversation .flow_id ,
93- type = ' assistant' ,
91+ type = " assistant" ,
9492 is_bot = True ,
9593 message = text ,
96- category = ' answer' ,
94+ category = " answer" ,
9795 sender = modelName ,
98- extra = json .dumps ({
99- 'parentMessageId' : requestMessage .id ,
100- 'error' : error
101- }),
102- source = 0
103- ))
96+ extra = json .dumps ({"parentMessageId" : requestMessage .id , "error" : error }),
97+ source = 0 ,
98+ )
99+ )
104100
105101 msg = json .dumps (
106102 {
107- ' final' : True ,
108- ' conversation' : WorkstationConversation .from_chat_session (conversation ).model_dump (),
109- ' title' : title ,
110- ' requestMessage' : (await WorkstationMessage .from_chat_message (requestMessage )).model_dump (),
111- ' responseMessage' : (await WorkstationMessage .from_chat_message (responseMessage )).model_dump (),
103+ " final" : True ,
104+ " conversation" : WorkstationConversation .from_chat_session (conversation ).model_dump (),
105+ " title" : title ,
106+ " requestMessage" : (await WorkstationMessage .from_chat_message (requestMessage )).model_dump (),
107+ " responseMessage" : (await WorkstationMessage .from_chat_message (responseMessage )).model_dump (),
112108 },
113- default = custom_json_serializer )
114- return f'event: message\n data: { msg } \n \n '
109+ default = custom_json_serializer ,
110+ )
111+ return f"event: message\n data: { msg } \n \n "
115112
116113
117- @router .post (' /completions' , summary = ' Channel Article AI Assistant Chat' )
114+ @router .post (" /completions" , summary = " Channel Article AI Assistant Chat" )
118115async def chat_completions (
119- data : ChannelArticleChatRequest ,
120- login_user : UserPayload = Depends (UserPayload .get_login_user ),
116+ data : ChannelArticleChatRequest ,
117+ login_user : UserPayload = Depends (UserPayload .get_login_user ),
121118):
122119 """
123120 Channel Article AI Assistant Chat API, returns SSE stream.
@@ -145,7 +142,7 @@ async def chat_completions(
145142 error_response = e if isinstance (e , BaseErrorCode ) else ServerError (msg = str (e ))
146143 return EventSourceResponse (iter ([error_response .to_sse_event_instance ()]))
147144 except Exception as e :
148- logger .exception (f' Error in channel article chat setup: { e } ' )
145+ logger .exception (f" Error in channel article chat setup: { e } " )
149146 return EventSourceResponse (iter ([ServerError (exception = e ).to_sse_event_instance ()]))
150147
151148 async def event_stream ():
@@ -162,35 +159,27 @@ async def event_stream():
162159 user_prompt_template = (
163160 subscription_config .user_prompt
164161 if subscription_config and subscription_config .user_prompt
165- else (
166- "# 参考资料\n ```\n {article_content}\n ```\n # 用户问题\n {question}"
167- )
168- )
169- user_prompt = user_prompt_template .format (
170- article_content = article_content ,
171- question = data .text
162+ else ("# 参考资料\n ```\n {article_content}\n ```\n # 用户问题\n {question}" )
172163 )
164+ user_prompt = user_prompt_template .format (article_content = article_content , question = data .text )
173165 await ChatMessageDao .ainsert_one (
174166 ChatMessage (
175167 user_id = login_user .user_id ,
176168 chat_id = conversation .chat_id ,
177169 flow_id = data .article_doc_id ,
178- type = ' human' ,
170+ type = " human" ,
179171 is_bot = False ,
180- sender = ' User' ,
172+ sender = " User" ,
181173 message = json .dumps ({"query" : data .text }, ensure_ascii = False ),
182174 category = MessageCategory .QUESTION ,
183175 source = 0 ,
184- ))
176+ )
177+ )
185178 # Get chat history (excluding the latest one)
186179 history_messages = (await ChannelChatService .get_chat_history (conversationId , 8 ))[:- 1 ]
187180
188181 # Build LLM input
189- inputs = [
190- SystemMessage (content = system_prompt ),
191- * history_messages ,
192- HumanMessage (content = user_prompt )
193- ]
182+ inputs = [SystemMessage (content = system_prompt ), * history_messages , HumanMessage (content = user_prompt )]
194183
195184 answer = ""
196185 reasoning_answer = ""
@@ -200,56 +189,61 @@ async def event_stream():
200189 reasoning_content = extract_reasoning_content (chunk )
201190 answer += content
202191 reasoning_answer += reasoning_content
203- yield SSEResponse (data = ChatResponse (
204- category = MessageCategory .STREAM ,
205- message = {
206- "content" : content ,
207- "reasoning_content" : reasoning_content ,
208- },
209- type = "stream"
210- )).to_string ()
211-
212- yield SSEResponse (data = ChatResponse (
213- category = MessageCategory .STREAM ,
214- message = {
215- "content" : answer ,
216- "reasoning_content" : reasoning_answer
217- },
218- type = "end"
219- )).to_string ()
220-
221- # Append reasoning process to final result
222- await ChatMessageDao .ainsert_one (
192+ yield SSEResponse (
193+ data = ChatResponse (
194+ category = MessageCategory .STREAM ,
195+ message = {
196+ "content" : content ,
197+ "reasoning_content" : reasoning_content ,
198+ },
199+ type = "stream" ,
200+ )
201+ ).to_string ()
202+
203+ # Persist the answer BEFORE the end event so we can hand the client the
204+ # real ChatMessage id. The client renders the streamed answer under a
205+ # temporary placeholder id; without the real id, like/dislike clicked
206+ # before a reload writes to a non-existent row and silently vanishes.
207+ answer_message = await ChatMessageDao .ainsert_one (
223208 ChatMessage (
224209 category = MessageCategory .ANSWER ,
225- message = json .dumps ({
226- "content" : answer ,
227- "reasoning_content" : reasoning_answer
228- }, ensure_ascii = False ),
210+ message = json .dumps ({"content" : answer , "reasoning_content" : reasoning_answer }, ensure_ascii = False ),
229211 user_id = login_user .user_id ,
230212 chat_id = conversation .chat_id ,
231213 flow_id = data .article_doc_id ,
232214 type = "end" ,
233215 is_bot = True ,
234216 )
235217 )
218+
219+ yield SSEResponse (
220+ data = ChatResponse (
221+ category = MessageCategory .STREAM ,
222+ message = {
223+ "content" : answer ,
224+ "reasoning_content" : reasoning_answer ,
225+ "message_id" : answer_message .id ,
226+ },
227+ type = "end" ,
228+ )
229+ ).to_string ()
236230 except BaseErrorCode as e :
237231 yield e .to_sse_event_instance_str ()
238232 except Exception as e :
239- logger .exception (f' Error in channel article chat processing' )
233+ logger .exception (" Error in channel article chat processing" )
240234 yield ServerError (exception = e ).to_sse_event_instance_str ()
241235
242236 try :
243- return StreamingResponse (event_stream (), media_type = ' text/event-stream' )
237+ return StreamingResponse (event_stream (), media_type = " text/event-stream" )
244238 except Exception as e :
245- logger .exception (f' Error creating channel article chat stream: { e } ' )
239+ logger .exception (f" Error creating channel article chat stream: { e } " )
246240 return EventSourceResponse (iter ([ServerError (exception = e ).to_sse_event_instance ()]))
247241
248242
249- @router .get (' /messages/{article_doc_id}' , summary = ' Query Channel Article AI Assistant Chat History' )
243+ @router .get (" /messages/{article_doc_id}" , summary = " Query Channel Article AI Assistant Chat History" )
250244async def get_chat_history (
251- article_doc_id : str ,
252- login_user : UserPayload = Depends (UserPayload .get_login_user ),
245+ article_doc_id : str ,
246+ login_user : UserPayload = Depends (UserPayload .get_login_user ),
253247):
254248 """Query Channel Article AI Assistant Chat History Content"""
255249 messages = await ChannelChatService .get_chat_messages (article_doc_id , login_user )
@@ -258,10 +252,10 @@ async def get_chat_history(
258252 return resp_200 (data = messages )
259253
260254
261- @router .delete (' /messages/{article_doc_id}' , summary = ' Clear Channel Article AI Assistant Chat Content' )
255+ @router .delete (" /messages/{article_doc_id}" , summary = " Clear Channel Article AI Assistant Chat Content" )
262256async def clear_chat (
263- article_doc_id : str ,
264- login_user : UserPayload = Depends (UserPayload .get_login_user ),
257+ article_doc_id : str ,
258+ login_user : UserPayload = Depends (UserPayload .get_login_user ),
265259):
266260 """Clear Channel Article AI Assistant Chat Content"""
267261 try :
0 commit comments