mem-llm 1.0.2__py3-none-any.whl → 2.1.0__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 mem-llm might be problematic. Click here for more details.
- mem_llm/__init__.py +71 -8
- mem_llm/api_server.py +595 -0
- mem_llm/base_llm_client.py +201 -0
- mem_llm/builtin_tools.py +311 -0
- mem_llm/builtin_tools_async.py +170 -0
- mem_llm/cli.py +254 -0
- mem_llm/clients/__init__.py +22 -0
- mem_llm/clients/lmstudio_client.py +393 -0
- mem_llm/clients/ollama_client.py +354 -0
- mem_llm/config.yaml.example +1 -1
- mem_llm/config_from_docs.py +1 -1
- mem_llm/config_manager.py +5 -3
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/dynamic_prompt.py +298 -0
- mem_llm/llm_client.py +77 -14
- mem_llm/llm_client_factory.py +260 -0
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +1178 -87
- mem_llm/memory_db.py +290 -59
- mem_llm/memory_manager.py +60 -1
- mem_llm/prompt_security.py +304 -0
- mem_llm/response_metrics.py +221 -0
- mem_llm/retry_handler.py +193 -0
- mem_llm/thread_safe_db.py +301 -0
- mem_llm/tool_system.py +537 -0
- mem_llm/vector_store.py +278 -0
- mem_llm/web_launcher.py +129 -0
- mem_llm/web_ui/README.md +44 -0
- mem_llm/web_ui/__init__.py +7 -0
- mem_llm/web_ui/index.html +641 -0
- mem_llm/web_ui/memory.html +569 -0
- mem_llm/web_ui/metrics.html +75 -0
- mem_llm-2.1.0.dist-info/METADATA +753 -0
- mem_llm-2.1.0.dist-info/RECORD +40 -0
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/WHEEL +1 -1
- mem_llm-2.1.0.dist-info/entry_points.txt +3 -0
- mem_llm/prompt_templates.py +0 -244
- mem_llm-1.0.2.dist-info/METADATA +0 -382
- mem_llm-1.0.2.dist-info/RECORD +0 -15
- {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/top_level.txt +0 -0
mem_llm/tool_system.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool System for Function Calling
|
|
3
|
+
=================================
|
|
4
|
+
|
|
5
|
+
Enables agents to call external functions/tools to perform actions.
|
|
6
|
+
Inspired by OpenAI's function calling and LangChain's tool system.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Decorator-based tool definition
|
|
10
|
+
- Automatic schema generation from type hints
|
|
11
|
+
- Tool execution with error handling
|
|
12
|
+
- Tool result formatting
|
|
13
|
+
- Built-in common tools
|
|
14
|
+
|
|
15
|
+
Author: C. Emre Karataş
|
|
16
|
+
Version: 2.1.0
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import inspect
|
|
21
|
+
import re
|
|
22
|
+
import asyncio
|
|
23
|
+
from typing import Callable, Dict, List, Any, Optional, get_type_hints, Union
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from enum import Enum
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolCallStatus(Enum):
|
|
32
|
+
"""Status of tool call execution"""
|
|
33
|
+
SUCCESS = "success"
|
|
34
|
+
ERROR = "error"
|
|
35
|
+
NOT_FOUND = "not_found"
|
|
36
|
+
INVALID_ARGS = "invalid_args"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ToolParameter:
|
|
41
|
+
"""Tool parameter definition with validation (v2.1.0+)"""
|
|
42
|
+
name: str
|
|
43
|
+
type: str
|
|
44
|
+
description: str
|
|
45
|
+
required: bool = True
|
|
46
|
+
default: Any = None
|
|
47
|
+
# Validation (v2.1.0+)
|
|
48
|
+
pattern: Optional[str] = None # Regex pattern for strings
|
|
49
|
+
min_value: Optional[Union[int, float]] = None # Minimum value for numbers
|
|
50
|
+
max_value: Optional[Union[int, float]] = None # Maximum value for numbers
|
|
51
|
+
min_length: Optional[int] = None # Minimum length for strings/lists
|
|
52
|
+
max_length: Optional[int] = None # Maximum length for strings/lists
|
|
53
|
+
choices: Optional[List[Any]] = None # Allowed values
|
|
54
|
+
validator: Optional[Callable[[Any], bool]] = None # Custom validator function
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Tool:
|
|
59
|
+
"""Tool definition with async support (v2.1.0+)"""
|
|
60
|
+
name: str
|
|
61
|
+
description: str
|
|
62
|
+
function: Callable
|
|
63
|
+
parameters: List[ToolParameter] = field(default_factory=list)
|
|
64
|
+
return_type: str = "string"
|
|
65
|
+
category: str = "general"
|
|
66
|
+
is_async: bool = field(default=False, init=False)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
"""Detect if function is async"""
|
|
70
|
+
self.is_async = asyncio.iscoroutinefunction(self.function)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
"""Convert tool to dictionary format for LLM"""
|
|
74
|
+
return {
|
|
75
|
+
"name": self.name,
|
|
76
|
+
"description": self.description,
|
|
77
|
+
"category": self.category,
|
|
78
|
+
"parameters": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
param.name: {
|
|
82
|
+
"type": param.type,
|
|
83
|
+
"description": param.description
|
|
84
|
+
}
|
|
85
|
+
for param in self.parameters
|
|
86
|
+
},
|
|
87
|
+
"required": [p.name for p in self.parameters if p.required]
|
|
88
|
+
},
|
|
89
|
+
"return_type": self.return_type
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def validate_arguments(self, **kwargs) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Validate tool arguments with comprehensive validation (v2.1.0+)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Validated arguments or raises ValueError
|
|
98
|
+
"""
|
|
99
|
+
validated = {}
|
|
100
|
+
|
|
101
|
+
for param in self.parameters:
|
|
102
|
+
value = kwargs.get(param.name, param.default)
|
|
103
|
+
|
|
104
|
+
# Check required
|
|
105
|
+
if param.required and value is None:
|
|
106
|
+
raise ValueError(f"Missing required parameter: {param.name}")
|
|
107
|
+
|
|
108
|
+
if value is None:
|
|
109
|
+
validated[param.name] = value
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Type validation
|
|
113
|
+
param_type = param.type.lower()
|
|
114
|
+
|
|
115
|
+
# String validations
|
|
116
|
+
if param_type == "string" and isinstance(value, str):
|
|
117
|
+
if param.pattern and not re.match(param.pattern, value):
|
|
118
|
+
raise ValueError(f"Parameter '{param.name}' does not match pattern: {param.pattern}")
|
|
119
|
+
if param.min_length and len(value) < param.min_length:
|
|
120
|
+
raise ValueError(f"Parameter '{param.name}' too short (min: {param.min_length})")
|
|
121
|
+
if param.max_length and len(value) > param.max_length:
|
|
122
|
+
raise ValueError(f"Parameter '{param.name}' too long (max: {param.max_length})")
|
|
123
|
+
|
|
124
|
+
# Number validations
|
|
125
|
+
if param_type in ["number", "integer"] and isinstance(value, (int, float)):
|
|
126
|
+
if param.min_value is not None and value < param.min_value:
|
|
127
|
+
raise ValueError(f"Parameter '{param.name}' too small (min: {param.min_value})")
|
|
128
|
+
if param.max_value is not None and value > param.max_value:
|
|
129
|
+
raise ValueError(f"Parameter '{param.name}' too large (max: {param.max_value})")
|
|
130
|
+
|
|
131
|
+
# Choices validation
|
|
132
|
+
if param.choices and value not in param.choices:
|
|
133
|
+
raise ValueError(f"Parameter '{param.name}' must be one of: {param.choices}")
|
|
134
|
+
|
|
135
|
+
# Custom validator
|
|
136
|
+
if param.validator and not param.validator(value):
|
|
137
|
+
raise ValueError(f"Parameter '{param.name}' failed custom validation")
|
|
138
|
+
|
|
139
|
+
validated[param.name] = value
|
|
140
|
+
|
|
141
|
+
return validated
|
|
142
|
+
|
|
143
|
+
def execute(self, **kwargs) -> Any:
|
|
144
|
+
"""
|
|
145
|
+
Execute the tool with arguments (supports async v2.1.0+)
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
# Validate arguments (v2.1.0+)
|
|
149
|
+
validated_kwargs = self.validate_arguments(**kwargs)
|
|
150
|
+
|
|
151
|
+
# Execute function
|
|
152
|
+
if self.is_async:
|
|
153
|
+
# Run async function
|
|
154
|
+
try:
|
|
155
|
+
loop = asyncio.get_running_loop()
|
|
156
|
+
# Already in async context, create task
|
|
157
|
+
return asyncio.create_task(self.function(**validated_kwargs))
|
|
158
|
+
except RuntimeError:
|
|
159
|
+
# No running loop, run in new loop
|
|
160
|
+
return asyncio.run(self.function(**validated_kwargs))
|
|
161
|
+
else:
|
|
162
|
+
# Sync function
|
|
163
|
+
return self.function(**validated_kwargs)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"Tool execution error ({self.name}): {e}")
|
|
166
|
+
raise
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class ToolCallResult:
|
|
171
|
+
"""Result of a tool call"""
|
|
172
|
+
tool_name: str
|
|
173
|
+
status: ToolCallStatus
|
|
174
|
+
result: Any = None
|
|
175
|
+
error: Optional[str] = None
|
|
176
|
+
execution_time: float = 0.0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def tool(
|
|
180
|
+
name: Optional[str] = None,
|
|
181
|
+
description: Optional[str] = None,
|
|
182
|
+
category: str = "general",
|
|
183
|
+
# Validation parameters (v2.1.0+)
|
|
184
|
+
pattern: Optional[Dict[str, str]] = None, # {param_name: regex_pattern}
|
|
185
|
+
min_value: Optional[Dict[str, Union[int, float]]] = None, # {param_name: min}
|
|
186
|
+
max_value: Optional[Dict[str, Union[int, float]]] = None, # {param_name: max}
|
|
187
|
+
min_length: Optional[Dict[str, int]] = None, # {param_name: min_len}
|
|
188
|
+
max_length: Optional[Dict[str, int]] = None, # {param_name: max_len}
|
|
189
|
+
choices: Optional[Dict[str, List[Any]]] = None, # {param_name: [valid_values]}
|
|
190
|
+
validators: Optional[Dict[str, Callable]] = None, # {param_name: validator_func}
|
|
191
|
+
) -> Callable:
|
|
192
|
+
"""
|
|
193
|
+
Decorator to define a tool/function that the agent can call.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
name: Tool name (defaults to function name)
|
|
197
|
+
description: Tool description (defaults to docstring)
|
|
198
|
+
category: Tool category for organization
|
|
199
|
+
pattern: Regex patterns for string parameters (v2.1.0+)
|
|
200
|
+
min_value: Minimum values for number parameters (v2.1.0+)
|
|
201
|
+
max_value: Maximum values for number parameters (v2.1.0+)
|
|
202
|
+
min_length: Minimum length for string/list parameters (v2.1.0+)
|
|
203
|
+
max_length: Maximum length for string/list parameters (v2.1.0+)
|
|
204
|
+
choices: Allowed values for parameters (v2.1.0+)
|
|
205
|
+
validators: Custom validator functions (v2.1.0+)
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
@tool(name="calculate", description="Perform mathematical calculations")
|
|
209
|
+
def calculator(expression: str) -> float:
|
|
210
|
+
'''Evaluate a mathematical expression'''
|
|
211
|
+
return eval(expression)
|
|
212
|
+
|
|
213
|
+
# With validation (v2.1.0+):
|
|
214
|
+
@tool(
|
|
215
|
+
name="validate_email",
|
|
216
|
+
pattern={"email": r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'},
|
|
217
|
+
min_length={"email": 5},
|
|
218
|
+
max_length={"email": 254}
|
|
219
|
+
)
|
|
220
|
+
def send_email(email: str) -> str:
|
|
221
|
+
return f"Email sent to {email}"
|
|
222
|
+
"""
|
|
223
|
+
def decorator(func: Callable) -> Tool:
|
|
224
|
+
# Get function metadata
|
|
225
|
+
func_name = name or func.__name__
|
|
226
|
+
func_desc = description or (func.__doc__ or "").strip()
|
|
227
|
+
|
|
228
|
+
# Extract parameters from type hints
|
|
229
|
+
type_hints = get_type_hints(func)
|
|
230
|
+
sig = inspect.signature(func)
|
|
231
|
+
parameters = []
|
|
232
|
+
|
|
233
|
+
for param_name, param in sig.parameters.items():
|
|
234
|
+
if param_name in type_hints:
|
|
235
|
+
param_type = type_hints[param_name]
|
|
236
|
+
# Map Python types to JSON schema types
|
|
237
|
+
type_map = {
|
|
238
|
+
str: "string",
|
|
239
|
+
int: "integer",
|
|
240
|
+
float: "number",
|
|
241
|
+
bool: "boolean",
|
|
242
|
+
list: "array",
|
|
243
|
+
dict: "object"
|
|
244
|
+
}
|
|
245
|
+
json_type = type_map.get(param_type, "string")
|
|
246
|
+
else:
|
|
247
|
+
json_type = "string"
|
|
248
|
+
|
|
249
|
+
param_desc = f"Parameter: {param_name}"
|
|
250
|
+
required = param.default == inspect.Parameter.empty
|
|
251
|
+
|
|
252
|
+
# Add validation parameters (v2.1.0+)
|
|
253
|
+
param_obj = ToolParameter(
|
|
254
|
+
name=param_name,
|
|
255
|
+
type=json_type,
|
|
256
|
+
description=param_desc,
|
|
257
|
+
required=required,
|
|
258
|
+
default=param.default if param.default != inspect.Parameter.empty else None,
|
|
259
|
+
pattern=pattern.get(param_name) if pattern else None,
|
|
260
|
+
min_value=min_value.get(param_name) if min_value else None,
|
|
261
|
+
max_value=max_value.get(param_name) if max_value else None,
|
|
262
|
+
min_length=min_length.get(param_name) if min_length else None,
|
|
263
|
+
max_length=max_length.get(param_name) if max_length else None,
|
|
264
|
+
choices=choices.get(param_name) if choices else None,
|
|
265
|
+
validator=validators.get(param_name) if validators else None
|
|
266
|
+
)
|
|
267
|
+
parameters.append(param_obj)
|
|
268
|
+
|
|
269
|
+
# Get return type
|
|
270
|
+
return_type = "string"
|
|
271
|
+
if "return" in type_hints:
|
|
272
|
+
ret_type = type_hints["return"]
|
|
273
|
+
type_map = {str: "string", int: "integer", float: "number", bool: "boolean"}
|
|
274
|
+
return_type = type_map.get(ret_type, "string")
|
|
275
|
+
|
|
276
|
+
# Create Tool object
|
|
277
|
+
tool_obj = Tool(
|
|
278
|
+
name=func_name,
|
|
279
|
+
description=func_desc,
|
|
280
|
+
function=func,
|
|
281
|
+
parameters=parameters,
|
|
282
|
+
return_type=return_type,
|
|
283
|
+
category=category
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Attach tool metadata to function
|
|
287
|
+
func._tool = tool_obj
|
|
288
|
+
return func
|
|
289
|
+
|
|
290
|
+
return decorator
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ToolRegistry:
|
|
294
|
+
"""Registry for managing available tools"""
|
|
295
|
+
|
|
296
|
+
def __init__(self):
|
|
297
|
+
self.tools: Dict[str, Tool] = {}
|
|
298
|
+
self._load_builtin_tools()
|
|
299
|
+
|
|
300
|
+
def _load_builtin_tools(self):
|
|
301
|
+
"""Load built-in tools"""
|
|
302
|
+
# Import built-in tools when available
|
|
303
|
+
try:
|
|
304
|
+
from .builtin_tools import BUILTIN_TOOLS
|
|
305
|
+
for tool_func in BUILTIN_TOOLS:
|
|
306
|
+
if hasattr(tool_func, '_tool'):
|
|
307
|
+
self.register(tool_func._tool)
|
|
308
|
+
except ImportError:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
def register(self, tool: Tool):
|
|
312
|
+
"""Register a tool"""
|
|
313
|
+
self.tools[tool.name] = tool
|
|
314
|
+
logger.info(f"Registered tool: {tool.name}")
|
|
315
|
+
|
|
316
|
+
def register_function(self, func: Callable):
|
|
317
|
+
"""Register a function as a tool"""
|
|
318
|
+
if hasattr(func, '_tool'):
|
|
319
|
+
self.register(func._tool)
|
|
320
|
+
else:
|
|
321
|
+
# Auto-create tool from function
|
|
322
|
+
tool_obj = tool()(func)
|
|
323
|
+
if hasattr(tool_obj, '_tool'):
|
|
324
|
+
self.register(tool_obj._tool)
|
|
325
|
+
|
|
326
|
+
def get(self, name: str) -> Optional[Tool]:
|
|
327
|
+
"""Get a tool by name"""
|
|
328
|
+
return self.tools.get(name)
|
|
329
|
+
|
|
330
|
+
def list_tools(self, category: Optional[str] = None) -> List[Tool]:
|
|
331
|
+
"""List all tools, optionally filtered by category"""
|
|
332
|
+
tools = list(self.tools.values())
|
|
333
|
+
if category:
|
|
334
|
+
tools = [t for t in tools if t.category == category]
|
|
335
|
+
return tools
|
|
336
|
+
|
|
337
|
+
def get_tools_schema(self) -> List[Dict[str, Any]]:
|
|
338
|
+
"""Get schema for all tools (for LLM prompt)"""
|
|
339
|
+
return [tool.to_dict() for tool in self.tools.values()]
|
|
340
|
+
|
|
341
|
+
def execute(self, tool_name: str, **kwargs) -> ToolCallResult:
|
|
342
|
+
"""Execute a tool by name"""
|
|
343
|
+
import time
|
|
344
|
+
start_time = time.time()
|
|
345
|
+
|
|
346
|
+
# Get tool
|
|
347
|
+
tool = self.get(tool_name)
|
|
348
|
+
if not tool:
|
|
349
|
+
return ToolCallResult(
|
|
350
|
+
tool_name=tool_name,
|
|
351
|
+
status=ToolCallStatus.NOT_FOUND,
|
|
352
|
+
error=f"Tool '{tool_name}' not found",
|
|
353
|
+
execution_time=time.time() - start_time
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Execute tool
|
|
357
|
+
try:
|
|
358
|
+
result = tool.execute(**kwargs)
|
|
359
|
+
return ToolCallResult(
|
|
360
|
+
tool_name=tool_name,
|
|
361
|
+
status=ToolCallStatus.SUCCESS,
|
|
362
|
+
result=result,
|
|
363
|
+
execution_time=time.time() - start_time
|
|
364
|
+
)
|
|
365
|
+
except ValueError as e:
|
|
366
|
+
return ToolCallResult(
|
|
367
|
+
tool_name=tool_name,
|
|
368
|
+
status=ToolCallStatus.INVALID_ARGS,
|
|
369
|
+
error=str(e),
|
|
370
|
+
execution_time=time.time() - start_time
|
|
371
|
+
)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
return ToolCallResult(
|
|
374
|
+
tool_name=tool_name,
|
|
375
|
+
status=ToolCallStatus.ERROR,
|
|
376
|
+
error=str(e),
|
|
377
|
+
execution_time=time.time() - start_time
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ToolCallParser:
|
|
382
|
+
"""Parse LLM output to detect and extract tool calls"""
|
|
383
|
+
|
|
384
|
+
# Pattern to detect tool calls in LLM output
|
|
385
|
+
# Format: TOOL_CALL: tool_name(arg1="value1", arg2="value2")
|
|
386
|
+
TOOL_CALL_PATTERN = r'TOOL_CALL:\s*(\w+)\((.*?)\)'
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def parse(text: str) -> List[Dict[str, Any]]:
|
|
390
|
+
"""
|
|
391
|
+
Parse text to extract tool calls.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of dicts with 'tool' and 'arguments' keys
|
|
395
|
+
"""
|
|
396
|
+
tool_calls = []
|
|
397
|
+
|
|
398
|
+
# Find all tool call matches
|
|
399
|
+
matches = re.finditer(ToolCallParser.TOOL_CALL_PATTERN, text, re.MULTILINE)
|
|
400
|
+
|
|
401
|
+
for match in matches:
|
|
402
|
+
tool_name = match.group(1)
|
|
403
|
+
args_str = match.group(2)
|
|
404
|
+
|
|
405
|
+
# Parse arguments
|
|
406
|
+
arguments = {}
|
|
407
|
+
if args_str.strip():
|
|
408
|
+
try:
|
|
409
|
+
# Try to parse as Python kwargs
|
|
410
|
+
# Handle both key="value" and positional args
|
|
411
|
+
args_dict = {}
|
|
412
|
+
|
|
413
|
+
# Split by comma, but respect quotes and parentheses
|
|
414
|
+
parts = []
|
|
415
|
+
current = ""
|
|
416
|
+
in_quotes = False
|
|
417
|
+
paren_depth = 0
|
|
418
|
+
quote_char = None
|
|
419
|
+
|
|
420
|
+
for char in args_str:
|
|
421
|
+
if char in ['"', "'"] and quote_char is None:
|
|
422
|
+
quote_char = char
|
|
423
|
+
in_quotes = True
|
|
424
|
+
current += char
|
|
425
|
+
elif char == quote_char:
|
|
426
|
+
in_quotes = False
|
|
427
|
+
quote_char = None
|
|
428
|
+
current += char
|
|
429
|
+
elif char == '(' and not in_quotes:
|
|
430
|
+
paren_depth += 1
|
|
431
|
+
current += char
|
|
432
|
+
elif char == ')' and not in_quotes:
|
|
433
|
+
paren_depth -= 1
|
|
434
|
+
current += char
|
|
435
|
+
elif char == ',' and not in_quotes and paren_depth == 0:
|
|
436
|
+
if current.strip():
|
|
437
|
+
parts.append(current.strip())
|
|
438
|
+
current = ""
|
|
439
|
+
else:
|
|
440
|
+
current += char
|
|
441
|
+
|
|
442
|
+
if current.strip():
|
|
443
|
+
parts.append(current.strip())
|
|
444
|
+
|
|
445
|
+
# Parse each part
|
|
446
|
+
for i, part in enumerate(parts):
|
|
447
|
+
if '=' in part and not part.strip().startswith('"'):
|
|
448
|
+
key, value = part.split('=', 1)
|
|
449
|
+
key = key.strip()
|
|
450
|
+
value = value.strip().strip('"\'')
|
|
451
|
+
args_dict[key] = value
|
|
452
|
+
else:
|
|
453
|
+
# Positional argument - use index as key
|
|
454
|
+
value = part.strip().strip('"\'')
|
|
455
|
+
# Try to infer parameter name from common patterns
|
|
456
|
+
if i == 0 and value:
|
|
457
|
+
# First arg is usually the main parameter
|
|
458
|
+
if tool_name == 'calculate':
|
|
459
|
+
args_dict['expression'] = value
|
|
460
|
+
elif tool_name in ['count_words', 'reverse_text', 'to_uppercase', 'to_lowercase']:
|
|
461
|
+
args_dict['text'] = value
|
|
462
|
+
elif tool_name == 'get_weather':
|
|
463
|
+
args_dict['city'] = value
|
|
464
|
+
elif tool_name in ['read_file', 'write_file']:
|
|
465
|
+
args_dict['filepath'] = value
|
|
466
|
+
else:
|
|
467
|
+
args_dict[f'arg{i}'] = value
|
|
468
|
+
|
|
469
|
+
arguments = args_dict
|
|
470
|
+
except Exception as e:
|
|
471
|
+
logger.warning(f"Failed to parse arguments: {args_str} - Error: {e}")
|
|
472
|
+
|
|
473
|
+
tool_calls.append({
|
|
474
|
+
"tool": tool_name,
|
|
475
|
+
"arguments": arguments
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
return tool_calls
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def has_tool_call(text: str) -> bool:
|
|
482
|
+
"""Check if text contains a tool call"""
|
|
483
|
+
return bool(re.search(ToolCallParser.TOOL_CALL_PATTERN, text))
|
|
484
|
+
|
|
485
|
+
@staticmethod
|
|
486
|
+
def remove_tool_calls(text: str) -> str:
|
|
487
|
+
"""Remove tool call syntax from text, keeping only natural language"""
|
|
488
|
+
return re.sub(ToolCallParser.TOOL_CALL_PATTERN, '', text).strip()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def format_tools_for_prompt(tools: List[Tool]) -> str:
|
|
492
|
+
"""
|
|
493
|
+
Format tools as a string for LLM prompt.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Formatted string describing available tools
|
|
497
|
+
"""
|
|
498
|
+
if not tools:
|
|
499
|
+
return ""
|
|
500
|
+
|
|
501
|
+
lines = ["You have access to the following tools:"]
|
|
502
|
+
lines.append("")
|
|
503
|
+
|
|
504
|
+
for tool in tools:
|
|
505
|
+
lines.append(f"- **{tool.name}**: {tool.description}")
|
|
506
|
+
|
|
507
|
+
if tool.parameters:
|
|
508
|
+
lines.append(" Parameters:")
|
|
509
|
+
for param in tool.parameters:
|
|
510
|
+
req = "required" if param.required else "optional"
|
|
511
|
+
lines.append(f" - {param.name} ({param.type}, {req}): {param.description}")
|
|
512
|
+
|
|
513
|
+
lines.append("")
|
|
514
|
+
|
|
515
|
+
lines.append("=" * 80)
|
|
516
|
+
lines.append("TOOL USAGE INSTRUCTIONS:")
|
|
517
|
+
lines.append("-" * 80)
|
|
518
|
+
lines.append("To call a tool, use EXACTLY this format:")
|
|
519
|
+
lines.append(' TOOL_CALL: tool_name(param1="value1", param2="value2")')
|
|
520
|
+
lines.append("")
|
|
521
|
+
lines.append("EXAMPLES:")
|
|
522
|
+
lines.append(' TOOL_CALL: calculate(expression="(25 * 4) + 10")')
|
|
523
|
+
lines.append(' TOOL_CALL: count_words(text="Hello world from AI")')
|
|
524
|
+
lines.append(' TOOL_CALL: get_current_time()')
|
|
525
|
+
lines.append(' TOOL_CALL: read_file(filepath="data.txt")')
|
|
526
|
+
lines.append("")
|
|
527
|
+
lines.append("IMPORTANT RULES:")
|
|
528
|
+
lines.append(" 1. Always use named parameters (param=\"value\")")
|
|
529
|
+
lines.append(" 2. Put ALL parameters inside the parentheses")
|
|
530
|
+
lines.append(" 3. Use double quotes for string values")
|
|
531
|
+
lines.append(" 4. One tool call per line")
|
|
532
|
+
lines.append(" 5. After tool execution, you will receive results to continue your response")
|
|
533
|
+
lines.append("=" * 80)
|
|
534
|
+
lines.append("")
|
|
535
|
+
|
|
536
|
+
return "\n".join(lines)
|
|
537
|
+
|