xgae 0.1.5__py3-none-any.whl → 0.1.7__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,25 +1,31 @@
1
1
 
2
2
  import logging
3
3
  import json
4
+ import os
4
5
 
5
- from typing import List, Any, Dict, Optional, AsyncGenerator, cast, Union, Literal
6
+ from typing import List, Any, Dict, Optional, AsyncGenerator, Union, Literal
6
7
  from uuid import uuid4
7
8
 
8
- from xgae.engine.responser.xga_responser_base import TaskResponseContext, TaskResponseProcessor, TaskRunContinuousState
9
- from xgae.engine.xga_base import XGAResponseMsg, XGAToolBox, XGATaskResult
9
+ from xgae.engine.responser.responser_base import TaskResponserContext, TaskResponseProcessor, TaskRunContinuousState
10
+ from xgae.engine.engine_base import XGAResponseMsgType, XGAResponseMessage, XGAToolBox, XGATaskResult
11
+
12
+ from xgae.utils import langfuse, handle_error
10
13
  from xgae.utils.llm_client import LLMClient, LLMConfig
11
- from xgae.utils.setup_env import langfuse
12
- from xgae.utils.utils import handle_error
13
14
 
14
- from xga_prompt_builder import XGAPromptBuilder
15
- from xga_mcp_tool_box import XGAMcpToolBox
15
+ from xgae.utils.json_helpers import format_for_yield
16
+ from xgae.engine.prompt_builder import XGAPromptBuilder
17
+ from xgae.engine.mcp_tool_box import XGAMcpToolBox
16
18
 
17
19
  class XGATaskEngine:
18
20
  def __init__(self,
19
21
  session_id: Optional[str] = None,
20
22
  task_id: Optional[str] = None,
21
23
  agent_id: Optional[str] = None,
24
+ general_tools: Optional[List[str]] = None,
25
+ custom_tools: Optional[List[str]] = None,
22
26
  system_prompt: Optional[str] = None,
27
+ max_auto_run: Optional[int] = None,
28
+ tool_exec_parallel: Optional[bool] = None,
23
29
  llm_config: Optional[LLMConfig] = None,
24
30
  prompt_builder: Optional[XGAPromptBuilder] = None,
25
31
  tool_box: Optional[XGAToolBox] = None):
@@ -32,121 +38,109 @@ class XGATaskEngine:
32
38
  self.is_stream = self.llm_client.is_stream
33
39
 
34
40
  self.prompt_builder = prompt_builder or XGAPromptBuilder(system_prompt)
35
- self.tool_box = tool_box or XGAMcpToolBox()
36
-
37
- self.task_response_msgs: List[XGAResponseMsg] = []
38
- self.task_no = -1
39
- self.task_run_id = f"{self.task_id}[{self.task_no}]"
40
- self.trace_id = None
41
-
42
- async def _post_init_(self, general_tools:List[str], custom_tools: List[str]) -> None:
43
- await self.tool_box.load_mcp_tools_schema()
44
- await self.tool_box.creat_task_tool_box(self.task_id, general_tools, custom_tools)
45
- general_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "general_tool")
46
- custom_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "custom_tool")
47
-
48
- self.task_prompt = self.prompt_builder.build_task_prompt(self.model_name, general_tool_schemas, custom_tool_schemas)
41
+ self.tool_box: XGAToolBox = tool_box or XGAMcpToolBox()
49
42
 
50
- @classmethod
51
- async def create(cls,
52
- session_id: Optional[str] = None,
53
- task_id: Optional[str] = None,
54
- agent_id: Optional[str] = None,
55
- system_prompt: Optional[str] = None,
56
- general_tools: Optional[List[str]] = None,
57
- custom_tools: Optional[List[str]] = None,
58
- llm_config: Optional[LLMConfig] = None,
59
- prompt_builder: Optional[XGAPromptBuilder] = None,
60
- tool_box: Optional[XGAToolBox] = None) -> 'XGATaskEngine':
61
- engine: XGATaskEngine = cls(session_id=session_id,
62
- task_id=task_id,
63
- agent_id=agent_id,
64
- system_prompt=system_prompt,
65
- llm_config=llm_config,
66
- prompt_builder=prompt_builder,
67
- tool_box=tool_box)
43
+ self.general_tools:List[str] = general_tools
44
+ self.custom_tools:List[str] = custom_tools
45
+ self.task_response_msgs: List[XGAResponseMessage] = []
68
46
 
69
- general_tools = general_tools or ["complete", "ask"]
70
- custom_tools = custom_tools or []
71
- await engine._post_init_(general_tools, custom_tools)
47
+ max_auto_run = max_auto_run if max_auto_run else int(os.getenv("MAX_AUTO_RUN", 15))
48
+ self.max_auto_run: int = 1 if max_auto_run <= 1 else max_auto_run
49
+ self.tool_exec_parallel = True if tool_exec_parallel is None else tool_exec_parallel
72
50
 
73
- logging.info("*"*30 + f" XGATaskEngine Task'{engine.task_id}' Initialized " + "*"*30)
74
- logging.info(f"model_name={engine.model_name}, is_stream={engine.is_stream}, trace_id={engine.trace_id}")
75
- logging.info(f"general_tools={general_tools}, custom_tools={custom_tools}")
51
+ self.task_no = -1
52
+ self.task_run_id :str = None
76
53
 
77
- return engine
54
+ self.task_prompt :str = None
55
+ self.trace_id :str = None
56
+ self.root_span_id :str = None
78
57
 
79
58
  async def run_task_with_final_answer(self,
80
- task_message: Dict[str, Any],
81
- max_auto_run: int = 25,
82
- trace_id: Optional[str] = None) -> XGATaskResult:
83
- chunks = []
84
- async for chunk in self.run_task(task_message=task_message, max_auto_run=max_auto_run, trace_id=trace_id):
85
- chunks.append(chunk)
59
+ task_message: Dict[str, Any],
60
+ trace_id: Optional[str] = None) -> XGATaskResult:
61
+ self.trace_id = trace_id or langfuse.create_trace_id()
62
+ with langfuse.start_as_current_span(trace_context={"trace_id": self.trace_id},
63
+ name="run_task_with_final_answer",
64
+ input=task_message,
65
+ metadata={"task_id": self.task_id},
66
+ ) as root_span:
67
+ self.root_span_id = root_span.id
68
+
69
+ chunks = []
70
+ async for chunk in self.run_task(task_message=task_message, trace_id=trace_id):
71
+ chunks.append(chunk)
72
+
73
+ if len(chunks) > 0:
74
+ final_result = self._parse_final_result(chunks)
75
+ else:
76
+ final_result = XGATaskResult(type="error", content="LLM Answer is Empty")
77
+
78
+ root_span.update(output=final_result)
79
+ return final_result
86
80
 
87
- final_result = self._parse_final_result(chunks)
88
- return final_result
89
81
 
90
82
  async def run_task(self,
91
83
  task_message: Dict[str, Any],
92
- max_auto_run: int = 25,
93
84
  trace_id: Optional[str] = None) -> AsyncGenerator[Dict[str, Any], None]:
94
85
  try:
95
- self.trace_id = trace_id or self.trace_id or langfuse.create_trace_id()
86
+ await self._init_task()
87
+ if self.root_span_id is None:
88
+ self.trace_id = trace_id or langfuse.create_trace_id()
89
+ with langfuse.start_as_current_span(trace_context={"trace_id": self.trace_id},
90
+ name="run_task",
91
+ input=task_message
92
+ ) as root_span:
93
+ self.root_span_id = root_span.id
94
+
95
+
96
+ self.add_response_message(type="user", content=task_message, is_llm_message=True)
97
+
98
+ continuous_state: TaskRunContinuousState = {
99
+ "accumulated_content": "",
100
+ "auto_continue_count": 0,
101
+ "auto_continue": False if self.max_auto_run <= 1 else True
102
+ }
103
+ async for chunk in self._run_task_auto(continuous_state):
104
+ yield chunk
105
+ finally:
106
+ await self.tool_box.destroy_task_tool_box(self.task_id)
107
+ self.root_span_id = None
96
108
 
97
- self.task_no += 1
98
- self.task_run_id = f"{self.task_id}[{self.task_no}]"
99
109
 
100
- self.add_response_msg(type="user", content=task_message, is_llm_message=True)
110
+ async def _init_task(self) -> None:
111
+ self.task_no = self.task_no + 1
112
+ self.task_run_id = f"{self.task_id}[{self.task_no}]"
101
113
 
102
- if max_auto_run <= 1:
103
- continuous_state:TaskRunContinuousState = {
104
- "accumulated_content": "",
105
- "auto_continue_count": 0,
106
- "auto_continue": False
107
- }
108
- async for chunk in self._run_task_once(continuous_state):
109
- yield chunk
110
- else:
111
- async for chunk in self._run_task_auto(max_auto_run):
112
- yield chunk
113
- finally:
114
- await self.tool_box.destroy_task_tool_box(self.task_id)
114
+ general_tools = self.general_tools or ["complete", "ask"]
115
+ if "*" not in general_tools:
116
+ if "complete" not in general_tools:
117
+ general_tools.append("complete")
118
+ elif "ask" not in general_tools:
119
+ general_tools.append("ask")
115
120
 
116
- async def _run_task_once(self, continuous_state: TaskRunContinuousState) -> AsyncGenerator[Dict[str, Any], None]:
117
- llm_messages = [{"role": "system", "content": self.task_prompt}]
118
- cxt_llm_contents = self._get_response_llm_contents()
119
- llm_messages.extend(cxt_llm_contents)
121
+ custom_tools = self.custom_tools or []
122
+ if isinstance(self.tool_box, XGAMcpToolBox):
123
+ await self.tool_box.load_mcp_tools_schema()
120
124
 
121
- partial_content = continuous_state.get('accumulated_content', '')
122
- if partial_content:
123
- temp_assistant_message = {
124
- "role": "assistant",
125
- "content": partial_content
126
- }
127
- llm_messages.append(temp_assistant_message)
125
+ await self.tool_box.creat_task_tool_box(self.task_id, general_tools, custom_tools)
126
+ general_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "general_tool")
127
+ custom_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "custom_tool")
128
128
 
129
- llm_response = await self.llm_client.create_completion(llm_messages)
130
- response_processor = self._create_response_processer()
129
+ self.task_prompt = self.prompt_builder.build_task_prompt(self.model_name, general_tool_schemas, custom_tool_schemas)
131
130
 
132
- async for chunk in response_processor.process_response(llm_response, llm_messages, continuous_state):
133
- self._reponse_chunk_log(chunk)
134
- yield chunk
131
+ logging.info("*" * 30 + f" XGATaskEngine Task'{self.task_id}' Initialized " + "*" * 30)
132
+ logging.info(f"model_name={self.model_name}, is_stream={self.is_stream}, trace_id={self.trace_id}")
133
+ logging.info(f"general_tools={general_tools}, custom_tools={custom_tools}")
135
134
 
136
- async def _run_task_auto(self, max_auto_run: int) -> AsyncGenerator[Dict[str, Any], None]:
137
- continuous_state: TaskRunContinuousState = {
138
- "accumulated_content": "",
139
- "auto_continue_count": 0,
140
- "auto_continue": True
141
- }
142
135
 
136
+ async def _run_task_auto(self, continuous_state: TaskRunContinuousState) -> AsyncGenerator[Dict[str, Any], None]:
143
137
  def update_continuous_state(_auto_continue_count, _auto_continue):
144
138
  continuous_state["auto_continue_count"] = _auto_continue_count
145
139
  continuous_state["auto_continue"] = _auto_continue
146
140
 
147
141
  auto_continue_count = 0
148
142
  auto_continue = True
149
- while auto_continue and auto_continue_count < max_auto_run:
143
+ while auto_continue and auto_continue_count < self.max_auto_run:
150
144
  auto_continue = False
151
145
 
152
146
  try:
@@ -163,30 +157,52 @@ class XGATaskEngine:
163
157
  elif status_type == 'finish':
164
158
  finish_reason = content.get('finish_reason', None)
165
159
  if finish_reason == 'completed':
166
- logging.warning(f"run_task_auto: Detected finish_reason='completed', Task Completed Success !")
160
+ logging.info(f"run_task_auto: Detected finish_reason='completed', TASK_COMPLETE Success !")
167
161
  auto_continue = False
168
162
  break
169
163
  elif finish_reason == 'xml_tool_limit_reached':
170
- logging.warning(f"run_task_auto: Detected finish_reason='xml_tool_limit_reached', stopping auto-continue")
164
+ logging.warning(f"run_task_auto: Detected finish_reason='xml_tool_limit_reached', stop auto-continue")
171
165
  auto_continue = False
172
166
  break
173
167
  elif finish_reason == 'stop' or finish_reason == 'length': # 'length' never occur
174
168
  auto_continue = True
175
169
  auto_continue_count += 1
176
170
  update_continuous_state(auto_continue_count, auto_continue)
177
- logging.info(f"run_task_auto: Detected finish_reason='{finish_reason}', auto-continuing ({auto_continue_count}/{max_auto_run})")
178
- except StopAsyncIteration:
179
- pass
171
+ logging.info(f"run_task_auto: Detected finish_reason='{finish_reason}', auto-continuing ({auto_continue_count}/{self.max_auto_run})")
180
172
  except Exception as parse_error:
181
173
  logging.error(f"run_task_auto: Error in parse chunk: {str(parse_error)}")
182
174
  content = {"role": "system", "status_type": "error", "message": "Parse response chunk Error"}
183
- error_msg = self.add_response_msg(type="status", content=content, is_llm_message=False)
184
- yield error_msg
175
+ handle_error(parse_error)
176
+ error_msg = self.add_response_message(type="status", content=content, is_llm_message=False)
177
+ yield format_for_yield(error_msg)
185
178
  except Exception as run_error:
186
179
  logging.error(f"run_task_auto: Call task_run_once error: {str(run_error)}")
187
180
  content = {"role": "system", "status_type": "error", "message": "Call task_run_once error"}
188
- error_msg = self.add_response_msg(type="status", content=content, is_llm_message=False)
189
- yield error_msg
181
+ handle_error(run_error)
182
+ error_msg = self.add_response_message(type="status", content=content, is_llm_message=False)
183
+ yield format_for_yield(error_msg)
184
+
185
+
186
+ async def _run_task_once(self, continuous_state: TaskRunContinuousState) -> AsyncGenerator[Dict[str, Any], None]:
187
+ llm_messages = [{"role": "system", "content": self.task_prompt}]
188
+ cxt_llm_contents = self.get_history_llm_messages()
189
+ llm_messages.extend(cxt_llm_contents)
190
+
191
+ partial_content = continuous_state.get('accumulated_content', '')
192
+ if partial_content:
193
+ temp_assistant_message = {
194
+ "role": "assistant",
195
+ "content": partial_content
196
+ }
197
+ llm_messages.append(temp_assistant_message)
198
+
199
+ llm_response = await self.llm_client.create_completion(llm_messages, self.trace_id)
200
+ response_processor = self._create_response_processer()
201
+
202
+ async for chunk in response_processor.process_response(llm_response, llm_messages, continuous_state):
203
+ self._logging_reponse_chunk(chunk)
204
+ yield chunk
205
+
190
206
 
191
207
  def _parse_final_result(self, chunks: List[Dict[str, Any]]) -> XGATaskResult:
192
208
  final_result: XGATaskResult = None
@@ -200,14 +216,11 @@ class XGATaskEngine:
200
216
  if status_type == "error":
201
217
  error = status_content.get('message', 'Unknown error')
202
218
  final_result = XGATaskResult(type="error", content=error)
203
- break
204
219
  elif status_type == "finish":
205
220
  finish_reason = status_content.get('finish_reason', None)
206
221
  if finish_reason == 'xml_tool_limit_reached':
207
222
  error = "Completed due to over task max_auto_run limit !"
208
223
  final_result = XGATaskResult(type="error", content=error)
209
- break
210
- continue
211
224
  elif chunk_type == "tool" and finish_reason in ['completed', 'stop']:
212
225
  tool_content = json.loads(chunk.get('content', '{}'))
213
226
  tool_execution = tool_content.get('tool_execution')
@@ -229,9 +242,10 @@ class XGATaskEngine:
229
242
  result_content = f"Task execute '{tool_name}' {result_type}: {output}"
230
243
  final_result = XGATaskResult(type=result_type, content=result_content)
231
244
  elif chunk_type == "assistant" and finish_reason == 'stop':
232
- assis_content = chunk.get('content', '{}')
245
+ assis_content = chunk.get('content', {})
233
246
  result_content = assis_content.get("content", "LLM output is empty")
234
247
  final_result = XGATaskResult(type="answer", content=result_content)
248
+
235
249
  if final_result is not None:
236
250
  break
237
251
  except Exception as e:
@@ -241,74 +255,80 @@ class XGATaskEngine:
241
255
 
242
256
  return final_result
243
257
 
244
- def add_response_msg(self, type: Literal["user", "status", "tool", "assistant", "assistant_response_end"],
245
- content: Union[Dict[str, Any], List[Any], str],
246
- is_llm_message: bool,
247
- metadata: Optional[Dict[str, Any]]=None)-> XGAResponseMsg:
248
- message = XGAResponseMsg(
258
+
259
+ def add_response_message(self, type: XGAResponseMsgType,
260
+ content: Union[Dict[str, Any], List[Any], str],
261
+ is_llm_message: bool,
262
+ metadata: Optional[Dict[str, Any]]=None)-> XGAResponseMessage:
263
+ metadata = metadata or {}
264
+ metadata["task_id"] = self.task_id
265
+ metadata["task_run_id"] = self.task_run_id
266
+ metadata["trace_id"] = self.trace_id
267
+ metadata["session_id"] = self.session_id
268
+ metadata["agent_id"] = self.agent_id
269
+
270
+ message = XGAResponseMessage(
249
271
  message_id = f"xga_msg_{uuid4()}",
250
272
  type = type,
251
- content = content,
252
273
  is_llm_message=is_llm_message,
253
- metadata = metadata,
254
- session_id = self.session_id,
255
- agent_id = self.agent_id,
256
- task_id = self.task_id,
257
- task_run_id = self.task_run_id,
258
- trace_id = self.trace_id
274
+ content = content,
275
+ metadata = metadata
259
276
  )
260
277
  self.task_response_msgs.append(message)
261
278
 
262
279
  return message
263
280
 
264
- def _get_response_llm_contents (self) -> List[Dict[str, Any]]:
281
+ def get_history_llm_messages (self) -> List[Dict[str, Any]]:
265
282
  llm_messages = []
266
283
  for message in self.task_response_msgs:
267
284
  if message["is_llm_message"]:
268
285
  llm_messages.append(message)
269
286
 
270
- cxt_llm_contents = []
287
+ response_llm_contents = []
271
288
  for llm_message in llm_messages:
272
289
  content = llm_message["content"]
273
290
  # @todo content List type
274
291
  if isinstance(content, str):
275
292
  try:
276
293
  _content = json.loads(content)
277
- cxt_llm_contents.append(_content)
294
+ response_llm_contents.append(_content)
278
295
  except json.JSONDecodeError as e:
279
296
  logging.error(f"get_context_llm_contents: Failed to decode json, content=:{content}")
280
297
  handle_error(e)
281
298
  else:
282
- cxt_llm_contents.append(content)
299
+ response_llm_contents.append(content)
300
+
301
+ return response_llm_contents
283
302
 
284
- return cxt_llm_contents
285
303
 
286
304
  def _create_response_processer(self) -> TaskResponseProcessor:
287
305
  response_context = self._create_response_context()
288
306
  is_stream = response_context.get("is_stream", False)
289
307
  if is_stream:
290
- from xgae.engine.responser.xga_stream_responser import StreamTaskResponser
308
+ from xgae.engine.responser.stream_responser import StreamTaskResponser
291
309
  return StreamTaskResponser(response_context)
292
310
  else:
293
- from xgae.engine.responser.xga_non_stream_responser import NonStreamTaskResponser
311
+ from xgae.engine.responser.non_stream_responser import NonStreamTaskResponser
294
312
  return NonStreamTaskResponser(response_context)
295
313
 
296
- def _create_response_context(self) -> TaskResponseContext:
297
- response_context: TaskResponseContext = {
314
+ def _create_response_context(self) -> TaskResponserContext:
315
+ response_context: TaskResponserContext = {
298
316
  "is_stream": self.is_stream,
299
317
  "task_id": self.task_id,
300
318
  "task_run_id": self.task_run_id,
301
319
  "trace_id": self.trace_id,
320
+ "root_span_id": self.root_span_id,
302
321
  "model_name": self.model_name,
303
322
  "max_xml_tool_calls": 0,
304
- "add_context_msg": self.add_response_msg,
323
+ "add_response_msg_func": self.add_response_message,
305
324
  "tool_box": self.tool_box,
306
- "tool_execution_strategy": "parallel",
325
+ "tool_execution_strategy": "parallel" if self.tool_exec_parallel else "sequential" ,#,
307
326
  "xml_adding_strategy": "user_message",
308
327
  }
309
328
  return response_context
310
329
 
311
- def _reponse_chunk_log(self, chunk):
330
+
331
+ def _logging_reponse_chunk(self, chunk):
312
332
  chunk_type = chunk.get('type')
313
333
  prefix = ""
314
334
 
@@ -327,21 +347,21 @@ class XGATaskEngine:
327
347
 
328
348
  if __name__ == "__main__":
329
349
  import asyncio
330
- from xgae.utils.utils import read_file
350
+ from xgae.utils.misc import read_file
351
+
331
352
  async def main():
332
353
  tool_box = XGAMcpToolBox(custom_mcp_server_file="mcpservers/custom_servers.json")
333
- system_prompt = read_file("templates/scp_test_prompt.txt")
334
- engine = await XGATaskEngine.create(tool_box=tool_box,
335
- general_tools=[],
336
- custom_tools=["bomc_fault.*"],
337
- llm_config=LLMConfig(stream=False),
338
- system_prompt=system_prompt)
339
- # engine = await XGATaskEngine.create(llm_config=LLMConfig(stream=False))
340
- #chunks = []
341
- # async for chunk in engine.run_task(task_message={"role": "user", "content": "定位10.0.0.1的故障"},max_auto_run=8):
342
- # print(chunk)
343
- #final_result = await engine.run_task_with_final_answer(task_message={"role": "user", "content": "1+1"}, max_auto_run=2)
344
-
345
- final_result = await engine.run_task_with_final_answer(task_message={"role": "user", "content": "定位10.0.1.1故障"},max_auto_run=8)
354
+ system_prompt = read_file("templates/example_user_prompt.txt")
355
+ engine = XGATaskEngine(tool_box=tool_box,
356
+ general_tools=[],
357
+ custom_tools=["*"],
358
+ llm_config=LLMConfig(stream=False),
359
+ system_prompt=system_prompt,
360
+ max_auto_run=8)
361
+
362
+ final_result = await engine.run_task_with_final_answer(task_message={"role": "user",
363
+ "content": "locate 10.0.0.1 fault and solution"})
346
364
  print("FINAL RESULT:", final_result)
365
+
366
+
347
367
  asyncio.run(main())
@@ -0,0 +1,48 @@
1
+ from typing import Annotated, Optional
2
+ from pydantic import Field
3
+
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+ from xgae.engine.engine_base import XGAToolResult
7
+
8
+ mcp = FastMCP(name="XGAE Message Tools")
9
+
10
+ @mcp.tool(
11
+ description="""A special tool to indicate you have completed all tasks and are about to enter complete state. Use ONLY when: 1) All tasks in todo.md are marked complete [x], 2) The user's original request has been fully addressed, 3) There are no pending actions or follow-ups required, 4) You've delivered all final outputs and results to the user. IMPORTANT: This is the ONLY way to properly terminate execution. Never use this tool unless ALL tasks are complete and verified. Always ensure you've provided all necessary outputs and references before using this tool. Include relevant attachments when the completion relates to specific files or resources."""
12
+ )
13
+ async def complete(task_id: str,
14
+ text: Annotated[Optional[str], Field(default=None,
15
+ description="Completion summary. Include: 1) Task summary 2) Key deliverables 3) Next steps 4) Impact achieved")],
16
+ attachments: Annotated[Optional[str], Field(default=None,
17
+ description="Comma-separated list of final outputs. Use when: 1) Completion relates to files 2) User needs to review outputs 3) Deliverables in files")]
18
+ ):
19
+ print(f"<XGAETools-complete>: task_id={task_id}, text={text}, attachments={attachments}")
20
+ return XGAToolResult(success=True, output=str({"status": "complete"}))
21
+
22
+
23
+ @mcp.tool(
24
+ description="""Ask user a question and wait for response. Use for: 1) Requesting clarification on ambiguous requirements, 2) Seeking confirmation before proceeding with high-impact changes, 3) Gathering additional information needed to complete a task, 4) Offering options and requesting user preference, 5) Validating assumptions when critical to task success, 6) When encountering unclear or ambiguous results during task execution, 7) When tool results don't match expectations, 8) For natural conversation and follow-up questions, 9) When research reveals multiple entities with the same name, 10) When user requirements are unclear or could be interpreted differently. IMPORTANT: Use this tool when user input is essential to proceed. Always provide clear context and options when applicable. Use natural, conversational language that feels like talking with a helpful friend. Include relevant attachments when the question relates to specific files or resources. CRITICAL: When you discover ambiguity (like multiple people with the same name), immediately stop and ask for clarification rather than making assumptions."""
25
+ )
26
+ async def ask(task_id: str,
27
+ text: Annotated[str, Field(
28
+ description="Question text to present to user. Include: 1) Clear question/request 2) Context why input is needed 3) Available options 4) Impact of choices 5) Relevant constraints")],
29
+ attachments: Annotated[Optional[str], Field(default=None,
30
+ description="Comma-separated list of files/URLs to attach. Use when: 1) Question relates to files/configs 2) User needs to review content 3) Options documented in files 4) Supporting evidence needed")]
31
+ ):
32
+ print(f"<XGAETools-ask>: task_id={task_id}, text={text}, attachments={attachments}")
33
+ return XGAToolResult(success=True, output=str({"status": "Awaiting user response..."}))
34
+
35
+ @mcp.tool(
36
+ description="end task, destroy sandbox"
37
+ )
38
+ async def end_task(task_id: str) :
39
+ print(f"<XGAETools-end_task> task_id: {task_id}")
40
+
41
+
42
+
43
+ def main():
44
+ #print("="*20 + " XGAE Message Tools Sever Started in Stdio mode " + "="*20)
45
+ mcp.run(transport="stdio")
46
+
47
+ if __name__ == "__main__":
48
+ main()
xgae/utils/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ import logging
2
+
3
+ from .setup_env import setup_langfuse, setup_logging
4
+
5
+ setup_logging()
6
+ langfuse = setup_langfuse()
7
+
8
+ def handle_error(e: Exception) -> None:
9
+ import traceback
10
+
11
+ logging.error("An error occurred: %s", str(e))
12
+ logging.error("Traceback details:\n%s", traceback.format_exc())
13
+ raise (e) from e
xgae/utils/llm_client.py CHANGED
@@ -47,6 +47,7 @@ class LLMClient:
47
47
  reasoning_effort: Optional level of reasoning effort, default is ‘low’
48
48
  top_p: Optional Top-p sampling parameter, default is None
49
49
  """
50
+
50
51
  llm_config = llm_config or LLMConfig()
51
52
  litellm.modify_params = True
52
53
  litellm.drop_params = True
@@ -205,9 +206,10 @@ class LLMClient:
205
206
  logging.debug(f"LLMClient: Waiting {delay} seconds before retry llm completion...")
206
207
  await asyncio.sleep(delay)
207
208
 
208
-
209
- async def create_completion(self, messages: List[Dict[str, Any]]) -> Union[ModelResponse, CustomStreamWrapper]:
209
+ async def create_completion(self, messages: List[Dict[str, Any]], trace_id: Optional[str]=None) -> Union[ModelResponse, CustomStreamWrapper]:
210
210
  complete_params = self._prepare_complete_params(messages)
211
+ if trace_id:
212
+ complete_params["litellm_trace_id"] = trace_id
211
213
 
212
214
  last_error = None
213
215
  for attempt in range(self.max_retries):
@@ -226,10 +228,13 @@ class LLMClient:
226
228
  raise LLMError(f"LLM completion failed after {self.max_retries} attempts !")
227
229
 
228
230
  if __name__ == "__main__":
231
+ from xgae.utils import langfuse
232
+
229
233
  async def llm_completion():
230
234
  llm_client = LLMClient(LLMConfig(stream=False))
231
235
  messages = [{"role": "user", "content": "今天是2025年8月15日,北京本周每天温度"}]
232
- response = await llm_client.create_completion(messages)
236
+ trace_id = langfuse.create_trace_id()
237
+ response = await llm_client.create_completion(messages, trace_id)
233
238
  if llm_client.is_stream:
234
239
  async for chunk in response:
235
240
  choices = chunk.get("choices", [{}])
@@ -1,17 +1,9 @@
1
1
  import logging
2
2
  import os
3
3
  import sys
4
- import datetime
5
4
 
6
5
  from typing import Any, Dict
7
6
 
8
- def handle_error(e: Exception) -> None:
9
- import traceback
10
-
11
- logging.error("An error occurred: %s", str(e))
12
- logging.error("Traceback details:\n%s", traceback.format_exc())
13
- raise (e) from e
14
-
15
7
  def read_file(file_path: str) -> str:
16
8
  if not os.path.exists(file_path):
17
9
  logging.error(f"File '{file_path}' not found")