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

@@ -0,0 +1,174 @@
1
+ """
2
+ JSON helper utilities for handling both legacy (string) and new (dict/list) formats.
3
+
4
+ These utilities help with the transition from storing JSON as strings to storing
5
+ them as proper JSONB objects in the database.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Union, Dict, List
10
+
11
+
12
+ def ensure_dict(value: Union[str, Dict[str, Any], None], default: Dict[str, Any] = None) -> Dict[str, Any]:
13
+ """
14
+ Ensure a value is a dictionary.
15
+
16
+ Handles:
17
+ - None -> returns default or {}
18
+ - Dict -> returns as-is
19
+ - JSON string -> parses and returns dict
20
+ - Other -> returns default or {}
21
+
22
+ Args:
23
+ value: The value to ensure is a dict
24
+ default: Default value if conversion fails
25
+
26
+ Returns:
27
+ A dictionary
28
+ """
29
+ if default is None:
30
+ default = {}
31
+
32
+ if value is None:
33
+ return default
34
+
35
+ if isinstance(value, dict):
36
+ return value
37
+
38
+ if isinstance(value, str):
39
+ try:
40
+ parsed = json.loads(value)
41
+ if isinstance(parsed, dict):
42
+ return parsed
43
+ return default
44
+ except (json.JSONDecodeError, TypeError):
45
+ return default
46
+
47
+ return default
48
+
49
+
50
+ def ensure_list(value: Union[str, List[Any], None], default: List[Any] = None) -> List[Any]:
51
+ """
52
+ Ensure a value is a list.
53
+
54
+ Handles:
55
+ - None -> returns default or []
56
+ - List -> returns as-is
57
+ - JSON string -> parses and returns list
58
+ - Other -> returns default or []
59
+
60
+ Args:
61
+ value: The value to ensure is a list
62
+ default: Default value if conversion fails
63
+
64
+ Returns:
65
+ A list
66
+ """
67
+ if default is None:
68
+ default = []
69
+
70
+ if value is None:
71
+ return default
72
+
73
+ if isinstance(value, list):
74
+ return value
75
+
76
+ if isinstance(value, str):
77
+ try:
78
+ parsed = json.loads(value)
79
+ if isinstance(parsed, list):
80
+ return parsed
81
+ return default
82
+ except (json.JSONDecodeError, TypeError):
83
+ return default
84
+
85
+ return default
86
+
87
+
88
+ def safe_json_parse(value: Union[str, Dict, List, Any], default: Any = None) -> Any:
89
+ """
90
+ Safely parse a value that might be JSON string or already parsed.
91
+
92
+ This handles the transition period where some data might be stored as
93
+ JSON strings (old format) and some as proper objects (new format).
94
+
95
+ Args:
96
+ value: The value to parse
97
+ default: Default value if parsing fails
98
+
99
+ Returns:
100
+ Parsed value or default
101
+ """
102
+ if value is None:
103
+ return default
104
+
105
+ # If it's already a dict or list, return as-is
106
+ if isinstance(value, (dict, list)):
107
+ return value
108
+
109
+ # If it's a string, try to parse it
110
+ if isinstance(value, str):
111
+ try:
112
+ return json.loads(value)
113
+ except (json.JSONDecodeError, TypeError):
114
+ # If it's not valid JSON, return the string itself
115
+ return value
116
+
117
+ # For any other type, return as-is
118
+ return value
119
+
120
+
121
+ def to_json_string(value: Any) -> str:
122
+ """
123
+ Convert a value to a JSON string if needed.
124
+
125
+ This is used for backwards compatibility when yielding data that
126
+ expects JSON strings.
127
+
128
+ Args:
129
+ value: The value to convert
130
+
131
+ Returns:
132
+ JSON string representation
133
+ """
134
+ if isinstance(value, str):
135
+ # If it's already a string, check if it's valid JSON
136
+ try:
137
+ json.loads(value)
138
+ return value # It's already a JSON string
139
+ except (json.JSONDecodeError, TypeError):
140
+ # It's a plain string, encode it as JSON
141
+ return json.dumps(value)
142
+
143
+ # For all other types, convert to JSON
144
+ return json.dumps(value)
145
+
146
+
147
+ def format_for_yield(message_object: Dict[str, Any]) -> Dict[str, Any]:
148
+ """
149
+ Format a message object for yielding, ensuring content and metadata are JSON strings.
150
+
151
+ This maintains backward compatibility with clients expecting JSON strings
152
+ while the database now stores proper objects.
153
+
154
+ Args:
155
+ message_object: The message object from the database
156
+
157
+ Returns:
158
+ Message object with content and metadata as JSON strings
159
+ """
160
+ if not message_object:
161
+ return message_object
162
+
163
+ # Create a copy to avoid modifying the original
164
+ formatted = message_object.copy()
165
+
166
+ # Ensure content is a JSON string
167
+ if 'content' in formatted and not isinstance(formatted['content'], str):
168
+ formatted['content'] = json.dumps(formatted['content'])
169
+
170
+ # Ensure metadata is a JSON string
171
+ if 'metadata' in formatted and not isinstance(formatted['metadata'], str):
172
+ formatted['metadata'] = json.dumps(formatted['metadata'])
173
+
174
+ return formatted
xgae/utils/llm_client.py CHANGED
@@ -4,11 +4,24 @@ import logging
4
4
  import os
5
5
  import litellm
6
6
 
7
- from typing import Union, Dict, Any, Optional, List
7
+ from typing import Union, Dict, Any, Optional, List, TypedDict
8
8
 
9
9
  from litellm.utils import ModelResponse, CustomStreamWrapper
10
10
  from openai import OpenAIError
11
11
 
12
+ class LLMConfig(TypedDict, total=False):
13
+ model: str
14
+ model_name: str
15
+ model_id: str
16
+ api_key: str
17
+ api_base: str
18
+ temperature: float
19
+ max_tokens: int
20
+ stream: bool
21
+ enable_thinking: bool
22
+ reasoning_effort: str
23
+ response_format: str
24
+ top_p: int
12
25
 
13
26
  class LLMError(Exception):
14
27
  """Base exception for LLM-related errors."""
@@ -18,7 +31,7 @@ class LLMClient:
18
31
  RATE_LIMIT_DELAY = 30
19
32
  RETRY_DELAY = 0.1
20
33
 
21
- def __init__(self, llm_config: Optional[Dict[str, Any]]=None) -> None:
34
+ def __init__(self, llm_config: LLMConfig=None) -> None:
22
35
  """
23
36
  Arg: llm_config (Optional[Dict[str, Any]], optional)
24
37
  model: Override default model to use, default set by .env LLM_MODEL
@@ -34,7 +47,7 @@ class LLMClient:
34
47
  reasoning_effort: Optional level of reasoning effort, default is ‘low’
35
48
  top_p: Optional Top-p sampling parameter, default is None
36
49
  """
37
- llm_config = llm_config or {}
50
+ llm_config = llm_config or LLMConfig()
38
51
  litellm.modify_params = True
39
52
  litellm.drop_params = True
40
53
 
@@ -214,9 +227,7 @@ class LLMClient:
214
227
 
215
228
  if __name__ == "__main__":
216
229
  async def llm_completion():
217
- llm_client = LLMClient({
218
- "stream": False #default is True
219
- })
230
+ llm_client = LLMClient(LLMConfig(stream=False))
220
231
  messages = [{"role": "user", "content": "今天是2025年8月15日,北京本周每天温度"}]
221
232
  response = await llm_client.create_completion(messages)
222
233
  if llm_client.is_stream:
xgae/utils/setup_env.py CHANGED
@@ -1,10 +1,8 @@
1
1
  import logging
2
2
  import os
3
- import traceback
4
3
 
5
4
  from langfuse import Langfuse
6
5
 
7
-
8
6
  _log_initialized = False
9
7
 
10
8
  def setup_logging() -> None:
@@ -65,17 +63,6 @@ def setup_logging() -> None:
65
63
 
66
64
  setup_logging()
67
65
 
68
-
69
- class XGAError(Exception):
70
- """Custom exception for errors in the XGA system."""
71
- pass
72
-
73
- def handle_error(e: Exception) -> None:
74
- logging.error("An error occurred: %s", str(e))
75
- logging.error("Traceback details:\n%s", traceback.format_exc())
76
- raise (e) from e
77
-
78
-
79
66
  _langfuse_initialized = False
80
67
 
81
68
  def setup_langfuse() -> Langfuse:
@@ -101,23 +88,6 @@ def setup_langfuse() -> Langfuse:
101
88
  langfuse: Langfuse = Langfuse if _langfuse_initialized else setup_langfuse()
102
89
 
103
90
 
104
- def read_file(file_path: str) -> str:
105
- if not os.path.exists(file_path):
106
- logging.error(f"File '{file_path}' not found")
107
- raise XGAError(f"File '{file_path}' not found")
108
-
109
- try:
110
- with open(file_path, "r", encoding="utf-8") as template_file:
111
- content = template_file.read()
112
- return content
113
- except Exception as e:
114
- logging.error(f"Read file '{file_path}' failed")
115
- handle_error(e)
116
-
117
-
118
91
  if __name__ == "__main__":
119
- try:
120
92
  trace_id = langfuse.create_trace_id()
121
- print(f"trace_id={trace_id}")
122
- except Exception as e:
123
- handle_error(e)
93
+ logging.warning(f"trace_id={trace_id}")
xgae/utils/utils.py ADDED
@@ -0,0 +1,42 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import datetime
5
+
6
+ from typing import Any, Dict
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
14
+
15
+ def read_file(file_path: str) -> str:
16
+ if not os.path.exists(file_path):
17
+ logging.error(f"File '{file_path}' not found")
18
+ raise Exception(f"File '{file_path}' not found")
19
+
20
+ try:
21
+ with open(file_path, "r", encoding="utf-8") as template_file:
22
+ content = template_file.read()
23
+ return content
24
+ except Exception as e:
25
+ logging.error(f"Read file '{file_path}' failed")
26
+ handle_error(e)
27
+
28
+ def format_file_with_args(file_content:str, args: Dict[str, Any])-> str:
29
+ from io import StringIO
30
+
31
+ formated = file_content
32
+ original_stdout = sys.stdout
33
+ buffer = StringIO()
34
+ sys.stdout = buffer
35
+ try:
36
+ code = f"print(f\"\"\"{file_content}\"\"\")"
37
+ exec(code, args)
38
+ formated = buffer.getvalue()
39
+ finally:
40
+ sys.stdout = original_stdout
41
+
42
+ return formated
@@ -0,0 +1,236 @@
1
+ """
2
+ XML Tool Call Parser Module
3
+
4
+ This module provides a reliable XML tool call parsing system that supports
5
+ the XML format with structured function_calls blocks.
6
+ """
7
+
8
+ import re
9
+ import xml.etree.ElementTree as ET
10
+ from typing import List, Dict, Any, Optional, Tuple
11
+ from dataclasses import dataclass
12
+ import json
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class XMLToolCall:
20
+ """Represents a parsed XML tool call."""
21
+ function_name: str
22
+ parameters: Dict[str, Any]
23
+ raw_xml: str
24
+ parsing_details: Dict[str, Any]
25
+
26
+
27
+ class XMLToolParser:
28
+ """
29
+ Parser for XML tool calls format:
30
+
31
+ <function_calls>
32
+ <invoke name="function_name">
33
+ <parameter name="param_name">param_value</parameter>
34
+ ...
35
+ </invoke>
36
+ </function_calls>
37
+ """
38
+
39
+ # Regex patterns for extracting XML blocks
40
+ FUNCTION_CALLS_PATTERN = re.compile(
41
+ r'<function_calls>(.*?)</function_calls>',
42
+ re.DOTALL | re.IGNORECASE
43
+ )
44
+
45
+ INVOKE_PATTERN = re.compile(
46
+ r'<invoke\s+name=["\']([^"\']+)["\']>(.*?)</invoke>',
47
+ re.DOTALL | re.IGNORECASE
48
+ )
49
+
50
+ PARAMETER_PATTERN = re.compile(
51
+ r'<parameter\s+name=["\']([^"\']+)["\']>(.*?)</parameter>',
52
+ re.DOTALL | re.IGNORECASE
53
+ )
54
+
55
+ def __init__(self):
56
+ """Initialize the XML tool parser."""
57
+ pass
58
+
59
+ def parse_content(self, content: str) -> List[XMLToolCall]:
60
+ """
61
+ Parse XML tool calls from content.
62
+
63
+ Args:
64
+ content: The text content potentially containing XML tool calls
65
+
66
+ Returns:
67
+ List of parsed XMLToolCall objects
68
+ """
69
+ tool_calls = []
70
+
71
+ # Find function_calls blocks
72
+ function_calls_matches = self.FUNCTION_CALLS_PATTERN.findall(content)
73
+
74
+ for fc_content in function_calls_matches:
75
+ # Find all invoke blocks within this function_calls block
76
+ invoke_matches = self.INVOKE_PATTERN.findall(fc_content)
77
+
78
+ for function_name, invoke_content in invoke_matches:
79
+ try:
80
+ tool_call = self._parse_invoke_block(
81
+ function_name,
82
+ invoke_content,
83
+ fc_content
84
+ )
85
+ if tool_call:
86
+ tool_calls.append(tool_call)
87
+ except Exception as e:
88
+ logger.error(f"Error parsing invoke block for {function_name}: {e}")
89
+
90
+ return tool_calls
91
+
92
+ def _parse_invoke_block(
93
+ self,
94
+ function_name: str,
95
+ invoke_content: str,
96
+ full_block: str
97
+ ) -> Optional[XMLToolCall]:
98
+ """Parse a single invoke block into an XMLToolCall."""
99
+ parameters = {}
100
+ parsing_details = {
101
+ "function_name": function_name,
102
+ "raw_parameters": {}
103
+ }
104
+
105
+ # Extract all parameters
106
+ param_matches = self.PARAMETER_PATTERN.findall(invoke_content)
107
+
108
+ for param_name, param_value in param_matches:
109
+ # Clean up the parameter value
110
+ param_value = param_value.strip()
111
+
112
+ # Try to parse as JSON if it looks like JSON
113
+ parsed_value = self._parse_parameter_value(param_value)
114
+
115
+ parameters[param_name] = parsed_value
116
+ parsing_details["raw_parameters"][param_name] = param_value
117
+
118
+ # Extract the raw XML for this specific invoke
119
+ invoke_pattern = re.compile(
120
+ rf'<invoke\s+name=["\']{re.escape(function_name)}["\']>.*?</invoke>',
121
+ re.DOTALL | re.IGNORECASE
122
+ )
123
+ raw_xml_match = invoke_pattern.search(full_block)
124
+ raw_xml = raw_xml_match.group(0) if raw_xml_match else f"<invoke name=\"{function_name}\">...</invoke>"
125
+
126
+ return XMLToolCall(
127
+ function_name=function_name,
128
+ parameters=parameters,
129
+ raw_xml=raw_xml,
130
+ parsing_details=parsing_details
131
+ )
132
+
133
+ def _parse_parameter_value(self, value: str) -> Any:
134
+ """
135
+ Parse a parameter value, attempting to convert to appropriate type.
136
+
137
+ Args:
138
+ value: The string value to parse
139
+
140
+ Returns:
141
+ Parsed value (could be dict, list, bool, int, float, or str)
142
+ """
143
+ value = value.strip()
144
+
145
+ # Try to parse as JSON first
146
+ if value.startswith(('{', '[')):
147
+ try:
148
+ return json.loads(value)
149
+ except json.JSONDecodeError:
150
+ pass
151
+
152
+ # Try to parse as boolean
153
+ if value.lower() in ('true', 'false'):
154
+ return value.lower() == 'true'
155
+
156
+ # Try to parse as number
157
+ try:
158
+ if '.' in value:
159
+ return float(value)
160
+ else:
161
+ return int(value)
162
+ except ValueError:
163
+ pass
164
+
165
+ # Return as string
166
+ return value
167
+
168
+
169
+ def format_tool_call(self, function_name: str, parameters: Dict[str, Any]) -> str:
170
+ """
171
+ Format a tool call in the XML format.
172
+
173
+ Args:
174
+ function_name: Name of the function to call
175
+ parameters: Dictionary of parameters
176
+
177
+ Returns:
178
+ Formatted XML string
179
+ """
180
+ lines = ['<function_calls>', '<invoke name="{}">'.format(function_name)]
181
+
182
+ for param_name, param_value in parameters.items():
183
+ # Convert value to string representation
184
+ if isinstance(param_value, (dict, list)):
185
+ value_str = json.dumps(param_value)
186
+ elif isinstance(param_value, bool):
187
+ value_str = str(param_value).lower()
188
+ else:
189
+ value_str = str(param_value)
190
+
191
+ lines.append('<parameter name="{}">{}</parameter>'.format(
192
+ param_name, value_str
193
+ ))
194
+
195
+ lines.extend(['</invoke>', '</function_calls>'])
196
+ return '\n'.join(lines)
197
+
198
+ def validate_tool_call(self, tool_call: XMLToolCall, expected_params: Optional[Dict[str, type]] = None) -> Tuple[bool, Optional[str]]:
199
+ """
200
+ Validate a tool call against expected parameters.
201
+
202
+ Args:
203
+ tool_call: The XMLToolCall to validate
204
+ expected_params: Optional dict of parameter names to expected types
205
+
206
+ Returns:
207
+ Tuple of (is_valid, error_message)
208
+ """
209
+ if not tool_call.function_name:
210
+ return False, "Function name is required"
211
+
212
+ if expected_params:
213
+ for param_name, expected_type in expected_params.items():
214
+ if param_name not in tool_call.parameters:
215
+ return False, f"Missing required parameter: {param_name}"
216
+
217
+ param_value = tool_call.parameters[param_name]
218
+ if not isinstance(param_value, expected_type):
219
+ return False, f"Parameter {param_name} should be of type {expected_type.__name__}"
220
+
221
+ return True, None
222
+
223
+
224
+ # Convenience function for quick parsing
225
+ def parse_xml_tool_calls(content: str) -> List[XMLToolCall]:
226
+ """
227
+ Parse XML tool calls from content.
228
+
229
+ Args:
230
+ content: The text content potentially containing XML tool calls
231
+
232
+ Returns:
233
+ List of parsed XMLToolCall objects
234
+ """
235
+ parser = XMLToolParser()
236
+ return parser.parse_content(content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xgae
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Extreme General Agent Engine
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: colorlog>=6.9.0
@@ -0,0 +1,16 @@
1
+ xgae/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ xgae/engine/xga_base.py,sha256=yM8YdThOVHo_TFrLUiu33hZzU2H9knQLs3JfD6hOx24,1669
3
+ xgae/engine/xga_engine.py,sha256=7XRRC6KKz3a2EvH7P9k8szbCCm2O6MxIL674GzUkwHI,12940
4
+ xgae/engine/xga_mcp_tool_box.py,sha256=dvToorrw8FJq4NrzUz494czI6QhT3bvmJQlt5oFgVBA,9994
5
+ xgae/engine/xga_prompt_builder.py,sha256=RuTvQCNufqxDwVvSOPXR0qxAc42cG7NuIaUy9amu66A,4351
6
+ xgae/engine/responser/xga_non_stream_responser.py,sha256=HlSN025jIsl-JY_n6fEdqJQkqc1UqCrgFr6K23uXF3E,12704
7
+ xgae/engine/responser/xga_responser_base.py,sha256=IBJPQELMxZSpnz8YlSCgvPNSHEEUp8_vglotVnHoSeY,36808
8
+ xgae/engine/responser/xga_stream_responser.py,sha256=FESVliTzHFy8BkTudi_Ftcty6QFpJWx7kYRubSuLqsg,50370
9
+ xgae/utils/json_helpers.py,sha256=K1ja6GJCatrAheW9bEWAYSQbDI42__boBCZgtsv1gtk,4865
10
+ xgae/utils/llm_client.py,sha256=mgzn8heUyRm92HTLEYGdfsGEpFtD-xLFr39P98_JP0s,12402
11
+ xgae/utils/setup_env.py,sha256=-Ehv7_E9udHc8AjP66Y78E4X7_G6gpuNJkioCh5fn4A,2902
12
+ xgae/utils/utils.py,sha256=cCYmWjKFksZ8BRD1YYnaM_jTLVHAg1ibEdjsczEUO6k,1134
13
+ xgae/utils/xml_tool_parser.py,sha256=7Ei7X8zSgVct0fFCSmxDtknCLtdrUIwL9hy_0qSNlvs,7546
14
+ xgae-0.1.4.dist-info/METADATA,sha256=R_nN1j5mESZfEQIiLFBQufsOc_Hbd6190GBLgo7-k-o,309
15
+ xgae-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ xgae-0.1.4.dist-info/RECORD,,
File without changes
File without changes
@@ -1,13 +0,0 @@
1
- xgae/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- xgae/engine/xga_base.py,sha256=vOiYWoBze7VrmndYnaNhMzoB1zCT80WtIlOWdp2IrUc,1253
3
- xgae/engine/xga_engine.py,sha256=AgklNAimyErvn74xUu6aardZvgjqbIR99gCojEmZfO0,4267
4
- xgae/engine/xga_mcp_tool_box.py,sha256=jRYvZ8eY72KRU0leD-o15fe_E8wjWJtxQZbsIqGVjPU,9753
5
- xgae/engine/xga_prompt_builder.py,sha256=Q3nrmYAofRkFQFdfSWkP00FZKjWMUQs2cvMs7bvyQgQ,4819
6
- xgae/engine/responser/xga_non_stream_responser.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- xgae/engine/responser/xga_responser_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- xgae/engine/responser/xga_stream_reponser.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- xgae/utils/llm_client.py,sha256=UMMK84psLsx36-Nn6Q8X1hl9wd-OdaS9ZhxbRjwNCr0,12149
10
- xgae/utils/setup_env.py,sha256=qZmVfqaYHtjq5t-zwRt_G3i-QqGPxSb4D2FnFHbhdcY,3689
11
- xgae-0.1.2.dist-info/METADATA,sha256=iBwWKlpbp7z0ZQgydnieP_2NBf0Z7H0D6AtavbIN3Zc,309
12
- xgae-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- xgae-0.1.2.dist-info/RECORD,,
File without changes