ZyphrZero commited on
Commit
d264039
·
1 Parent(s): e985dd1

✨ feat(app/utils): 优化SSE工具处理逻辑,支持增量解析和状态管理

Browse files
Files changed (1) hide show
  1. app/utils/sse_tool_handler.py +374 -160
app/utils/sse_tool_handler.py CHANGED
@@ -3,11 +3,13 @@
3
 
4
  """
5
  SSE Tool Handler - 处理工具调用的SSE流
 
6
  """
7
 
8
  import json
 
9
  import time
10
- from typing import Dict, Any, Optional, Generator
11
 
12
  from app.utils.logger import get_logger
13
 
@@ -20,125 +22,367 @@ class SSEToolHandler:
20
  self.chat_id = chat_id
21
  self.model = model
22
 
 
23
  self.has_tool_call = False
24
- self.tool_args = "" # 当前工具的参数累积
25
- self.tool_id = "" # 当前工具ID
26
- self.tool_name = "" # 当前工具名称
27
  self.tool_call_usage = None # 工具调用的usage信息
28
  self.content_index = 0
29
  self.has_thinking = False
30
 
 
 
 
 
 
 
 
 
 
31
  def process_tool_call_phase(self, data: Dict[str, Any], is_stream: bool = True) -> Generator[str, None, None]:
32
  """
33
- 处理tool_call阶段
34
- 参考JS的forEach逻辑,每个块独立处理
35
  """
36
  if not self.has_tool_call:
37
  self.has_tool_call = True
38
  logger.debug("🔧 进入工具调用阶段")
39
 
40
  edit_content = data.get("edit_content", "")
 
 
41
  if not edit_content:
42
  return
43
 
44
- logger.debug(f"📦 解析数据块: {edit_content}")
45
-
46
- # 分割glm_block块
47
- blocks = edit_content.split("<glm_block >")
48
-
49
- for index, block in enumerate(blocks):
50
- if not block:
51
- continue
52
-
53
- logger.debug(f" 📦 处理块 {index}: {block[:200]}...")
54
-
55
- if "</glm_block>" not in block:
56
- # 这个块不完整,可能是参数片段
57
- if index == 0:
58
- # 第一个块的参数片段
59
- self.tool_args += block
60
- logger.debug(f" 📦 累积参数片段: {block}")
61
- continue
62
-
63
- if index == 0:
64
- # 第一个块:提取参数片段(到"result"之前)
65
- # 提取到 '"result"' 之前的内容
66
- if '"result"' in edit_content:
67
- result_index = edit_content.index('"result"')
68
- args_fragment = edit_content[:result_index - 3]
69
- self.tool_args += args_fragment
70
- logger.debug(f"📦 从第一个块提取参数片段: {args_fragment}")
71
- else:
72
- # 后续块:新的工具调用
73
- # 如果当前有工具正在处理,先完成它
74
- if self.tool_id:
75
- logger.debug(f" 🎯 完成当前工具: {self.tool_name}")
76
- yield from self._finish_current_tool(is_stream)
77
 
78
- # 解析新工具信息
79
- try:
80
- block_content = block[:block.index("</glm_block>")]
81
- content = json.loads(block_content)
82
- metadata = content.get("data", {}).get("metadata", {})
83
 
84
- # 开始新工具
85
- self.tool_id = metadata.get("id", "")
86
- self.tool_name = metadata.get("name", "")
87
- arguments = metadata.get("arguments", {})
88
 
89
- # 累积参数(去掉最后的}以便后续累积)
90
- self.tool_args = json.dumps(arguments, ensure_ascii=False)[:-1]
 
 
 
 
 
91
 
92
- logger.debug(f"🎯 新工具调用: {self.tool_name}(id={self.tool_id})")
93
- logger.debug(f" 📦 初始参数: {self.tool_args}")
 
 
94
 
95
- if is_stream:
96
- yield self._create_tool_start_chunk()
 
97
 
98
- self.content_index += 1
 
 
99
 
100
- except (json.JSONDecodeError, KeyError) as e:
101
- logger.error(f"❌ 解析工具块失败: {e}")
102
- logger.error(f" 📦 块内容: {block[:500]}")
103
 
104
- def _finish_current_tool(self, is_stream: bool) -> Generator[str, None, None]:
105
- if not self.tool_id:
106
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
 
 
 
 
 
 
 
 
108
  try:
109
- test_args = self.tool_args + '"'
 
 
 
110
 
111
- logger.debug(f" 工具参数解析成功: {self.tool_name}")
112
- logger.debug(f" 📦 最终参数字符串: {test_args}")
 
113
 
114
- # 解析参数
115
- params = json.loads(test_args)
116
 
117
- logger.debug(f" 完成工具调用: {self.tool_name} with params: {params}")
118
 
119
- if is_stream:
120
- yield self._create_tool_arguments_chunk(params)
121
 
122
  except json.JSONDecodeError as e:
123
- logger.error(f" 工具参数解析失败: {e}")
124
- logger.error(f" 📦 原始参数: {self.tool_args[:200]}")
125
- logger.error(f" 📦 测试参数: {test_args[:200] if 'test_args' in locals() else 'N/A'}")
126
- # 解析失败时使用空参数
127
- params = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  if is_stream:
129
- yield self._create_tool_arguments_chunk(params)
130
- finally:
131
- # 清理当前工具状态
132
- self.tool_args = ""
133
- self.tool_id = ""
134
- self.tool_name = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  def process_other_phase(self, data: Dict[str, Any], is_stream: bool = True) -> Generator[str, None, None]:
137
  """
138
- 处理other阶段
139
- 主要检测工具调用结束
140
  """
141
  edit_content = data.get("edit_content", "")
 
142
  usage = data.get("usage")
143
 
144
  # 保存usage信息
@@ -146,93 +390,63 @@ class SSEToolHandler:
146
  self.tool_call_usage = usage
147
  logger.debug(f"💾 保存工具调用usage: {usage}")
148
 
149
- # 检测工具调用结束标记 "null,"
150
- if self.has_tool_call and edit_content and edit_content.startswith("null,"):
151
- logger.debug("🏁 检测到工具调用结束标记: null,")
 
 
 
 
 
 
 
 
 
152
 
153
- # 补充引号并完成最后一个工具调用
154
- self.tool_args += '"'
 
 
 
155
  self.has_tool_call = False
156
 
157
- try:
158
- # 解析最终参数
159
- params = json.loads(self.tool_args)
160
- logger.debug(f"✅ 最终工具参数解析成功: {params}")
161
-
162
- if is_stream:
163
- # 创建工具参数块
164
- tool_call_delta = {
165
- "id": self.tool_id,
166
- "type": "function",
167
- "function": {
168
- "name": None,
169
- "arguments": json.dumps(params, ensure_ascii=False),
170
- },
171
- }
172
- delta_res = {
173
- "choices": [
174
- {
175
- "delta": {
176
- "role": "assistant",
177
- "content": None,
178
- "tool_calls": [tool_call_delta],
179
- },
180
- "finish_reason": None,
181
- "index": 0,
182
- "logprobs": None,
183
- }
184
- ],
185
- "created": int(time.time()),
186
- "id": self.chat_id,
187
- "model": self.model,
188
- "object": "chat.completion.chunk",
189
- "system_fingerprint": "fp_zai_001",
190
- }
191
- yield f"data: {json.dumps(delta_res, ensure_ascii=False)}\n\n"
192
 
193
- # 发送工具完成信号
194
- finish_res = {
195
- "choices": [
196
- {
197
- "delta": {
198
- "role": "assistant",
199
- "content": None,
200
- "tool_calls": [],
201
- },
202
- "finish_reason": "tool_calls",
203
- "index": 0,
204
- "logprobs": None,
205
- }
206
- ],
207
- "created": int(time.time()),
208
- "id": self.chat_id,
209
- "usage": self.tool_call_usage or None,
210
- "model": self.model,
211
- "object": "chat.completion.chunk",
212
- "system_fingerprint": "fp_zai_001",
213
- }
214
 
215
- logger.info("🏁 发送工具调用完成信号")
216
- yield f"data: {json.dumps(finish_res, ensure_ascii=False)}\n\n"
217
- yield "data: [DONE]\n\n"
 
218
 
219
- except json.JSONDecodeError as e:
220
- logger.error(f" 最终参数解析失败: {e}")
221
- logger.error(f" 📦 参数内容: {self.tool_args}")
222
 
223
- # 重置所有状态
224
- self._reset_all_state()
225
 
226
  def _reset_all_state(self):
227
  """重置所有状态"""
228
  self.has_tool_call = False
229
- self.tool_args = ""
230
- self.tool_id = ""
231
- self.tool_name = ""
232
  self.tool_call_usage = None
233
  self.content_index = 0
 
 
 
 
 
234
 
235
- def _create_tool_start_chunk(self) -> str:
236
  """创建工具调用开始的chunk"""
237
  chunk = {
238
  "choices": [
@@ -242,9 +456,9 @@ class SSEToolHandler:
242
  "content": None,
243
  "tool_calls": [
244
  {
245
- "id": self.tool_id,
246
  "type": "function",
247
- "function": {"name": self.tool_name, "arguments": ""},
248
  }
249
  ],
250
  },
@@ -261,7 +475,7 @@ class SSEToolHandler:
261
  }
262
  return f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
263
 
264
- def _create_tool_arguments_chunk(self, arguments: Dict) -> str:
265
  """创建工具参数的chunk"""
266
  chunk = {
267
  "choices": [
@@ -271,14 +485,14 @@ class SSEToolHandler:
271
  "content": None,
272
  "tool_calls": [
273
  {
274
- "id": self.tool_id,
275
  "type": "function",
276
  "function": {"name": None, "arguments": json.dumps(arguments, ensure_ascii=False)},
277
  }
278
  ],
279
  },
280
  "finish_reason": None,
281
- "index": self.content_index, # 使用正确的索引
282
  "logprobs": None,
283
  }
284
  ],
 
3
 
4
  """
5
  SSE Tool Handler - 处理工具调用的SSE流
6
+ 基于 Z.AI 原生的 edit_index 和 edit_content 机制,更原生地处理工具调用
7
  """
8
 
9
  import json
10
+ import re
11
  import time
12
+ from typing import Dict, Any, Optional, Generator, List
13
 
14
  from app.utils.logger import get_logger
15
 
 
22
  self.chat_id = chat_id
23
  self.model = model
24
 
25
+ # 工具调用状态
26
  self.has_tool_call = False
 
 
 
27
  self.tool_call_usage = None # 工具调用的usage信息
28
  self.content_index = 0
29
  self.has_thinking = False
30
 
31
+ # 原生内容重建机制 - 基于 Z.AI 的 edit_index 机制
32
+ self.content_buffer = bytearray() # 使用字节数组提高性能
33
+ self.last_edit_index = 0 # 上次编辑的位置
34
+
35
+ # 工具调用解析状态
36
+ self.active_tools = {} # 活跃的工具调用 {tool_id: tool_info}
37
+ self.completed_tools = [] # 已完成的工具调用
38
+ self.tool_blocks_cache = {} # 缓存解析的工具块
39
+
40
  def process_tool_call_phase(self, data: Dict[str, Any], is_stream: bool = True) -> Generator[str, None, None]:
41
  """
42
+ 处理tool_call阶段 - 基于原生edit_index机制处理工具调用
 
43
  """
44
  if not self.has_tool_call:
45
  self.has_tool_call = True
46
  logger.debug("🔧 进入工具调用阶段")
47
 
48
  edit_content = data.get("edit_content", "")
49
+ edit_index = data.get("edit_index", 0)
50
+
51
  if not edit_content:
52
  return
53
 
54
+ # logger.debug(f"📦 接收内容片段 [index={edit_index}]: {edit_content[:1000]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ # 使用原生的edit_index机制更新内容缓冲区
57
+ self._apply_edit_to_buffer(edit_index, edit_content)
 
 
 
58
 
59
+ # 尝试解析和处理工具调用
60
+ yield from self._process_tool_calls_from_buffer(is_stream)
 
 
61
 
62
+ def _apply_edit_to_buffer(self, edit_index: int, edit_content: str):
63
+ """
64
+ 基于edit_index原生地更新内容缓冲区
65
+ 这是Z.AI的核心机制:在指定位置替换/插入内容
66
+ """
67
+ edit_bytes = edit_content.encode('utf-8')
68
+ required_length = edit_index + len(edit_bytes)
69
 
70
+ # 扩展缓冲区到所需长度(如果需要)
71
+ if len(self.content_buffer) < edit_index:
72
+ # 如果edit_index超出当前缓冲区,用空字节填充
73
+ self.content_buffer.extend(b'\x00' * (edit_index - len(self.content_buffer)))
74
 
75
+ # 确保缓冲区足够长以容纳新内容
76
+ if len(self.content_buffer) < required_length:
77
+ self.content_buffer.extend(b'\x00' * (required_length - len(self.content_buffer)))
78
 
79
+ # 在指定位置替换内容(不是插入,而是覆盖)
80
+ end_index = edit_index + len(edit_bytes)
81
+ self.content_buffer[edit_index:end_index] = edit_bytes
82
 
83
+ # logger.debug(f"📝 缓冲区更新 [index={edit_index}, 长度={len(self.content_buffer)}]")
 
 
84
 
85
+ def _process_tool_calls_from_buffer(self, is_stream: bool) -> Generator[str, None, None]:
86
+ """
87
+ 从内容缓冲区中解析和处理工具调用
88
+ """
89
+ try:
90
+ # 解码内容并清理空字节
91
+ content_str = self.content_buffer.decode('utf-8', errors='ignore').replace('\x00', '')
92
+ yield from self._extract_and_process_tools(content_str, is_stream)
93
+ except Exception as e:
94
+ logger.debug(f"📦 内容解析暂时失败,等待更多数据: {e}")
95
+ # 不抛出异常,继续等待更多数据
96
+
97
+ def _extract_and_process_tools(self, content_str: str, is_stream: bool) -> Generator[str, None, None]:
98
+ """
99
+ 从内容字符串中提取和处理工具调用
100
+ 使用更原生的方式解析 glm_block
101
+ """
102
+ # 查找所有 glm_block,包括不完整的
103
+ pattern = r'<glm_block\s*>(.*?)(?:</glm_block>|$)'
104
+ matches = re.findall(pattern, content_str, re.DOTALL)
105
 
106
+ for block_content in matches:
107
+ # 尝试解析每个块
108
+ yield from self._process_single_tool_block(block_content, is_stream)
109
+
110
+ def _process_single_tool_block(self, block_content: str, is_stream: bool) -> Generator[str, None, None]:
111
+ """
112
+ 处理单个工具块,支持增量解析
113
+ """
114
  try:
115
+ # 尝试修复和解析完整的JSON
116
+ fixed_content = self._fix_json_structure(block_content)
117
+ tool_data = json.loads(fixed_content)
118
+ metadata = tool_data.get("data", {}).get("metadata", {})
119
 
120
+ tool_id = metadata.get("id", "")
121
+ tool_name = metadata.get("name", "")
122
+ arguments_raw = metadata.get("arguments", "{}")
123
 
124
+ if not tool_id or not tool_name:
125
+ return
126
 
127
+ logger.debug(f"🎯 解析完整工具块: {tool_name}(id={tool_id}), 参数: {arguments_raw}")
128
 
129
+ # 检查是否是新工具或更新的工具
130
+ yield from self._handle_tool_update(tool_id, tool_name, arguments_raw, is_stream)
131
 
132
  except json.JSONDecodeError as e:
133
+ logger.debug(f"📦 JSON解析失败: {e}, 尝试部分解析")
134
+ # JSON 不完整,尝试部分解析
135
+ yield from self._handle_partial_tool_block(block_content, is_stream)
136
+ except Exception as e:
137
+ logger.debug(f"📦 工具块处理失败: {e}")
138
+
139
+ def _fix_json_structure(self, content: str) -> str:
140
+ """
141
+ 修复JSON结构中的常见问题
142
+ """
143
+ if not content:
144
+ return content
145
+
146
+ # 计算括号平衡
147
+ open_braces = content.count('{')
148
+ close_braces = content.count('}')
149
+
150
+ # 如果闭括号多于开括号,移除多余的闭括号
151
+ if close_braces > open_braces:
152
+ excess = close_braces - open_braces
153
+ fixed_content = content
154
+ for _ in range(excess):
155
+ # 从右侧移除多余的闭括号
156
+ last_brace_pos = fixed_content.rfind('}')
157
+ if last_brace_pos != -1:
158
+ fixed_content = fixed_content[:last_brace_pos] + fixed_content[last_brace_pos + 1:]
159
+ return fixed_content
160
+
161
+ return content
162
+
163
+ def _handle_tool_update(self, tool_id: str, tool_name: str, arguments_raw: str, is_stream: bool) -> Generator[str, None, None]:
164
+ """
165
+ 处理工具的创建或更新
166
+ """
167
+ # 解析参数
168
+ try:
169
+ if isinstance(arguments_raw, str):
170
+ # 先处理转义和清理
171
+ cleaned_args = self._clean_arguments_string(arguments_raw)
172
+ arguments = json.loads(cleaned_args) if cleaned_args.strip() else {}
173
+ else:
174
+ arguments = arguments_raw
175
+ except json.JSONDecodeError:
176
+ logger.debug(f"📦 参数解析失败,使用部分参数: {arguments_raw[:100]}")
177
+ arguments = self._parse_partial_arguments(arguments_raw)
178
+
179
+ # 检查是否是新工具
180
+ if tool_id not in self.active_tools:
181
+ logger.debug(f"🎯 发现新工具: {tool_name}(id={tool_id})")
182
+
183
+ self.active_tools[tool_id] = {
184
+ "id": tool_id,
185
+ "name": tool_name,
186
+ "arguments": arguments,
187
+ "status": "active",
188
+ "sent_start": False,
189
+ "sent_args": False
190
+ }
191
+
192
  if is_stream:
193
+ # 发送工具开始信号
194
+ yield self._create_tool_start_chunk(tool_id, tool_name)
195
+ self.active_tools[tool_id]["sent_start"] = True
196
+
197
+ # 更新参数(如果有变化)
198
+ current_tool = self.active_tools[tool_id]
199
+ if current_tool["arguments"] != arguments:
200
+ current_tool["arguments"] = arguments
201
+
202
+ if is_stream and current_tool["sent_start"] and not current_tool["sent_args"]:
203
+ # 发送工具参数
204
+ yield self._create_tool_arguments_chunk(tool_id, arguments)
205
+ current_tool["sent_args"] = True
206
+
207
+ def _handle_partial_tool_block(self, block_content: str, is_stream: bool) -> Generator[str, None, None]:
208
+ """
209
+ 处理不完整的工具块,尝试提取可用信息
210
+ """
211
+ try:
212
+ # 尝试提取工具ID和名称
213
+ id_match = re.search(r'"id":\s*"([^"]+)"', block_content)
214
+ name_match = re.search(r'"name":\s*"([^"]+)"', block_content)
215
+
216
+ if id_match and name_match:
217
+ tool_id = id_match.group(1)
218
+ tool_name = name_match.group(1)
219
+
220
+ # 尝试提取参数部分
221
+ args_match = re.search(r'"arguments":\s*"([^"]*)', block_content)
222
+ partial_args = args_match.group(1) if args_match else ""
223
+
224
+ logger.debug(f"📦 部分工具块: {tool_name}(id={tool_id}), 部分参数: {partial_args[:50]}")
225
+
226
+ # 如果是新工具,先创建记录
227
+ if tool_id not in self.active_tools:
228
+ self.active_tools[tool_id] = {
229
+ "id": tool_id,
230
+ "name": tool_name,
231
+ "arguments": {},
232
+ "status": "partial",
233
+ "sent_start": False,
234
+ "sent_args": False,
235
+ "partial_args": partial_args
236
+ }
237
+
238
+ if is_stream:
239
+ yield self._create_tool_start_chunk(tool_id, tool_name)
240
+ self.active_tools[tool_id]["sent_start"] = True
241
+ else:
242
+ # 更新部分参数
243
+ self.active_tools[tool_id]["partial_args"] = partial_args
244
+
245
+ except Exception as e:
246
+ logger.debug(f"📦 部分块解析失败: {e}")
247
+
248
+ def _clean_arguments_string(self, arguments_raw: str) -> str:
249
+ """
250
+ 清理和标准化参数字符串
251
+ """
252
+ if not arguments_raw:
253
+ return "{}"
254
+
255
+ # 移除首尾空白
256
+ cleaned = arguments_raw.strip()
257
+
258
+ # 处理特殊值
259
+ if cleaned.lower() == "null":
260
+ return "{}"
261
+
262
+ # 处理转义的JSON字符串
263
+ if cleaned.startswith('{\\"') and cleaned.endswith('\\"}'):
264
+ # 这是一个转义的JSON字符串,需要反转义
265
+ cleaned = cleaned.replace('\\"', '"')
266
+ elif cleaned.startswith('"{\\"') and cleaned.endswith('\\"}'):
267
+ # 双重转义的情况
268
+ cleaned = cleaned[1:-1].replace('\\"', '"')
269
+
270
+ # 标准化空格(移除JSON中的多余空格,但保留字符串值中的空格)
271
+ try:
272
+ # 先尝试解析,然后重新序列化以标准化格式
273
+ parsed = json.loads(cleaned)
274
+ if parsed is None:
275
+ return "{}"
276
+ cleaned = json.dumps(parsed, ensure_ascii=False, separators=(',', ':'))
277
+ except json.JSONDecodeError:
278
+ # 如果解析失败,只做基本的空格清理
279
+ pass
280
+
281
+ return cleaned
282
+
283
+ def _parse_partial_arguments(self, arguments_raw: str) -> Dict[str, Any]:
284
+ """
285
+ 解析不完整的参数字符串,尽可能提取有效信息
286
+ """
287
+ if not arguments_raw or arguments_raw.strip() == "" or arguments_raw.strip().lower() == "null":
288
+ return {}
289
+
290
+ try:
291
+ # 先尝试清理字符串
292
+ cleaned = self._clean_arguments_string(arguments_raw)
293
+ result = json.loads(cleaned)
294
+ # 确保返回字典类型
295
+ return result if isinstance(result, dict) else {}
296
+ except json.JSONDecodeError:
297
+ pass
298
+
299
+ try:
300
+ # 尝试修复常见的JSON问题
301
+ fixed_args = arguments_raw.strip()
302
+
303
+ # 处理转义字符
304
+ if '\\' in fixed_args:
305
+ fixed_args = fixed_args.replace('\\"', '"')
306
+
307
+ # 如果不是以{开头,添加{
308
+ if not fixed_args.startswith('{'):
309
+ fixed_args = '{' + fixed_args
310
+
311
+ # 如果不是以}结尾,尝试添加}
312
+ if not fixed_args.endswith('}'):
313
+ # 计算未闭合的引号和括号
314
+ quote_count = fixed_args.count('"') - fixed_args.count('\\"')
315
+ if quote_count % 2 != 0:
316
+ fixed_args += '"'
317
+ fixed_args += '}'
318
+
319
+ return json.loads(fixed_args)
320
+ except json.JSONDecodeError:
321
+ # 尝试提取键值对
322
+ return self._extract_key_value_pairs(arguments_raw)
323
+ except Exception:
324
+ # 如果所有方法都失败,返回空字典
325
+ return {}
326
+
327
+ def _extract_key_value_pairs(self, text: str) -> Dict[str, Any]:
328
+ """
329
+ 从文本中提取键值对,作为最后的解析尝试
330
+ """
331
+ result = {}
332
+ try:
333
+ # 使用正则表达式提取简单的键值对
334
+ import re
335
+
336
+ # 匹配 "key": "value" 或 "key": value 格式
337
+ pattern = r'"([^"]+)":\s*"([^"]*)"'
338
+ matches = re.findall(pattern, text)
339
+
340
+ for key, value in matches:
341
+ result[key] = value
342
+
343
+ # 匹配数字值
344
+ pattern = r'"([^"]+)":\s*(\d+)'
345
+ matches = re.findall(pattern, text)
346
+
347
+ for key, value in matches:
348
+ try:
349
+ result[key] = int(value)
350
+ except ValueError:
351
+ result[key] = value
352
+
353
+ # 匹配布尔值
354
+ pattern = r'"([^"]+)":\s*(true|false)'
355
+ matches = re.findall(pattern, text)
356
+
357
+ for key, value in matches:
358
+ result[key] = value.lower() == 'true'
359
+
360
+ except Exception:
361
+ pass
362
+
363
+ return result
364
+
365
+ def _complete_active_tools(self, is_stream: bool) -> Generator[str, None, None]:
366
+ """
367
+ 完成所有活跃的工具调用
368
+ """
369
+ for tool_id, tool in self.active_tools.items():
370
+ tool["status"] = "completed"
371
+ self.completed_tools.append(tool)
372
+ logger.debug(f"✅ 完成工具调用: {tool['name']}(id={tool_id})")
373
+
374
+ self.active_tools.clear()
375
+
376
+ if is_stream and self.completed_tools:
377
+ # 发送工具完成信号
378
+ yield self._create_tool_finish_chunk()
379
 
380
  def process_other_phase(self, data: Dict[str, Any], is_stream: bool = True) -> Generator[str, None, None]:
381
  """
382
+ 处理other阶段 - 检测工具调用结束和状态更新
 
383
  """
384
  edit_content = data.get("edit_content", "")
385
+ edit_index = data.get("edit_index", 0)
386
  usage = data.get("usage")
387
 
388
  # 保存usage信息
 
390
  self.tool_call_usage = usage
391
  logger.debug(f"💾 保存工具调用usage: {usage}")
392
 
393
+ # 如果有edit_content,继续更新内容缓冲区
394
+ if edit_content:
395
+ self._apply_edit_to_buffer(edit_index, edit_content)
396
+ # 继续处理可能的工具调用更新
397
+ yield from self._process_tool_calls_from_buffer(is_stream)
398
+
399
+ # 检测工具调用结束的多种标记
400
+ if self.has_tool_call and self._is_tool_call_finished(edit_content):
401
+ logger.debug("🏁 检测到工具调用结束")
402
+
403
+ # 完成所有活跃的工具
404
+ yield from self._complete_active_tools(is_stream)
405
 
406
+ if is_stream:
407
+ logger.info("🏁 发送工具调用完成信号")
408
+ yield "data: [DONE]\n\n"
409
+
410
+ # 重置工具调用状态
411
  self.has_tool_call = False
412
 
413
+ def _is_tool_call_finished(self, edit_content: str) -> bool:
414
+ """
415
+ 检测工具调用是否结束的多种标记
416
+ """
417
+ if not edit_content:
418
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ # 检测各种结束标记
421
+ end_markers = [
422
+ "null,", # 原有的结束标记
423
+ '"status": "completed"', # 状态完成标记
424
+ '"is_error": false', # 错误状态标记
425
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
+ for marker in end_markers:
428
+ if marker in edit_content:
429
+ logger.debug(f"🔍 检测到结束标记: {marker}")
430
+ return True
431
 
432
+ # 检查是否所有工具都有完整的结构
433
+ if self.active_tools and '"status": "completed"' in self.content_buffer:
434
+ return True
435
 
436
+ return False
 
437
 
438
  def _reset_all_state(self):
439
  """重置所有状态"""
440
  self.has_tool_call = False
 
 
 
441
  self.tool_call_usage = None
442
  self.content_index = 0
443
+ self.content_buffer = bytearray()
444
+ self.last_edit_index = 0
445
+ self.active_tools.clear()
446
+ self.completed_tools.clear()
447
+ self.tool_blocks_cache.clear()
448
 
449
+ def _create_tool_start_chunk(self, tool_id: str, tool_name: str) -> str:
450
  """创建工具调用开始的chunk"""
451
  chunk = {
452
  "choices": [
 
456
  "content": None,
457
  "tool_calls": [
458
  {
459
+ "id": tool_id,
460
  "type": "function",
461
+ "function": {"name": tool_name, "arguments": "{}"},
462
  }
463
  ],
464
  },
 
475
  }
476
  return f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
477
 
478
+ def _create_tool_arguments_chunk(self, tool_id: str, arguments: Dict) -> str:
479
  """创建工具参数的chunk"""
480
  chunk = {
481
  "choices": [
 
485
  "content": None,
486
  "tool_calls": [
487
  {
488
+ "id": tool_id,
489
  "type": "function",
490
  "function": {"name": None, "arguments": json.dumps(arguments, ensure_ascii=False)},
491
  }
492
  ],
493
  },
494
  "finish_reason": None,
495
+ "index": self.content_index,
496
  "logprobs": None,
497
  }
498
  ],