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.
- xgae/engine/mcp_tool_box.py +8 -7
- xgae/engine/prompt_builder.py +9 -8
- xgae/engine/responser/non_stream_responser.py +13 -13
- xgae/engine/responser/responser_base.py +174 -262
- xgae/engine/responser/stream_responser.py +165 -45
- xgae/engine/task_engine.py +93 -93
- xgae/engine/task_langfuse.py +11 -11
- xgae/utils/json_helpers.py +0 -29
- xgae/utils/llm_client.py +23 -23
- xgae/utils/xml_tool_parser.py +7 -7
- {xgae-0.1.13.dist-info → xgae-0.1.15.dist-info}/METADATA +1 -1
- xgae-0.1.15.dist-info/RECORD +21 -0
- {xgae-0.1.13.dist-info → xgae-0.1.15.dist-info}/entry_points.txt +1 -1
- xgae-0.1.13.dist-info/RECORD +0 -21
- {xgae-0.1.13.dist-info → xgae-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -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
|
-
|
|
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
|
|
21
|
-
auto_continue_count = continuous_state
|
|
22
|
-
can_auto_continue
|
|
23
|
-
|
|
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
|
-
|
|
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],
|
|
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":
|
|
48
|
-
assistant_chunk_msg = self.create_response_message(type="assistant_chunk",
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
74
|
-
f"parsed_xml_data_len={len(parsed_xml_data)},
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
188
|
+
tool_start_msg = self._add_tool_start_message(tool_context)
|
|
189
|
+
yield tool_start_msg
|
|
105
190
|
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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'] =
|
|
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")
|