1- """LangChain-based assistant backend.
2-
3- This module provides:
4- - Factory to build LangChain ChatModel from LLMConfig.
5- - A simple streaming generator that yields JSON-line events compatible with
6- the existing assistant event protocol (currently only `token` events).
7-
8- It is intentionally minimal for the first migration step and does not yet
9- implement tools / ReAct. Those will be added incrementally on top.
10- """
111
122from __future__ import annotations
133
5747_ACTION_TAG_RE = re .compile (r"<Action>(.*?)</Action>" , re .IGNORECASE | re .DOTALL )
5848_CODE_FENCE_RE = re .compile (r"```(?:json)?\s*(.*?)```" , re .IGNORECASE | re .DOTALL )
5949_JSON_BLOCK_RE = re .compile (r"Action\s*:?\s*(\{.*\})" , re .IGNORECASE | re .DOTALL )
60- _PROTOCOL_TAGS = ("action" , "thought" , "finalanswer" )
50+ # React 文本协议仅保留 Action,一律使用 <Action>{...}</Action> 格式声明工具调用
51+ _PROTOCOL_TAGS = ("action" ,)
6152
6253MAX_REACT_STEPS = 8
6354
@@ -135,22 +126,6 @@ def _parse_action_payload(text: str) -> Optional[Tuple[str, Dict[str, Any]]]:
135126 return tool_name .strip (), args
136127
137128
138- def _clean_react_output (text : str ) -> str :
139- """清理 React 输出中的残留协议标记,但保留必要的空白字符。"""
140-
141- if not text :
142- return text
143-
144- # 移除 Action 标签(已被系统处理)
145- cleaned = _ACTION_TAG_RE .sub ("" , text )
146-
147- # 移除其他可能的协议标签,但保留换行符
148- cleaned = re .sub (r"</?(?:Thought|FinalAnswer|Action).*?>" , "" , cleaned , flags = re .IGNORECASE | re .DOTALL )
149-
150- # 只移除首尾空白,保留中间的换行符和缩进
151- return cleaned .strip ()
152-
153-
154129def _process_react_stream_text (state : dict [str , str ], new_text : str ) -> str :
155130 """在流式阶段移除协议标签,但保留换行符和空白字符以维护 Markdown 格式。"""
156131
@@ -215,12 +190,9 @@ def _process_react_stream_text(state: dict[str, str], new_text: str) -> str:
215190 state ["buffer" ] = buffer
216191 return "" .join (output_parts )
217192
218- content = block [inner_start + 1 : close_idx ]
219-
220- if potential_tag == "finalanswer" :
221- # 保留 FinalAnswer 的内容,包括换行符
222- output_parts .append (content )
223- # Action 和 Thought 直接丢弃,但不影响前后的空白字符
193+ # 提取标签内部内容(目前仅用于完整跳过 <Action> ... </Action>)
194+ # 注意:这里不直接拼接任何协议标签内部的文本,保证前端只看到清洗后的可见正文。
195+ _ = block [inner_start + 1 : close_idx ]
224196
225197 # 推进 buffer
226198 buffer = buffer [block_end :]
@@ -525,7 +497,7 @@ async def stream_chat_with_react(
525497 llm_config_id = request .llm_config_id ,
526498 temperature = request .temperature or 0.6 ,
527499 max_tokens = request .max_tokens or 8192 ,
528- timeout = request .timeout or 60 ,
500+ timeout = request .timeout or 90 ,
529501 thinking_enabled = getattr (request , "thinking_enabled" , None ),
530502 )
531503
@@ -607,9 +579,12 @@ async def stream_chat_with_react(
607579 usage_in_total += in_tokens
608580 usage_out_total += out_tokens
609581
610- # 仅在本轮已经产生过面向用户的正文文本时,才解析 Action 协议。
611- # 这样可以避免模型在纯思考/Reasoning 阶段输出的 <Action> 触发前端提前进入工具调用状态。
612- action_payload = _parse_action_payload (step_text ) if has_visible_text else None
582+ # 直接从本轮累计的文本中解析 Action 协议。
583+ # 早期实现曾经要求 has_visible_text 才允许解析,为的是避免模型在纯思考阶段输出 <Action>。
584+ # 但在当前提示词下,我们只约定了 <Action>{...}</Action>,没有显式的 Thought/FinalAnswer 标签,
585+ # 严格依赖 has_visible_text 会导致"只输出 Action、不输出正文"的情况完全被忽略,前端看到的是空回复。
586+ # 因此这里放宽限制:总是尝试从 step_text 中解析 Action,由上游提示词约束模型行为。
587+ action_payload = _parse_action_payload (step_text )
613588
614589 if action_payload :
615590 tool_name , args = action_payload
@@ -657,6 +632,7 @@ async def stream_chat_with_react(
657632 raise RuntimeError ("React 模式未能产生最终回复" )
658633
659634 except asyncio .CancelledError :
635+ logger .warning (f"[React-Agent] 请求被客户端取消 (CancelledError)" )
660636 if usage_in_total and usage_out_total :
661637 in_tokens = usage_in_total
662638 out_tokens = usage_out_total
@@ -671,7 +647,8 @@ async def stream_chat_with_react(
671647 calls = 1 ,
672648 aborted = True ,
673649 )
674- return
650+ # 必须重新抛出 CancelledError 以便上层协程正确感知取消
651+ raise
675652 except Exception as e :
676653 logger .error (f"[React-Agent] 执行失败: { e } " )
677654 raise
@@ -728,7 +705,7 @@ async def stream_chat_with_tools(
728705 llm_config_id = request .llm_config_id ,
729706 temperature = request .temperature or 0.6 ,
730707 max_tokens = request .max_tokens or 8192 ,
731- timeout = request .timeout or 60 ,
708+ timeout = request .timeout or 90 ,
732709 thinking_enabled = getattr (request , "thinking_enabled" , None ),
733710 )
734711
0 commit comments