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.

Files changed (41) hide show
  1. mem_llm/__init__.py +71 -8
  2. mem_llm/api_server.py +595 -0
  3. mem_llm/base_llm_client.py +201 -0
  4. mem_llm/builtin_tools.py +311 -0
  5. mem_llm/builtin_tools_async.py +170 -0
  6. mem_llm/cli.py +254 -0
  7. mem_llm/clients/__init__.py +22 -0
  8. mem_llm/clients/lmstudio_client.py +393 -0
  9. mem_llm/clients/ollama_client.py +354 -0
  10. mem_llm/config.yaml.example +1 -1
  11. mem_llm/config_from_docs.py +1 -1
  12. mem_llm/config_manager.py +5 -3
  13. mem_llm/conversation_summarizer.py +372 -0
  14. mem_llm/data_export_import.py +640 -0
  15. mem_llm/dynamic_prompt.py +298 -0
  16. mem_llm/llm_client.py +77 -14
  17. mem_llm/llm_client_factory.py +260 -0
  18. mem_llm/logger.py +129 -0
  19. mem_llm/mem_agent.py +1178 -87
  20. mem_llm/memory_db.py +290 -59
  21. mem_llm/memory_manager.py +60 -1
  22. mem_llm/prompt_security.py +304 -0
  23. mem_llm/response_metrics.py +221 -0
  24. mem_llm/retry_handler.py +193 -0
  25. mem_llm/thread_safe_db.py +301 -0
  26. mem_llm/tool_system.py +537 -0
  27. mem_llm/vector_store.py +278 -0
  28. mem_llm/web_launcher.py +129 -0
  29. mem_llm/web_ui/README.md +44 -0
  30. mem_llm/web_ui/__init__.py +7 -0
  31. mem_llm/web_ui/index.html +641 -0
  32. mem_llm/web_ui/memory.html +569 -0
  33. mem_llm/web_ui/metrics.html +75 -0
  34. mem_llm-2.1.0.dist-info/METADATA +753 -0
  35. mem_llm-2.1.0.dist-info/RECORD +40 -0
  36. {mem_llm-1.0.2.dist-info → mem_llm-2.1.0.dist-info}/WHEEL +1 -1
  37. mem_llm-2.1.0.dist-info/entry_points.txt +3 -0
  38. mem_llm/prompt_templates.py +0 -244
  39. mem_llm-1.0.2.dist-info/METADATA +0 -382
  40. mem_llm-1.0.2.dist-info/RECORD +0 -15
  41. {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
+