xgae 0.1.13__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xgae might be problematic. Click here for more details.

@@ -1,9 +1,9 @@
1
1
  import logging
2
-
2
+ import asyncio
3
3
  from typing import List, Dict, Any, Optional, AsyncGenerator, override
4
4
 
5
5
  from xgae.utils import log_trace
6
- from xgae.utils.json_helpers import format_for_yield
6
+
7
7
  from xgae.engine.responser.responser_base import TaskResponseProcessor, TaskResponserContext, TaskRunContinuousState
8
8
 
9
9
 
@@ -17,20 +17,30 @@ class StreamTaskResponser(TaskResponseProcessor):
17
17
  prompt_messages: List[Dict[str, Any]],
18
18
  continuous_state: TaskRunContinuousState
19
19
  ) -> AsyncGenerator[Dict[str, Any], None]:
20
- accumulated_content = continuous_state.get('accumulated_content', "")
21
- auto_continue_count = continuous_state.get('auto_continue_count', 0)
22
- can_auto_continue = continuous_state.get("auto_continue", False)
23
- use_assistant_chunk_msg = self.response_context.get("use_assistant_chunk_msg")
24
-
20
+ accumulated_content = continuous_state['accumulated_content']
21
+ auto_continue_count = continuous_state['auto_continue_count']
22
+ can_auto_continue = continuous_state['auto_continue']
23
+ msg_sequence = continuous_state['assistant_msg_sequence']
24
+
25
+ use_assistant_chunk_msg = self.response_context["use_assistant_chunk_msg"]
26
+
25
27
  finish_reason = None
26
28
  should_auto_continue = False
27
- sequence = continuous_state.get('assistant_msg_sequence', 0)
29
+
30
+ pending_tool_executions = []
31
+ yielded_tool_indices = set() # Track which tool statuses have been yielded
32
+ tool_results_buffer = [] # Store (tool_call, result, tool_index, context)
33
+ tool_index = 0
28
34
 
35
+ current_xml_content = accumulated_content # Track XML content for streaming detection
36
+
37
+ logging.info(f"=== StreamResp:tool_exec_on_stream={self.tool_exec_on_stream}, auto_continue_count={auto_continue_count}, "
38
+ f"accumulated_content_len={len(accumulated_content)}")
29
39
  try:
30
40
  async for llm_chunk in llm_response:
31
- if hasattr(llm_chunk, 'choices') and llm_chunk.choices and hasattr(llm_chunk.choices[0], 'finish_reason'):
41
+ if hasattr(llm_chunk, 'choices') and llm_chunk.choices and hasattr(llm_chunk.choices[0],'finish_reason'):
32
42
  if llm_chunk.choices[0].finish_reason:
33
- finish_reason = llm_chunk.choices[0].finish_reason
43
+ finish_reason = llm_chunk.choices[0].finish_reason # LLM finish reason: ‘stop' , 'length'
34
44
  logging.info(f"StreamResp:LLM chunk response finish_reason={finish_reason}")
35
45
 
36
46
  if hasattr(llm_chunk, 'choices') and llm_chunk.choices:
@@ -39,24 +49,77 @@ class StreamTaskResponser(TaskResponseProcessor):
39
49
  if llm_chunk_msg and hasattr(llm_chunk_msg, 'content') and llm_chunk_msg.content:
40
50
  chunk_content = llm_chunk_msg.content
41
51
  accumulated_content += chunk_content
52
+ current_xml_content += chunk_content #Track streaming XML content
42
53
 
43
54
  xml_tool_call_count = len(self._extract_xml_chunks(accumulated_content))
44
55
  if self.max_xml_tool_calls <= 0 or xml_tool_call_count < self.max_xml_tool_calls:
45
56
  if use_assistant_chunk_msg:
46
57
  message_data = {"role": "assistant", "content": chunk_content}
47
- metadata = {"sequence": sequence}
48
- assistant_chunk_msg = self.create_response_message(type="assistant_chunk", content=message_data,
49
- is_llm_message=True, metadata=metadata)
58
+ metadata = {"sequence": msg_sequence}
59
+ assistant_chunk_msg = self.create_response_message(type="assistant_chunk",
60
+ content=message_data,
61
+ is_llm_message=True,
62
+ metadata=metadata)
50
63
  yield assistant_chunk_msg
64
+ msg_sequence += 1
65
+
66
+ #Process XML tool calls during streaming
67
+ if self.tool_exec_on_stream:
68
+ xml_chunks = self._extract_xml_chunks(current_xml_content)
69
+ for xml_chunk in xml_chunks:
70
+ current_xml_content = current_xml_content.replace(xml_chunk, "", 1)
71
+ result = self._parse_xml_tool_call(xml_chunk)
72
+ if result:
73
+ tool_call, parsing_details = result
51
74
 
52
- sequence += 1
75
+ # Create tool context for streaming execution
76
+ tool_context = self._create_tool_context(tool_call, tool_index, None, parsing_details)
77
+
78
+ # Yield tool start status immediately
79
+ tool_start_msg = self._add_tool_start_message(tool_context)
80
+ yield tool_start_msg
81
+ yielded_tool_indices.add(tool_index)
82
+
83
+ # Create async execution task
84
+ execution_task = asyncio.create_task(self._execute_tool(tool_call))
85
+ pending_tool_executions.append({'task': execution_task,
86
+ 'tool_call': tool_call,
87
+ 'tool_index': tool_index,
88
+ 'tool_context': tool_context,
89
+ 'parsing_details': parsing_details})
90
+ tool_index += 1
53
91
  else:
54
92
  finish_reason = "xml_tool_limit_reached"
93
+ logging.warning(f"StreamResp: Over XML Tool Limit, finish_reason='xml_tool_limit_reached', "
94
+ f"xml_tool_call_count={xml_tool_call_count}")
55
95
  break
96
+ # for chunk is end
56
97
 
57
- if len(accumulated_content) == 0:
98
+ if len(accumulated_content) == 0:
58
99
  logging.warning(f"StreamResp: LLM response_message content is empty")
59
100
 
101
+ # Wait for pending tool executions from streaming phase
102
+ if pending_tool_executions:
103
+ logging.info(f"StreamResp: Waiting for {len(pending_tool_executions)} pending streamed tool executions")
104
+
105
+ pending_tasks = [execution['task'] for execution in pending_tool_executions]
106
+ done, _ = await asyncio.wait(pending_tasks)
107
+
108
+ for pend_tool_exec in pending_tool_executions:
109
+ pend_tool_index = pend_tool_exec['tool_index']
110
+ pend_tool_context = pend_tool_exec['tool_context']
111
+
112
+ try:
113
+ if pend_tool_exec["task"].done():
114
+ result = pend_tool_exec['task'].result()
115
+ pend_tool_context.result = result
116
+ tool_results_buffer.append((pend_tool_exec["tool_call"], result, pend_tool_index, pend_tool_context))
117
+ else:
118
+ logging.warning(f"StreamResp: Task for tool index {pend_tool_index} is not done after wait.")
119
+ except Exception as e:
120
+ logging.error(f"StreamResp: Error getting result for pending tool execution {pend_tool_index}: {str(e)}")
121
+ pend_tool_context.error = e
122
+
60
123
  if finish_reason == "xml_tool_limit_reached":
61
124
  xml_chunks = self._extract_xml_chunks(accumulated_content)
62
125
  if len(xml_chunks) > self.max_xml_tool_calls:
@@ -65,51 +128,108 @@ class StreamTaskResponser(TaskResponseProcessor):
65
128
  last_chunk = limited_chunks[-1]
66
129
  last_chunk_pos = accumulated_content.find(last_chunk) + len(last_chunk)
67
130
  accumulated_content = accumulated_content[:last_chunk_pos]
68
- parsed_xml_data = self._parse_xml_tool_calls(accumulated_content)
69
131
 
132
+ parsed_xml_data = self._parse_xml_tool_calls(accumulated_content)
70
133
  should_auto_continue = (can_auto_continue and finish_reason == 'length')
71
134
 
72
- self.root_span.event(name=f"stream_processor_start[{self.task_no}]({auto_continue_count})",level="DEFAULT",
73
- status_message=f"finish_reason={finish_reason}, tool_exec_strategy={self.tool_execution_strategy}, "
74
- f"parsed_xml_data_len={len(parsed_xml_data)}, accumulated_content={len(accumulated_content)}, "
75
- f"should_auto_continue={should_auto_continue}")
135
+ self.root_span.event(name=f"stream_processor_start[{self.task_no}]({auto_continue_count})", level="DEFAULT",
136
+ status_message=f"finish_reason={finish_reason}, tool_exec_strategy={self.tool_exec_strategy}, "
137
+ f"parsed_xml_data_len={len(parsed_xml_data)}, accumulated_content_len={len(accumulated_content)}, "
138
+ f"should_auto_continue={should_auto_continue}, pending_executions_len={len(pending_tool_executions)}")
76
139
 
77
140
  assistant_msg = None
78
141
  if accumulated_content and not should_auto_continue:
79
142
  message_data = {"role": "assistant", "content": accumulated_content}
80
- assistant_msg = self.add_response_message(type="assistant", content=message_data,
81
- is_llm_message=True)
143
+ assistant_msg = self.add_response_message(type="assistant", content=message_data, is_llm_message=True)
82
144
  yield assistant_msg
83
145
 
146
+ # Process results from both streaming and non-streaming executions
84
147
  tool_calls_to_execute = [item['tool_call'] for item in parsed_xml_data]
148
+
149
+ # Update assistant_message_id for streaming tool contexts
150
+ assistant_msg_id = assistant_msg['message_id'] if assistant_msg else None
151
+ for pend_tool_exec in pending_tool_executions:
152
+ if not pend_tool_exec["tool_context"].assistant_message_id:
153
+ pend_tool_exec["tool_context"].assistant_message_id = assistant_msg_id
154
+
85
155
  if len(tool_calls_to_execute) > 0:
86
- tool_results = await self._execute_tools(tool_calls_to_execute, self.tool_execution_strategy)
156
+ if self.tool_exec_on_stream:
157
+ # Handle results from streaming executions + any remaining tools
158
+ remaining_tools = []
159
+ streamed_tool_indices = set()
160
+
161
+ # Identify which tools were already executed during streaming by index
162
+ for pend_tool_exec in pending_tool_executions:
163
+ streamed_tool_indices.add(pend_tool_exec["tool_index"])
87
164
 
88
- tool_index = 0
89
- for i, (returned_tool_call, tool_result) in enumerate(tool_results):
90
- parsed_xml_item = parsed_xml_data[i]
91
- tool_call = parsed_xml_item['tool_call']
92
- parsing_details = parsed_xml_item['parsing_details']
93
- assistant_msg_id = assistant_msg['message_id'] if assistant_msg else None
165
+ # Find remaining tools that weren't executed during streaming
166
+ for i, parsed_item in enumerate(parsed_xml_data):
167
+ tool_call = parsed_item['tool_call']
168
+ tool_identifier = (tool_call.get('function_name', ''), str(tool_call.get('arguments', {})))
94
169
 
95
- tool_context = self._create_tool_context(tool_call, tool_index, assistant_msg_id, parsing_details)
96
- tool_context.result = tool_result
170
+ # Check if this tool was already executed during streaming
171
+ already_executed = False
172
+ for pend_tool_exec in pending_tool_executions:
173
+ exec_tool_call = pend_tool_exec["tool_call"]
174
+ exec_identifier = (exec_tool_call.get('function_name', ''),str(exec_tool_call.get('arguments', {})))
175
+ if tool_identifier == exec_identifier:
176
+ already_executed = True
177
+ break
97
178
 
98
- tool_start_msg = self._add_tool_start_message(tool_context)
99
- yield format_for_yield(tool_start_msg)
179
+ if not already_executed:
180
+ remaining_tools.append((parsed_item['tool_call'], parsed_item['parsing_details'], tool_index))
181
+ tool_index += 1
100
182
 
101
- tool_message = self._add_tool_messsage(tool_call, tool_result, self.xml_adding_strategy, assistant_msg_id, parsing_details)
183
+ # Execute remaining tools if any
184
+ if remaining_tools:
185
+ for tool_call, parsing_details, t_idx in remaining_tools:
186
+ tool_context = self._create_tool_context(tool_call, t_idx, assistant_msg_id,parsing_details)
102
187
 
103
- tool_completed_msg = self._add_tool_completed_message(tool_context, tool_message['message_id'])
104
- yield format_for_yield(tool_completed_msg)
188
+ tool_start_msg = self._add_tool_start_message(tool_context)
189
+ yield tool_start_msg
105
190
 
106
- yield format_for_yield(tool_message)
191
+ result = await self._execute_tool(tool_call)
192
+ tool_context.result = result
193
+ tool_results_buffer.append((tool_call, result, t_idx, tool_context))
107
194
 
108
- if tool_completed_msg["metadata"].get("agent_should_terminate") == "true":
109
- finish_reason = "completed"
110
- break
195
+ # Process all tool results
196
+ for tool_call, result, t_idx, pend_tool_context in tool_results_buffer:
197
+ tool_message = self._add_tool_messsage(tool_call, result, self.xml_adding_strategy,assistant_msg_id,
198
+ getattr(pend_tool_context, 'parsing_details', None))
199
+
200
+ tool_completed_msg = self._add_tool_completed_message(pend_tool_context,tool_message['message_id'] if tool_message else None)
201
+ yield tool_completed_msg
202
+
203
+ yield tool_message
204
+
205
+ if pend_tool_context.function_name in ['ask', 'complete']:
206
+ finish_reason = "completed"
207
+ break
208
+ else: # non-streaming execution
209
+ tool_results = await self._execute_tools(tool_calls_to_execute, self.tool_exec_strategy)
210
+ tool_index = 0
211
+ for i, (returned_tool_call, tool_result) in enumerate(tool_results):
212
+ parsed_xml_item = parsed_xml_data[i]
213
+ tool_call = parsed_xml_item['tool_call']
214
+ parsing_details = parsed_xml_item['parsing_details']
215
+
216
+ tool_context = self._create_tool_context(tool_call, tool_index, assistant_msg_id,parsing_details, tool_result)
217
+
218
+ tool_start_msg = self._add_tool_start_message(tool_context)
219
+ yield tool_start_msg
220
+
221
+ tool_message = self._add_tool_messsage(tool_call, tool_result, self.xml_adding_strategy,assistant_msg_id, parsing_details)
222
+
223
+ tool_completed_msg = self._add_tool_completed_message(tool_context, tool_message['message_id'])
224
+ yield tool_completed_msg
225
+
226
+ yield tool_message
227
+
228
+ if tool_context.function_name in ['ask', 'complete']:
229
+ finish_reason = "completed"
230
+ break
111
231
 
112
- tool_index += 1
232
+ tool_index += 1
113
233
  else:
114
234
  finish_reason = "non_tool_call"
115
235
  logging.warning(f"StreamResp: tool_calls is empty, No Tool need to call !")
@@ -117,7 +237,7 @@ class StreamTaskResponser(TaskResponseProcessor):
117
237
  if finish_reason:
118
238
  finish_content = {"status_type": "finish", "finish_reason": finish_reason}
119
239
  finish_msg = self.add_response_message(type="status", content=finish_content, is_llm_message=False)
120
- yield format_for_yield(finish_msg)
240
+ yield finish_msg
121
241
  except Exception as e:
122
242
  trace = log_trace(e, f"StreamResp: Process response accumulated_content:\n {accumulated_content}")
123
243
  self.root_span.event(name="stream_response_process_error", level="ERROR",
@@ -126,11 +246,11 @@ class StreamTaskResponser(TaskResponseProcessor):
126
246
 
127
247
  content = {"role": "system", "status_type": "error", "message": f"Process streaming response error: {e}"}
128
248
  error_msg = self.add_response_message(type="status", content=content, is_llm_message=False)
129
- yield format_for_yield(error_msg)
249
+ yield error_msg
130
250
 
131
251
  raise # Use bare 'raise' to preserve the original exception with its traceback
132
252
  finally:
133
253
  if should_auto_continue:
134
254
  continuous_state['accumulated_content'] = accumulated_content
135
- continuous_state['assistant_msg_sequence'] = sequence
136
- logging.warning(f"StreamResp: Updated continuous state for auto-continue with {len(accumulated_content)} chars")
255
+ continuous_state['assistant_msg_sequence'] = msg_sequence
256
+ logging.warning(f"StreamResp: Updated continuous state for auto-continue with {len(accumulated_content)} chars")