xgae 0.1.8__py3-none-any.whl → 0.1.9__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.

@@ -7,7 +7,7 @@ class XGAError(Exception):
7
7
  pass
8
8
 
9
9
  XGAMsgStatusType = Literal["error", "finish", "tool_error", "tool_started", "tool_completed", "tool_failed", "thread_run_start", "thread_run_end", "assistant_response_start", "assistant_response_end"]
10
- XGAResponseMsgType = Literal["user", "status", "tool", "assistant"]
10
+ XGAResponseMsgType = Literal["user", "status", "tool", "assistant", "assistant_complete"]
11
11
 
12
12
  class XGAResponseMessage(TypedDict, total=False):
13
13
  message_id: str
@@ -190,11 +190,16 @@ class XGAMcpToolBox(XGAToolBox):
190
190
  if __name__ == "__main__":
191
191
  import asyncio
192
192
  from dataclasses import asdict
193
+ from xgae.utils.setup_env import setup_logging
194
+
195
+ setup_logging()
193
196
 
194
197
  async def main():
195
- task_id = "task1"
198
+ ## Before Run Exec: uv run custom_fault_tools
196
199
  mcp_tool_box = XGAMcpToolBox(custom_mcp_server_file="mcpservers/custom_servers.json")
197
200
  #mcp_tool_box = XGAMcpToolBox()
201
+
202
+ task_id = "task1"
198
203
  await mcp_tool_box.load_mcp_tools_schema()
199
204
  await mcp_tool_box.creat_task_tool_box(task_id=task_id, general_tools=["*"], custom_tools=["bomc_fault.*"])
200
205
  tool_schemas = mcp_tool_box.get_task_tool_schemas(task_id, "general_tool")
@@ -3,7 +3,7 @@ import datetime
3
3
 
4
4
  from typing import Optional, List
5
5
 
6
- from xgae.engine.engine_base import XGAToolSchema, XGAError
6
+ from xgae.engine.engine_base import XGAToolSchema
7
7
  from xgae.utils.misc import read_file, format_file_with_args
8
8
 
9
9
 
@@ -1,9 +1,10 @@
1
1
  import logging
2
2
 
3
3
  from typing import List, Dict, Any, AsyncGenerator, override,Optional
4
+ from xgae.utils.json_helpers import format_for_yield
4
5
 
5
6
  from xgae.engine.responser.responser_base import TaskResponseProcessor, TaskResponserContext, TaskRunContinuousState
6
- from xgae.utils.json_helpers import format_for_yield
7
+
7
8
 
8
9
  class NonStreamTaskResponser(TaskResponseProcessor):
9
10
  def __init__(self, response_context: TaskResponserContext):
@@ -47,8 +48,8 @@ class NonStreamTaskResponser(TaskResponseProcessor):
47
48
  else:
48
49
  logging.warning(f"NonStreamTask:LLM response_message is empty")
49
50
 
50
- message_data = {"role": "assistant", "content": llm_content, "index": -1} # index=-1, full llm_content
51
- assistant_msg = self.add_response_message(type="assistant", content=message_data, is_llm_message=True)
51
+ message_data = {"role": "assistant", "content": llm_content} # index=-1, full llm_content
52
+ assistant_msg = self.add_response_message(type="assistant_complete", content=message_data, is_llm_message=True)
52
53
  yield assistant_msg
53
54
 
54
55
  tool_calls_to_execute = [item['tool_call'] for item in parsed_xml_data]
@@ -6,11 +6,12 @@ from abc import ABC, abstractmethod
6
6
  from dataclasses import dataclass
7
7
  from typing import List, Dict, Any, Optional, Tuple, Union, Literal, Callable, TypedDict, AsyncGenerator
8
8
 
9
+ from xgae.utils.json_helpers import safe_json_parse
10
+ from xgae.utils.xml_tool_parser import XMLToolParser
11
+
9
12
  from xgae.engine.engine_base import XGAToolResult, XGAToolBox
10
- from xgae.utils.setup_env import langfuse
13
+ from xgae.engine.task_langfuse import XGATaskLangFuse
11
14
 
12
- from xgae.utils.json_helpers import safe_json_parse, format_for_yield
13
- from xgae.utils.xml_tool_parser import XMLToolParser
14
15
 
15
16
  # Type alias for XML result adding strategy
16
17
  XmlAddingStrategy = Literal["user_message", "assistant_message", "inline_edit"]
@@ -23,14 +24,13 @@ class TaskResponserContext(TypedDict, total=False):
23
24
  task_id: str
24
25
  task_run_id: str
25
26
  task_no: int
26
- trace_id: str
27
- root_span_id: str
28
27
  model_name: str
29
28
  max_xml_tool_calls: int # LLM generate max_xml_tool limit, 0 is no limit
30
- add_response_msg_func: Callable
31
- tool_box: XGAToolBox
32
29
  tool_execution_strategy: ToolExecutionStrategy
33
30
  xml_adding_strategy: XmlAddingStrategy
31
+ add_response_msg_func: Callable
32
+ tool_box: XGAToolBox
33
+ task_langfuse: XGATaskLangFuse
34
34
 
35
35
 
36
36
  class TaskRunContinuousState(TypedDict, total=False):
@@ -63,14 +63,14 @@ class TaskResponseProcessor(ABC):
63
63
  self.xml_adding_strategy = self.response_context.get("xml_adding_strategy", "user_message")
64
64
  self.max_xml_tool_calls = self.response_context.get("max_xml_tool_calls", 0)
65
65
 
66
- self.root_span = langfuse.span(trace_id=response_context.get("trace_id"), id=response_context.get("root_span_id"))
66
+ task_langfuse = response_context.get("task_langfuse")
67
+ self.root_span = task_langfuse.root_span
67
68
  self.add_response_message = response_context.get("add_response_msg_func")
68
69
 
69
70
  self.tool_box = response_context.get("tool_box")
70
71
  self.xml_parser = XMLToolParser()
71
72
 
72
73
 
73
-
74
74
  @abstractmethod
75
75
  async def process_response(self,
76
76
  llm_response: AsyncGenerator,
@@ -1,4 +1,3 @@
1
-
2
1
  import logging
3
2
  import json
4
3
  import os
@@ -6,18 +5,15 @@ import os
6
5
  from typing import List, Any, Dict, Optional, AsyncGenerator, Union, Literal
7
6
  from uuid import uuid4
8
7
 
9
- from xgae.engine.responser.responser_base import TaskResponserContext, TaskResponseProcessor, TaskRunContinuousState
10
- from xgae.engine.engine_base import XGAResponseMsgType, XGAResponseMessage, XGAToolBox, XGATaskResult
11
-
12
8
  from xgae.utils import handle_error
13
- from xgae.utils.setup_env import langfuse
14
-
15
- from xgae.utils.llm_client import LLMClient, LLMConfig, LangfuseMetadata
16
-
9
+ from xgae.utils.llm_client import LLMClient, LLMConfig
17
10
  from xgae.utils.json_helpers import format_for_yield
11
+
12
+ from xgae.engine.engine_base import XGAResponseMsgType, XGAResponseMessage, XGAToolBox, XGATaskResult
13
+ from xgae.engine.task_langfuse import XGATaskLangFuse
18
14
  from xgae.engine.prompt_builder import XGAPromptBuilder
19
15
  from xgae.engine.mcp_tool_box import XGAMcpToolBox
20
-
16
+ from xgae.engine.responser.responser_base import TaskResponserContext, TaskResponseProcessor, TaskRunContinuousState
21
17
 
22
18
  class XGATaskEngine:
23
19
  def __init__(self,
@@ -42,6 +38,7 @@ class XGATaskEngine:
42
38
 
43
39
  self.prompt_builder = prompt_builder or XGAPromptBuilder(system_prompt)
44
40
  self.tool_box: XGAToolBox = tool_box or XGAMcpToolBox()
41
+ self.task_langfuse: XGATaskLangFuse = None
45
42
 
46
43
  self.general_tools:List[str] = general_tools
47
44
  self.custom_tools:List[str] = custom_tools
@@ -53,18 +50,18 @@ class XGATaskEngine:
53
50
 
54
51
  self.task_no = -1
55
52
  self.task_run_id :str = None
56
-
57
53
  self.task_prompt :str = None
58
- self.trace_id :str = None
59
- self.root_span_id :str = None
60
- self.root_span_name :str = None
54
+
61
55
 
62
56
  async def run_task_with_final_answer(self,
63
57
  task_message: Dict[str, Any],
64
58
  trace_id: Optional[str] = None) -> XGATaskResult:
65
59
  final_result:XGATaskResult = None
66
60
  try:
67
- self._init_langfuse("run_task_with_final_answer", task_message, trace_id)
61
+ await self._init_task()
62
+
63
+ self.task_langfuse.start_root_span("run_task_with_final_answer", task_message, trace_id)
64
+
68
65
  chunks = []
69
66
  async for chunk in self.run_task(task_message=task_message, trace_id=trace_id):
70
67
  chunks.append(chunk)
@@ -76,7 +73,7 @@ class XGATaskEngine:
76
73
 
77
74
  return final_result
78
75
  finally:
79
- self._end_langfuse("run_task_with_final_answer", final_result)
76
+ self.task_langfuse.end_root_span("run_task_with_final_answer", final_result)
80
77
 
81
78
 
82
79
  async def run_task(self,
@@ -84,7 +81,8 @@ class XGATaskEngine:
84
81
  trace_id: Optional[str] = None) -> AsyncGenerator[Dict[str, Any], None]:
85
82
  try:
86
83
  await self._init_task()
87
- self._init_langfuse("run_task", task_message, trace_id)
84
+
85
+ self.task_langfuse.start_root_span("run_task", task_message, trace_id)
88
86
 
89
87
  self.add_response_message(type="user", content=task_message, is_llm_message=True)
90
88
 
@@ -92,33 +90,36 @@ class XGATaskEngine:
92
90
  yield chunk
93
91
  finally:
94
92
  await self.tool_box.destroy_task_tool_box(self.task_id)
95
- self._end_langfuse("run_task")
96
-
93
+ self.task_langfuse.end_root_span("run_task")
94
+ self.task_run_id = None
97
95
 
98
96
  async def _init_task(self) -> None:
99
- self.task_no = self.task_no + 1
100
- self.task_run_id = f"{self.task_id}[{self.task_no}]"
97
+ if self.task_run_id is None:
98
+ self.task_no = self.task_no + 1
99
+ self.task_run_id = f"{self.task_id}[{self.task_no}]"
100
+
101
+ self.task_langfuse =self._create_task_langfuse()
101
102
 
102
- general_tools = self.general_tools or ["complete", "ask"]
103
- if "*" not in general_tools:
104
- if "complete" not in general_tools:
105
- general_tools.append("complete")
106
- elif "ask" not in general_tools:
107
- general_tools.append("ask")
103
+ general_tools = self.general_tools or ["complete", "ask"]
104
+ if "*" not in general_tools:
105
+ if "complete" not in general_tools:
106
+ general_tools.append("complete")
107
+ elif "ask" not in general_tools:
108
+ general_tools.append("ask")
108
109
 
109
- custom_tools = self.custom_tools or []
110
- if isinstance(self.tool_box, XGAMcpToolBox):
111
- await self.tool_box.load_mcp_tools_schema()
110
+ custom_tools = self.custom_tools or []
111
+ if isinstance(self.tool_box, XGAMcpToolBox):
112
+ await self.tool_box.load_mcp_tools_schema()
112
113
 
113
- await self.tool_box.creat_task_tool_box(self.task_id, general_tools, custom_tools)
114
- general_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "general_tool")
115
- custom_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "custom_tool")
114
+ await self.tool_box.creat_task_tool_box(self.task_id, general_tools, custom_tools)
115
+ general_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "general_tool")
116
+ custom_tool_schemas = self.tool_box.get_task_tool_schemas(self.task_id, "custom_tool")
116
117
 
117
- self.task_prompt = self.prompt_builder.build_task_prompt(self.model_name, general_tool_schemas, custom_tool_schemas)
118
+ self.task_prompt = self.prompt_builder.build_task_prompt(self.model_name, general_tool_schemas, custom_tool_schemas)
118
119
 
119
- logging.info("*" * 30 + f" XGATaskEngine Task'{self.task_id}' Initialized " + "*" * 30)
120
- logging.info(f"model_name={self.model_name}, is_stream={self.is_stream}, trace_id={self.trace_id}")
121
- logging.info(f"general_tools={general_tools}, custom_tools={custom_tools}")
120
+ logging.info("*" * 30 + f" XGATaskEngine Task'{self.task_id}' Initialized " + "*" * 30)
121
+ logging.info(f"model_name={self.model_name}, is_stream={self.is_stream}")
122
+ logging.info(f"general_tools={general_tools}, custom_tools={custom_tools}")
122
123
 
123
124
 
124
125
  async def _run_task_auto(self) -> AsyncGenerator[Dict[str, Any], None]:
@@ -191,7 +192,8 @@ class XGATaskEngine:
191
192
  llm_messages.append(temp_assistant_message)
192
193
 
193
194
  llm_count = continuous_state.get("auto_continue_count")
194
- langfuse_metadata = self._create_llm_langfuse_meta(llm_count)
195
+ langfuse_metadata = self.task_langfuse.create_llm_langfuse_meta(llm_count)
196
+
195
197
  llm_response = await self.llm_client.create_completion(llm_messages, langfuse_metadata)
196
198
  response_processor = self._create_response_processer()
197
199
 
@@ -236,7 +238,7 @@ class XGATaskEngine:
236
238
  result_type = "answer" if success else "error"
237
239
  result_content = f"Task execute '{tool_name}' {result_type}: {output}"
238
240
  final_result = XGATaskResult(type=result_type, content=result_content)
239
- elif chunk_type == "assistant" and finish_reason == 'stop':
241
+ elif chunk_type == "assistant_complete" and finish_reason == 'stop':
240
242
  assis_content = chunk.get('content', {})
241
243
  result_content = assis_content.get("content", "LLM output is empty")
242
244
  final_result = XGATaskResult(type="answer", content=result_content)
@@ -258,7 +260,7 @@ class XGATaskEngine:
258
260
  metadata = metadata or {}
259
261
  metadata["task_id"] = self.task_id
260
262
  metadata["task_run_id"] = self.task_run_id
261
- metadata["trace_id"] = self.trace_id
263
+ metadata["trace_id"] = self.task_langfuse.trace_id
262
264
  metadata["session_id"] = self.session_id
263
265
  metadata["agent_id"] = self.agent_id
264
266
 
@@ -295,41 +297,6 @@ class XGATaskEngine:
295
297
 
296
298
  return response_llm_contents
297
299
 
298
-
299
- def _create_llm_langfuse_meta(self, llm_count:int)-> LangfuseMetadata:
300
- generation_name = f"xga_task_engine_llm_completion[{self.task_no}]({llm_count})"
301
- generation_id = f"{self.task_run_id}({llm_count})"
302
- return LangfuseMetadata(
303
- generation_name=generation_name,
304
- generation_id=generation_id,
305
- existing_trace_id=self.trace_id,
306
- session_id=self.session_id,
307
- )
308
-
309
- def _init_langfuse(self,
310
- root_span_name: str,
311
- task_message: Dict[str, Any],
312
- trace_id: Optional[str] = None):
313
-
314
- if self.root_span_id is None:
315
- trace = None
316
- if trace_id:
317
- self.trace_id = trace_id
318
- trace = langfuse.trace(id=trace_id)
319
- else:
320
- trace = langfuse.trace(name="xga_task_engine")
321
- self.trace_id = trace.id
322
-
323
- span = trace.span(name=root_span_name, input=task_message,metadata={"task_id": self.task_id})
324
- self.root_span_id = span.id
325
- self.root_span_name = root_span_name
326
-
327
- def _end_langfuse(self, root_span_name:str, output: Optional[XGATaskResult]=None):
328
- if self.root_span_id and self.root_span_name == root_span_name:
329
- langfuse.span(trace_id=self.trace_id, id=self.root_span_id).end(output=output)
330
- self.root_span_id = None
331
- self.root_span_name = None
332
-
333
300
  def _create_response_processer(self) -> TaskResponseProcessor:
334
301
  response_context = self._create_response_context()
335
302
  is_stream = response_context.get("is_stream", False)
@@ -346,18 +313,21 @@ class XGATaskEngine:
346
313
  "task_id": self.task_id,
347
314
  "task_run_id": self.task_run_id,
348
315
  "task_no": self.task_no,
349
- "trace_id": self.trace_id,
350
- "root_span_id": self.root_span_id,
351
316
  "model_name": self.model_name,
352
317
  "max_xml_tool_calls": 0,
318
+ "tool_execution_strategy": "parallel" if self.tool_exec_parallel else "sequential", # ,
319
+ "xml_adding_strategy": "user_message",
353
320
  "add_response_msg_func": self.add_response_message,
354
321
  "tool_box": self.tool_box,
355
- "tool_execution_strategy": "parallel" if self.tool_exec_parallel else "sequential" ,#,
356
- "xml_adding_strategy": "user_message",
322
+ "task_langfuse": self.task_langfuse,
357
323
  }
358
324
  return response_context
359
325
 
360
326
 
327
+ def _create_task_langfuse(self)-> XGATaskLangFuse:
328
+ return XGATaskLangFuse(self.session_id, self.task_id, self.task_run_id, self.task_no, self.agent_id)
329
+
330
+
361
331
  def _logging_reponse_chunk(self, chunk):
362
332
  chunk_type = chunk.get('type')
363
333
  prefix = ""
@@ -380,6 +350,7 @@ if __name__ == "__main__":
380
350
  from xgae.utils.misc import read_file
381
351
 
382
352
  async def main():
353
+ # Before Run Exec: uv run custom_fault_tools
383
354
  tool_box = XGAMcpToolBox(custom_mcp_server_file="mcpservers/custom_servers.json")
384
355
  system_prompt = read_file("templates/example_user_prompt.txt")
385
356
  engine = XGATaskEngine(tool_box=tool_box,
@@ -387,7 +358,9 @@ if __name__ == "__main__":
387
358
  custom_tools=["*"],
388
359
  llm_config=LLMConfig(stream=False),
389
360
  system_prompt=system_prompt,
390
- max_auto_run=8)
361
+ max_auto_run=8,
362
+ session_id="session_1",
363
+ agent_id="agent_1",)
391
364
 
392
365
  final_result = await engine.run_task_with_final_answer(task_message={"role": "user",
393
366
  "content": "locate 10.0.0.1 fault and solution"})
@@ -0,0 +1,63 @@
1
+
2
+ from typing import Any, Dict, Optional
3
+ from langfuse import Langfuse
4
+
5
+ from xgae.utils.setup_env import setup_langfuse, setup_env_logging
6
+ from xgae.utils.llm_client import LangfuseMetadata
7
+ from xgae.engine.engine_base import XGATaskResult
8
+
9
+ setup_env_logging()
10
+ langfuse:Langfuse = setup_langfuse()
11
+
12
+ class XGATaskLangFuse:
13
+ def __init__(self,
14
+ session_id: str,
15
+ task_id:str,
16
+ task_run_id: str,
17
+ task_no: int,
18
+ agent_id: str) -> None:
19
+ self.session_id = session_id
20
+ self.task_id = task_id
21
+ self.task_run_id = task_run_id
22
+ self.task_no = task_no
23
+ self.agent_id = agent_id
24
+
25
+ self.trace_id = None
26
+ self.root_span = None
27
+ self.root_span_name = None
28
+
29
+
30
+ def start_root_span(self,
31
+ root_span_name: str,
32
+ task_message: Dict[str, Any],
33
+ trace_id: Optional[str] = None):
34
+ if self.root_span is None:
35
+ trace = None
36
+ if trace_id:
37
+ self.trace_id = trace_id
38
+ trace = langfuse.trace(id=trace_id)
39
+ else:
40
+ trace = langfuse.trace(name="xga_task_engine")
41
+ self.trace_id = trace.id
42
+
43
+ metadata = {"task_id": self.task_id, "session_id": self.session_id, "agent_id": self.agent_id}
44
+ self.root_span = trace.span(id=self.task_run_id, name=root_span_name, input=task_message,metadata=metadata)
45
+ self.root_span_name = root_span_name
46
+
47
+
48
+ def end_root_span(self, root_span_name:str, output: Optional[XGATaskResult]=None):
49
+ if self.root_span and self.root_span_name == root_span_name:
50
+ self.root_span.end(output=output)
51
+ self.root_span = None
52
+ self.root_span_name = None
53
+
54
+
55
+ def create_llm_langfuse_meta(self, llm_count:int)-> LangfuseMetadata:
56
+ generation_name = f"xga_task_engine_llm_completion[{self.task_no}]({llm_count})"
57
+ generation_id = f"{self.task_run_id}({llm_count})"
58
+ return LangfuseMetadata(
59
+ generation_name=generation_name,
60
+ generation_id=generation_id,
61
+ existing_trace_id=self.trace_id,
62
+ session_id=self.session_id,
63
+ )
@@ -41,7 +41,7 @@ async def end_task(task_id: str) :
41
41
 
42
42
 
43
43
  def main():
44
- #print("="*20 + " XGAE Message Tools Sever Started in Stdio mode " + "="*20)
44
+ print("="*20 + " XGAE Message Tools Sever Started in Stdio mode " + "="*20)
45
45
  mcp.run(transport="stdio")
46
46
 
47
47
  if __name__ == "__main__":
xgae/utils/__init__.py CHANGED
@@ -7,3 +7,9 @@ def handle_error(e: Exception) -> None:
7
7
  logging.error("Traceback details:\n%s", traceback.format_exc())
8
8
  raise (e) from e
9
9
 
10
+
11
+ def to_bool(value: str) -> bool:
12
+ if value is None:
13
+ return False
14
+
15
+ return True if value.lower() == "true" else False
@@ -26,8 +26,7 @@ def ensure_dict(value: Union[str, Dict[str, Any], None], default: Dict[str, Any]
26
26
  Returns:
27
27
  A dictionary
28
28
  """
29
- if default is None:
30
- default = {}
29
+ default = default or {}
31
30
 
32
31
  if value is None:
33
32
  return default
@@ -64,8 +63,7 @@ def ensure_list(value: Union[str, List[Any], None], default: List[Any] = None) -
64
63
  Returns:
65
64
  A list
66
65
  """
67
- if default is None:
68
- default = []
66
+ default = default or []
69
67
 
70
68
  if value is None:
71
69
  return default
@@ -84,7 +82,7 @@ def ensure_list(value: Union[str, List[Any], None], default: List[Any] = None) -
84
82
 
85
83
  return default
86
84
 
87
-
85
+ # @todo if all call value is str, delete useless code
88
86
  def safe_json_parse(value: Union[str, Dict, List, Any], default: Any = None) -> Any:
89
87
  """
90
88
  Safely parse a value that might be JSON string or already parsed.
@@ -105,16 +103,13 @@ def safe_json_parse(value: Union[str, Dict, List, Any], default: Any = None) ->
105
103
  # If it's already a dict or list, return as-is
106
104
  if isinstance(value, (dict, list)):
107
105
  return value
108
-
109
- # If it's a string, try to parse it
106
+
110
107
  if isinstance(value, str):
111
108
  try:
112
109
  return json.loads(value)
113
110
  except (json.JSONDecodeError, TypeError):
114
- # If it's not valid JSON, return the string itself
115
111
  return value
116
-
117
- # For any other type, return as-is
112
+
118
113
  return value
119
114
 
120
115
 
@@ -137,9 +132,8 @@ def to_json_string(value: Any) -> str:
137
132
  json.loads(value)
138
133
  return value # It's already a JSON string
139
134
  except (json.JSONDecodeError, TypeError):
140
- # It's a plain string, encode it as JSON
141
- return json.dumps(value)
142
-
135
+ pass
136
+
143
137
  # For all other types, convert to JSON
144
138
  return json.dumps(value)
145
139
 
xgae/utils/llm_client.py CHANGED
@@ -1,15 +1,14 @@
1
1
  import asyncio
2
2
  import json
3
- import logging
4
3
  import os
4
+ import logging
5
5
  import litellm
6
6
 
7
7
  from typing import Union, Dict, Any, Optional, List, TypedDict
8
-
9
- from litellm.utils import ModelResponse, CustomStreamWrapper
10
8
  from openai import OpenAIError
9
+ from litellm.utils import ModelResponse, CustomStreamWrapper
11
10
 
12
- from xgae.utils.setup_env import langfuse
11
+ from xgae.utils.setup_env import setup_langfuse
13
12
 
14
13
  class LLMConfig(TypedDict, total=False):
15
14
  model: str # Optional Name of the model to use , Override .env LLM_MODEL
@@ -85,7 +84,8 @@ class LLMClient:
85
84
  def _init_langfuse():
86
85
  if not LLMClient.langfuse_inited:
87
86
  LLMClient.langfuse_inited =True
88
- if langfuse and langfuse.enabled:
87
+ env_langfuse = setup_langfuse()
88
+ if env_langfuse and env_langfuse.enabled:
89
89
  litellm.success_callback = ["langfuse"]
90
90
  litellm.failure_callback = ["langfuse"]
91
91
  LLMClient.langfuse_enabled = True
@@ -95,7 +95,6 @@ class LLMClient:
95
95
  logging.warning("*** LiteLLM Langfuse is disable !")
96
96
 
97
97
 
98
-
99
98
  def _prepare_llm_params(self, llm_config_params: Dict[str, Any]) -> Dict[str, Any]:
100
99
  prepared_llm_params = llm_config_params.copy()
101
100
 
@@ -240,12 +239,24 @@ class LLMClient:
240
239
  raise LLMError(f"LLM completion failed after {self.max_retries} attempts !")
241
240
 
242
241
  if __name__ == "__main__":
242
+ from xgae.utils.setup_env import setup_logging
243
+
244
+ setup_logging()
245
+ langfuse = setup_langfuse()
246
+
243
247
  async def llm_completion():
244
248
  llm_client = LLMClient(LLMConfig(stream=False))
249
+
245
250
  messages = [{"role": "user", "content": "1+1="}]
246
251
  trace_id = langfuse.trace(name = "xgae_litellm_test").trace_id
247
-
248
- response = await llm_client.create_completion(messages, trace_id)
252
+ meta = LangfuseMetadata(
253
+ generation_name="llm_completion_test",
254
+ generation_id="generation_id",
255
+ existing_trace_id=trace_id,
256
+ session_id="session_0",
257
+ )
258
+
259
+ response = await llm_client.create_completion(messages, meta)
249
260
  if llm_client.is_stream:
250
261
  async for chunk in response:
251
262
  choices = chunk.get("choices", [{}])
xgae/utils/misc.py CHANGED
@@ -4,6 +4,8 @@ import sys
4
4
 
5
5
  from typing import Any, Dict
6
6
 
7
+ from xgae.utils import handle_error
8
+
7
9
  def read_file(file_path: str) -> str:
8
10
  if not os.path.exists(file_path):
9
11
  logging.error(f"File '{file_path}' not found")
@@ -31,4 +33,4 @@ def format_file_with_args(file_content:str, args: Dict[str, Any])-> str:
31
33
  finally:
32
34
  sys.stdout = original_stdout
33
35
 
34
- return formated
36
+ return formated
xgae/utils/setup_env.py CHANGED
@@ -4,24 +4,14 @@ import os
4
4
  from dotenv import load_dotenv
5
5
  from langfuse import Langfuse
6
6
 
7
- load_dotenv()
7
+ from xgae.utils import to_bool
8
8
 
9
- def setup_logging() :
10
- log_enable = bool(os.getenv("LOG_ENABLE", True))
11
- if not log_enable :
12
- return
9
+ load_dotenv()
13
10
 
11
+ def setup_logging(log_file: str=None, log_level: str="INFO") :
14
12
  import colorlog
15
13
 
16
- env_log_level = os.getenv("LOG_LEVEL", "INFO")
17
- log_file = os.getenv("LOG_FILE", "log/xga.log")
18
- log_level = getattr(logging, env_log_level.upper(), logging.INFO)
19
-
20
- log_dir = os.path.dirname(log_file)
21
- if log_dir and not os.path.exists(log_dir):
22
- os.makedirs(log_dir, exist_ok=True)
23
- else:
24
- os.remove(log_file)
14
+ logging_level = getattr(logging, log_level.upper(), logging.INFO)
25
15
 
26
16
  logger = logging.getLogger()
27
17
  for handler in logger.handlers[:]:
@@ -40,25 +30,36 @@ def setup_logging() :
40
30
  datefmt='%Y-%m-%d %H:%M:%S'
41
31
  )
42
32
 
43
- file_formatter = logging.Formatter(
44
- '%(asctime)s -%(levelname)-8s %(message)s',
45
- datefmt='%Y-%m-%d %H:%M:%S'
46
- )
47
-
48
33
  console_handler = logging.StreamHandler()
49
34
  console_handler.setFormatter(console_formatter)
35
+ logger.addHandler(console_handler)
50
36
 
51
- file_handler = logging.FileHandler(log_file, encoding='utf-8')
52
- file_handler.setFormatter(file_formatter)
37
+ if log_file:
38
+ log_dir = os.path.dirname(log_file)
39
+ if log_dir and not os.path.exists(log_dir):
40
+ os.makedirs(log_dir, exist_ok=True)
41
+ else:
42
+ os.remove(log_file)
53
43
 
54
- logger.addHandler(console_handler)
55
- logger.addHandler(file_handler)
44
+ file_formatter = logging.Formatter(
45
+ '%(asctime)s -%(levelname)-8s %(message)s',
46
+ datefmt='%Y-%m-%d %H:%M:%S'
47
+ )
48
+
49
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
50
+ file_handler.setFormatter(file_formatter)
51
+ logger.addHandler(file_handler)
56
52
 
57
- logger.setLevel(log_level)
53
+ logger.setLevel(logging_level)
58
54
 
59
- logging.info(f"📡 XGAE_LOGGING is initialized, log_level={env_log_level}, log_file={log_file}")
55
+ logging.info(f"📡 XGAE_LOGGING is initialized, log_level={log_level}, log_file={log_file}")
60
56
 
61
- setup_logging()
57
+ def setup_env_logging():
58
+ log_enable = to_bool(os.getenv("LOG_ENABLE", True))
59
+ log_level = os.getenv("LOG_LEVEL", "INFO")
60
+ log_file = os.getenv("LOG_FILE", "log/xga.log")
61
+ if log_enable :
62
+ setup_logging(log_file, log_level)
62
63
 
63
64
  def setup_langfuse() -> Langfuse:
64
65
  env_public_key = os.getenv("LANGFUSE_PUBLIC_KEY")
@@ -77,4 +78,6 @@ def setup_langfuse() -> Langfuse:
77
78
 
78
79
  return _langfuse
79
80
 
80
- langfuse = setup_langfuse()
81
+ if __name__ == "__main__":
82
+ langfuse = setup_langfuse()
83
+ logging.warning(f"langfuse is enable={langfuse.enabled}")
@@ -68,16 +68,16 @@ class XMLToolParser:
68
68
  # Find function_calls blocks
69
69
  function_calls_matches = self.FUNCTION_CALLS_PATTERN.findall(content)
70
70
 
71
- for fc_content in function_calls_matches:
71
+ for func_content in function_calls_matches:
72
72
  # Find all invoke blocks within this function_calls block
73
- invoke_matches = self.INVOKE_PATTERN.findall(fc_content)
73
+ invoke_matches = self.INVOKE_PATTERN.findall(func_content)
74
74
 
75
75
  for function_name, invoke_content in invoke_matches:
76
76
  try:
77
77
  tool_call = self._parse_invoke_block(
78
78
  function_name,
79
79
  invoke_content,
80
- fc_content
80
+ func_content
81
81
  )
82
82
  if tool_call:
83
83
  tool_calls.append(tool_call)
@@ -98,17 +98,11 @@ class XMLToolParser:
98
98
  "function_name": function_name,
99
99
  "raw_parameters": {}
100
100
  }
101
-
102
- # Extract all parameters
101
+
103
102
  param_matches = self.PARAMETER_PATTERN.findall(invoke_content)
104
-
105
103
  for param_name, param_value in param_matches:
106
- # Clean up the parameter value
107
104
  param_value = param_value.strip()
108
-
109
- # Try to parse as JSON if it looks like JSON
110
105
  parsed_value = self._parse_parameter_value(param_value)
111
-
112
106
  parameters[param_name] = parsed_value
113
107
  parsing_details["raw_parameters"][param_name] = param_value
114
108
 
@@ -161,73 +155,3 @@ class XMLToolParser:
161
155
 
162
156
  # Return as string
163
157
  return value
164
-
165
-
166
- def format_tool_call(self, function_name: str, parameters: Dict[str, Any]) -> str:
167
- """
168
- Format a tool call in the XML format.
169
-
170
- Args:
171
- function_name: Name of the function to call
172
- parameters: Dictionary of parameters
173
-
174
- Returns:
175
- Formatted XML string
176
- """
177
- lines = ['<function_calls>', '<invoke name="{}">'.format(function_name)]
178
-
179
- for param_name, param_value in parameters.items():
180
- # Convert value to string representation
181
- if isinstance(param_value, (dict, list)):
182
- value_str = json.dumps(param_value)
183
- elif isinstance(param_value, bool):
184
- value_str = str(param_value).lower()
185
- else:
186
- value_str = str(param_value)
187
-
188
- lines.append('<parameter name="{}">{}</parameter>'.format(
189
- param_name, value_str
190
- ))
191
-
192
- lines.extend(['</invoke>', '</function_calls>'])
193
- return '\n'.join(lines)
194
-
195
- def validate_tool_call(self, tool_call: XMLToolCall, expected_params: Optional[Dict[str, type]] = None) -> Tuple[bool, Optional[str]]:
196
- """
197
- Validate a tool call against expected parameters.
198
-
199
- Args:
200
- tool_call: The XMLToolCall to validate
201
- expected_params: Optional dict of parameter names to expected types
202
-
203
- Returns:
204
- Tuple of (is_valid, error_message)
205
- """
206
- if not tool_call.function_name:
207
- return False, "Function name is required"
208
-
209
- if expected_params:
210
- for param_name, expected_type in expected_params.items():
211
- if param_name not in tool_call.parameters:
212
- return False, f"Missing required parameter: {param_name}"
213
-
214
- param_value = tool_call.parameters[param_name]
215
- if not isinstance(param_value, expected_type):
216
- return False, f"Parameter {param_name} should be of type {expected_type.__name__}"
217
-
218
- return True, None
219
-
220
-
221
- # Convenience function for quick parsing
222
- def parse_xml_tool_calls(content: str) -> List[XMLToolCall]:
223
- """
224
- Parse XML tool calls from content.
225
-
226
- Args:
227
- content: The text content potentially containing XML tool calls
228
-
229
- Returns:
230
- List of parsed XMLToolCall objects
231
- """
232
- parser = XMLToolParser()
233
- return parser.parse_content(content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xgae
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Extreme General Agent Engine
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: colorlog==6.9.0
@@ -0,0 +1,20 @@
1
+ xgae/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ xgae/engine/engine_base.py,sha256=ioywuTpDMHEmyVcd6BInoU-vR70PhQStE2MVRWoEiJg,1768
3
+ xgae/engine/mcp_tool_box.py,sha256=ZSCBSXRWhISwyZ1uEIbt3esjesM46g-ktv6CxvyPVDU,10030
4
+ xgae/engine/prompt_builder.py,sha256=X9bS7YIms6LYplCpNHeUmi74xFP5MwFXmXNqOt1Xz-Q,4356
5
+ xgae/engine/task_engine.py,sha256=BLy32wJUbBFGkjAPfHcbSpCUPQm2uZAmLHtZVqvXwx0,18026
6
+ xgae/engine/task_langfuse.py,sha256=-WMnlMcBGfDuJpV0ZYKnVzlO0HizGOfFVGyzVkP6Rag,2260
7
+ xgae/engine/responser/non_stream_responser.py,sha256=80ZFcitjXpQJANxbYpQ0LlhVzIxmURNJi9Fbo953Uak,6349
8
+ xgae/engine/responser/responser_base.py,sha256=8PcsvQHP68FEhu6v3dT9hDCc_rLKs38i4txWLcJD4ck,29851
9
+ xgae/engine/responser/stream_responser.py,sha256=oPGtrT1nedGMjiBAwPzUlu6Z_rPWeVSODC1xQ6D8cTY,52055
10
+ xgae/tools/without_general_tools_app.py,sha256=FGMV6njcOKwwfitc0j_nUov0RC-eWlhO1IP8_KHz1tQ,3788
11
+ xgae/utils/__init__.py,sha256=tMSYguPrubusr7IyKZn6iPYE6qbN7sOsEVDeJPRdPa8,351
12
+ xgae/utils/json_helpers.py,sha256=6BkqiyEF3jV3Irb4Z6-wGY2_FNaLlxE1WKlMJHHT6E0,4645
13
+ xgae/utils/llm_client.py,sha256=nOhPlllUj4mASdj2dU5HkSmDqYZ8ZMpDa7AKQwUWJnQ,13600
14
+ xgae/utils/misc.py,sha256=M8lMXYp1pHiY6Ee8ZTUG88GpOAsE5fbYoRO_hcBFUCE,953
15
+ xgae/utils/setup_env.py,sha256=XD6WmGNyMFDKVzNU_ufYtqJNd2Otq4hzLs9YZ2aSz6g,2613
16
+ xgae/utils/xml_tool_parser.py,sha256=I9xAZC_ElwBY19PNUq-WLXe9FSIJMeAv2Xs-VlajI7Y,4782
17
+ xgae-0.1.9.dist-info/METADATA,sha256=q4_B-_Kt2hswi0kGX9B_H3uI_7C7SWiJOo6HnzoMF8g,309
18
+ xgae-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ xgae-0.1.9.dist-info/entry_points.txt,sha256=bYzQ-1iabCduosArMzq1hktJrCjp8jq4_E8Sg8-gEic,137
20
+ xgae-0.1.9.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ custom_fault_tools = examples.tools.custom_fault_tools_app:main
2
3
  xgae-tools = xgae.tools.without_general_tools_app:main
@@ -1,19 +0,0 @@
1
- xgae/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- xgae/engine/engine_base.py,sha256=MExJxXHkufv_a-n9tm3FZAbJyxVAwEkRC3C_WAGdu4Q,1746
3
- xgae/engine/mcp_tool_box.py,sha256=6bsORGK2HE-NS8vWNnbbgUQ9uGr_MI7JGO9j_FYKUKA,9903
4
- xgae/engine/prompt_builder.py,sha256=8_rNRJksn2QLV_K98S0x0qNeHcmxhU0kB_53IZJTGOU,4366
5
- xgae/engine/task_engine.py,sha256=kOntdzVtTjjakIhcBNK5vBMiBuAXPqB4R5ustY-BNfU,18931
6
- xgae/engine/responser/non_stream_responser.py,sha256=tXvkGHFGm2oO0IE8Bz9DFDwqemBkO92Nb6MbX1ukHeE,6352
7
- xgae/engine/responser/responser_base.py,sha256=FdR0yX7VfU5kW-4JmTXBfnn9AwyX1EbOAnslkC7Vcyg,29874
8
- xgae/engine/responser/stream_responser.py,sha256=oPGtrT1nedGMjiBAwPzUlu6Z_rPWeVSODC1xQ6D8cTY,52055
9
- xgae/tools/without_general_tools_app.py,sha256=QknIF4OW9xvOad8gx-F_sCBwQYXqMalnNFvYvZXkQ_I,3789
10
- xgae/utils/__init__.py,sha256=GPubcIs2XFPiWKnuCpevAtYEmVWKJuXlmGkmsH9qoXA,219
11
- xgae/utils/json_helpers.py,sha256=K1ja6GJCatrAheW9bEWAYSQbDI42__boBCZgtsv1gtk,4865
12
- xgae/utils/llm_client.py,sha256=Y-o26VW1MOhJYsWJ0zR4u_YXsHSEbvVPY6r90zLQJXU,13213
13
- xgae/utils/misc.py,sha256=EK94YesZp8AmRUqWfN-CjTxyEHPWdIIWpFNO17dzm9g,915
14
- xgae/utils/setup_env.py,sha256=P_p74q3nroBdTkAElfGr4QLm4fu7ZRP0R9BMGNUL010,2352
15
- xgae/utils/xml_tool_parser.py,sha256=EJ6BjpD4CSdmS_LqViUJ6P8H9GY2R1e4Dh8rLCR6nSE,7474
16
- xgae-0.1.8.dist-info/METADATA,sha256=mk0D1208wUJnbIZxyteCAjE2tlKtQixumHo0mcqD3RI,309
17
- xgae-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- xgae-0.1.8.dist-info/entry_points.txt,sha256=rhQ9Vksnu8nA78lPTjiJxOCZ5k6sH6s5YNMR68y7C-A,73
19
- xgae-0.1.8.dist-info/RECORD,,
File without changes