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
@@ -0,0 +1,201 @@
1
+ """
2
+ Base LLM Client Interface
3
+ ==========================
4
+
5
+ Abstract base class for all LLM client implementations.
6
+ Ensures consistent interface across different backends (Ollama, LM Studio)
7
+
8
+ Author: C. Emre Karataş
9
+ Version: 1.3.0
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from typing import List, Dict, Optional, Any, Iterator, Union
14
+ import logging
15
+
16
+
17
+ class BaseLLMClient(ABC):
18
+ """
19
+ Abstract base class for LLM clients
20
+
21
+ All LLM backends must implement these methods to ensure
22
+ compatibility with MemAgent and other components.
23
+ """
24
+
25
+ def __init__(self, model: str = None, **kwargs):
26
+ """
27
+ Initialize LLM client
28
+
29
+ Args:
30
+ model: Model name/identifier
31
+ **kwargs: Backend-specific configuration
32
+ """
33
+ self.model = model
34
+ self.logger = logging.getLogger(self.__class__.__name__)
35
+
36
+ @abstractmethod
37
+ def chat(self, messages: List[Dict[str, str]],
38
+ temperature: float = 0.7,
39
+ max_tokens: int = 2000,
40
+ **kwargs) -> str:
41
+ """
42
+ Send chat request and return response
43
+
44
+ Args:
45
+ messages: List of messages in format:
46
+ [{"role": "system/user/assistant", "content": "..."}]
47
+ temperature: Sampling temperature (0.0-1.0)
48
+ max_tokens: Maximum tokens in response
49
+ **kwargs: Additional backend-specific parameters
50
+
51
+ Returns:
52
+ Model response text
53
+
54
+ Raises:
55
+ ConnectionError: If cannot connect to service
56
+ ValueError: If invalid parameters
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def check_connection(self) -> bool:
62
+ """
63
+ Check if LLM service is available and responding
64
+
65
+ Returns:
66
+ True if service is available, False otherwise
67
+ """
68
+ pass
69
+
70
+ def chat_stream(self, messages: List[Dict[str, str]],
71
+ temperature: float = 0.7,
72
+ max_tokens: int = 2000,
73
+ **kwargs) -> Iterator[str]:
74
+ """
75
+ Send chat request and stream response chunks (optional, not all backends support)
76
+
77
+ Args:
78
+ messages: List of messages in format:
79
+ [{"role": "system/user/assistant", "content": "..."}]
80
+ temperature: Sampling temperature (0.0-1.0)
81
+ max_tokens: Maximum tokens in response
82
+ **kwargs: Additional backend-specific parameters
83
+
84
+ Yields:
85
+ Response text chunks as they arrive
86
+
87
+ Raises:
88
+ NotImplementedError: If backend doesn't support streaming
89
+ ConnectionError: If cannot connect to service
90
+ ValueError: If invalid parameters
91
+ """
92
+ # Default implementation: fall back to non-streaming
93
+ response = self.chat(messages, temperature, max_tokens, **kwargs)
94
+ yield response
95
+
96
+ def generate(self, prompt: str,
97
+ system_prompt: Optional[str] = None,
98
+ temperature: float = 0.7,
99
+ max_tokens: int = 500,
100
+ **kwargs) -> str:
101
+ """
102
+ Generate text from a simple prompt (convenience method)
103
+
104
+ Args:
105
+ prompt: User prompt
106
+ system_prompt: Optional system prompt
107
+ temperature: Sampling temperature
108
+ max_tokens: Maximum tokens
109
+ **kwargs: Additional parameters
110
+
111
+ Returns:
112
+ Generated text
113
+ """
114
+ # Convert to chat format
115
+ messages = []
116
+ if system_prompt:
117
+ messages.append({"role": "system", "content": system_prompt})
118
+ messages.append({"role": "user", "content": prompt})
119
+
120
+ return self.chat(messages, temperature, max_tokens, **kwargs)
121
+
122
+ def list_models(self) -> List[str]:
123
+ """
124
+ List available models (optional, not all backends support this)
125
+
126
+ Returns:
127
+ List of model names
128
+ """
129
+ return [self.model] if self.model else []
130
+
131
+ def _format_messages_to_text(self, messages: List[Dict]) -> str:
132
+ """
133
+ Helper: Convert message list to text format
134
+
135
+ Useful for backends that don't support chat format natively.
136
+
137
+ Args:
138
+ messages: Message list
139
+
140
+ Returns:
141
+ Formatted text prompt
142
+ """
143
+ result = []
144
+ for msg in messages:
145
+ role = msg.get('role', 'user').upper()
146
+ content = msg.get('content', '').strip()
147
+ if content:
148
+ result.append(f"{role}: {content}")
149
+ return "\n\n".join(result)
150
+
151
+ def _validate_messages(self, messages: List[Dict]) -> bool:
152
+ """
153
+ Validate message format
154
+
155
+ Args:
156
+ messages: Messages to validate
157
+
158
+ Returns:
159
+ True if valid
160
+
161
+ Raises:
162
+ ValueError: If invalid format
163
+ """
164
+ if not isinstance(messages, list):
165
+ raise ValueError("Messages must be a list")
166
+
167
+ if not messages:
168
+ raise ValueError("Messages list cannot be empty")
169
+
170
+ for i, msg in enumerate(messages):
171
+ if not isinstance(msg, dict):
172
+ raise ValueError(f"Message {i} must be a dictionary")
173
+
174
+ if 'role' not in msg:
175
+ raise ValueError(f"Message {i} missing 'role' field")
176
+
177
+ if 'content' not in msg:
178
+ raise ValueError(f"Message {i} missing 'content' field")
179
+
180
+ if msg['role'] not in ['system', 'user', 'assistant']:
181
+ raise ValueError(f"Message {i} has invalid role: {msg['role']}")
182
+
183
+ return True
184
+
185
+ def get_info(self) -> Dict[str, Any]:
186
+ """
187
+ Get client information
188
+
189
+ Returns:
190
+ Dictionary with client metadata
191
+ """
192
+ return {
193
+ 'backend': self.__class__.__name__,
194
+ 'model': self.model,
195
+ 'available': self.check_connection()
196
+ }
197
+
198
+ def __repr__(self) -> str:
199
+ """String representation"""
200
+ return f"{self.__class__.__name__}(model='{self.model}')"
201
+
@@ -0,0 +1,311 @@
1
+ """
2
+ Built-in Tools
3
+ ==============
4
+
5
+ Common tools that are available by default.
6
+
7
+ Author: C. Emre Karataş
8
+ Version: 2.0.0
9
+ """
10
+
11
+ import math
12
+ import os
13
+ import json
14
+ from datetime import datetime
15
+ from typing import List, Dict, Any
16
+ from .tool_system import tool
17
+
18
+
19
+ # ============================================================================
20
+ # Math & Calculation Tools
21
+ # ============================================================================
22
+
23
+ @tool(name="calculate", description="Evaluate mathematical expressions", category="math")
24
+ def calculate(expression: str) -> float:
25
+ """
26
+ Evaluate a mathematical expression safely.
27
+
28
+ Args:
29
+ expression: Mathematical expression (e.g., "2 + 2", "sqrt(16)", "pi * 2", "(25 * 4) + 10")
30
+
31
+ Returns:
32
+ Result of the calculation
33
+
34
+ Examples:
35
+ calculate("2 + 2") -> 4.0
36
+ calculate("sqrt(16)") -> 4.0
37
+ calculate("pi * 2") -> 6.283185307179586
38
+ calculate("(25 * 4) + 10") -> 110.0
39
+ """
40
+ # Safe eval with math functions
41
+ allowed_names = {
42
+ k: v for k, v in math.__dict__.items() if not k.startswith("__")
43
+ }
44
+ allowed_names["abs"] = abs
45
+ allowed_names["round"] = round
46
+ allowed_names["min"] = min
47
+ allowed_names["max"] = max
48
+ allowed_names["sum"] = sum
49
+
50
+ try:
51
+ # Clean up expression - replace common text with symbols
52
+ clean_expr = expression.strip()
53
+ clean_expr = clean_expr.replace(" divided by ", " / ")
54
+ clean_expr = clean_expr.replace(" times ", " * ")
55
+ clean_expr = clean_expr.replace(" plus ", " + ")
56
+ clean_expr = clean_expr.replace(" minus ", " - ")
57
+
58
+ result = eval(clean_expr, {"__builtins__": {}}, allowed_names)
59
+ return float(result)
60
+ except Exception as e:
61
+ raise ValueError(f"Invalid expression '{expression}': {str(e)}")
62
+
63
+
64
+ # ============================================================================
65
+ # Text Processing Tools
66
+ # ============================================================================
67
+
68
+ @tool(name="count_words", description="Count words in text", category="text")
69
+ def count_words(text: str) -> int:
70
+ """
71
+ Count the number of words in a text.
72
+
73
+ Args:
74
+ text: Text to count words in
75
+
76
+ Returns:
77
+ Number of words
78
+ """
79
+ return len(text.split())
80
+
81
+
82
+ @tool(name="reverse_text", description="Reverse a text string", category="text")
83
+ def reverse_text(text: str) -> str:
84
+ """
85
+ Reverse the order of characters in text.
86
+
87
+ Args:
88
+ text: Text to reverse
89
+
90
+ Returns:
91
+ Reversed text
92
+ """
93
+ return text[::-1]
94
+
95
+
96
+ @tool(name="to_uppercase", description="Convert text to uppercase", category="text")
97
+ def to_uppercase(text: str) -> str:
98
+ """
99
+ Convert text to uppercase.
100
+
101
+ Args:
102
+ text: Text to convert
103
+
104
+ Returns:
105
+ Uppercase text
106
+ """
107
+ return text.upper()
108
+
109
+
110
+ @tool(name="to_lowercase", description="Convert text to lowercase", category="text")
111
+ def to_lowercase(text: str) -> str:
112
+ """
113
+ Convert text to lowercase.
114
+
115
+ Args:
116
+ text: Text to convert
117
+
118
+ Returns:
119
+ Lowercase text
120
+ """
121
+ return text.lower()
122
+
123
+
124
+ # ============================================================================
125
+ # File System Tools
126
+ # ============================================================================
127
+
128
+ @tool(name="read_file", description="Read contents of a text file", category="file")
129
+ def read_file(filepath: str) -> str:
130
+ """
131
+ Read and return the contents of a text file.
132
+
133
+ Args:
134
+ filepath: Path to the file to read
135
+
136
+ Returns:
137
+ File contents as string
138
+ """
139
+ try:
140
+ with open(filepath, 'r', encoding='utf-8') as f:
141
+ return f.read()
142
+ except Exception as e:
143
+ raise ValueError(f"Error reading file: {e}")
144
+
145
+
146
+ @tool(name="write_file", description="Write text to a file", category="file")
147
+ def write_file(filepath: str, content: str) -> str:
148
+ """
149
+ Write text content to a file.
150
+
151
+ Args:
152
+ filepath: Path to the file to write
153
+ content: Content to write to the file
154
+
155
+ Returns:
156
+ Success message
157
+ """
158
+ try:
159
+ with open(filepath, 'w', encoding='utf-8') as f:
160
+ f.write(content)
161
+ return f"Successfully wrote {len(content)} characters to {filepath}"
162
+ except Exception as e:
163
+ raise ValueError(f"Error writing file: {e}")
164
+
165
+
166
+ @tool(name="list_files", description="List files in a directory", category="file")
167
+ def list_files(directory: str) -> List[str]:
168
+ """
169
+ List all files in a directory.
170
+
171
+ Args:
172
+ directory: Path to the directory
173
+
174
+ Returns:
175
+ List of filenames
176
+ """
177
+ try:
178
+ return os.listdir(directory)
179
+ except Exception as e:
180
+ raise ValueError(f"Error listing directory: {e}")
181
+
182
+
183
+ # ============================================================================
184
+ # Utility Tools
185
+ # ============================================================================
186
+
187
+ @tool(name="get_current_time", description="Get current date and time", category="utility")
188
+ def get_current_time() -> str:
189
+ """
190
+ Get the current date and time.
191
+
192
+ Returns:
193
+ Current datetime as string
194
+ """
195
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
196
+
197
+
198
+ @tool(name="create_json", description="Create JSON from text", category="utility")
199
+ def create_json(data: str) -> str:
200
+ """
201
+ Parse and format text as JSON.
202
+
203
+ Args:
204
+ data: Text to convert to JSON
205
+
206
+ Returns:
207
+ Formatted JSON string
208
+ """
209
+ try:
210
+ parsed = json.loads(data)
211
+ return json.dumps(parsed, indent=2)
212
+ except:
213
+ # Try to create simple key-value JSON
214
+ lines = data.strip().split('\n')
215
+ result = {}
216
+ for line in lines:
217
+ if ':' in line:
218
+ key, value = line.split(':', 1)
219
+ result[key.strip()] = value.strip()
220
+ return json.dumps(result, indent=2)
221
+
222
+
223
+ # ============================================================================
224
+ # Memory & Context Tools (NEW in v2.0.0)
225
+ # ============================================================================
226
+
227
+ @tool(name="search_memory", description="Search through conversation history", category="memory")
228
+ def search_memory(keyword: str) -> str:
229
+ """
230
+ Search through conversation history for a keyword.
231
+
232
+ Args:
233
+ keyword: The keyword to search for in conversation history
234
+
235
+ Returns:
236
+ Search results or error message
237
+
238
+ Examples:
239
+ search_memory("weather") -> "Found 2 conversations about weather..."
240
+
241
+ Note:
242
+ This tool requires MemAgent context. Will return instructions if called standalone.
243
+ """
244
+ # This is a placeholder - actual implementation happens in MemAgent._execute_tool_calls
245
+ return f"MEMORY_SEARCH:{keyword}"
246
+
247
+
248
+ @tool(name="get_user_info", description="Get current user profile information", category="memory")
249
+ def get_user_info() -> str:
250
+ """
251
+ Get information about the current user.
252
+
253
+ Returns:
254
+ User profile information
255
+
256
+ Examples:
257
+ get_user_info() -> "Current user: john_doe (active since 2025-01-15)"
258
+
259
+ Note:
260
+ This tool requires MemAgent context. Will return instructions if called standalone.
261
+ """
262
+ # This is a placeholder - actual implementation happens in MemAgent._execute_tool_calls
263
+ return "MEMORY_USER_INFO"
264
+
265
+
266
+ @tool(name="list_conversations", description="List recent conversations", category="memory")
267
+ def list_conversations(limit: int = 5) -> str:
268
+ """
269
+ List recent conversation sessions.
270
+
271
+ Args:
272
+ limit: Maximum number of conversations to list (default: 5)
273
+
274
+ Returns:
275
+ List of recent conversations
276
+
277
+ Examples:
278
+ list_conversations(3) -> "Last 3 conversations: ..."
279
+
280
+ Note:
281
+ This tool requires MemAgent context. Will return instructions if called standalone.
282
+ """
283
+ # This is a placeholder - actual implementation happens in MemAgent._execute_tool_calls
284
+ return f"MEMORY_LIST_CONVERSATIONS:{limit}"
285
+
286
+
287
+ # ============================================================================
288
+ # Export all tools
289
+ # ============================================================================
290
+
291
+ BUILTIN_TOOLS = [
292
+ # Math
293
+ calculate,
294
+ # Text
295
+ count_words,
296
+ reverse_text,
297
+ to_uppercase,
298
+ to_lowercase,
299
+ # File
300
+ read_file,
301
+ write_file,
302
+ list_files,
303
+ # Utility
304
+ get_current_time,
305
+ create_json,
306
+ # Memory (v2.0.0+)
307
+ search_memory,
308
+ get_user_info,
309
+ list_conversations,
310
+ ]
311
+
@@ -0,0 +1,170 @@
1
+ """
2
+ Built-in Async Tools (v2.1.0+)
3
+ ==============================
4
+
5
+ Async versions of common tools for I/O-bound operations.
6
+
7
+ Author: C. Emre Karataş
8
+ Version: 2.1.0
9
+ """
10
+
11
+ import asyncio
12
+ import aiohttp
13
+ from typing import Dict, Any
14
+ from .tool_system import tool
15
+
16
+ # ============================================================================
17
+ # Async Web & API Tools
18
+ # ============================================================================
19
+
20
+ @tool(
21
+ name="fetch_url",
22
+ description="Fetch content from a URL asynchronously",
23
+ category="web",
24
+ pattern={"url": r'^https?://'},
25
+ max_length={"url": 2048}
26
+ )
27
+ async def fetch_url(url: str, timeout: int = 10) -> str:
28
+ """
29
+ Fetch content from a URL.
30
+
31
+ Args:
32
+ url: The URL to fetch (must start with http:// or https://)
33
+ timeout: Request timeout in seconds (default: 10)
34
+
35
+ Returns:
36
+ Response text or error message
37
+ """
38
+ try:
39
+ async with aiohttp.ClientSession() as session:
40
+ async with session.get(url, timeout=timeout) as response:
41
+ if response.status == 200:
42
+ text = await response.text()
43
+ return text[:5000] # Limit to 5000 chars
44
+ return f"HTTP {response.status}: {response.reason}"
45
+ except asyncio.TimeoutError:
46
+ return f"Request timed out after {timeout}s"
47
+ except Exception as e:
48
+ return f"Error fetching URL: {e}"
49
+
50
+
51
+ @tool(
52
+ name="post_json",
53
+ description="Post JSON data to an API endpoint",
54
+ category="web",
55
+ pattern={"url": r'^https?://'}
56
+ )
57
+ async def post_json(url: str, data: Dict[str, Any], timeout: int = 10) -> str:
58
+ """
59
+ Post JSON data to an API endpoint.
60
+
61
+ Args:
62
+ url: The API endpoint URL
63
+ data: JSON data to post
64
+ timeout: Request timeout in seconds
65
+
66
+ Returns:
67
+ Response text or error message
68
+ """
69
+ try:
70
+ async with aiohttp.ClientSession() as session:
71
+ async with session.post(url, json=data, timeout=timeout) as response:
72
+ text = await response.text()
73
+ return f"Status {response.status}: {text[:500]}"
74
+ except Exception as e:
75
+ return f"Error posting data: {e}"
76
+
77
+
78
+ # ============================================================================
79
+ # Async File Operations
80
+ # ============================================================================
81
+
82
+ @tool(
83
+ name="read_file_async",
84
+ description="Read a file asynchronously",
85
+ category="file",
86
+ max_length={"filepath": 260}
87
+ )
88
+ async def read_file_async(filepath: str) -> str:
89
+ """
90
+ Read a file asynchronously.
91
+
92
+ Args:
93
+ filepath: Path to the file
94
+
95
+ Returns:
96
+ File contents or error message
97
+ """
98
+ try:
99
+ loop = asyncio.get_event_loop()
100
+ with open(filepath, 'r', encoding='utf-8') as f:
101
+ content = await loop.run_in_executor(None, f.read)
102
+ return content
103
+ except FileNotFoundError:
104
+ return f"File not found: {filepath}"
105
+ except Exception as e:
106
+ return f"Error reading file: {e}"
107
+
108
+
109
+ @tool(
110
+ name="write_file_async",
111
+ description="Write to a file asynchronously",
112
+ category="file"
113
+ )
114
+ async def write_file_async(filepath: str, content: str) -> str:
115
+ """
116
+ Write to a file asynchronously.
117
+
118
+ Args:
119
+ filepath: Path to the file
120
+ content: Content to write
121
+
122
+ Returns:
123
+ Success message or error
124
+ """
125
+ try:
126
+ loop = asyncio.get_event_loop()
127
+ with open(filepath, 'w', encoding='utf-8') as f:
128
+ await loop.run_in_executor(None, f.write, content)
129
+ return f"Successfully wrote {len(content)} chars to {filepath}"
130
+ except Exception as e:
131
+ return f"Error writing file: {e}"
132
+
133
+
134
+ # ============================================================================
135
+ # Async Utility Tools
136
+ # ============================================================================
137
+
138
+ @tool(
139
+ name="sleep",
140
+ description="Asynchronously wait for specified seconds",
141
+ category="utility",
142
+ min_value={"seconds": 0},
143
+ max_value={"seconds": 60}
144
+ )
145
+ async def async_sleep(seconds: float) -> str:
146
+ """
147
+ Wait asynchronously for specified seconds.
148
+
149
+ Args:
150
+ seconds: Number of seconds to wait (0-60)
151
+
152
+ Returns:
153
+ Completion message
154
+ """
155
+ await asyncio.sleep(seconds)
156
+ return f"Waited for {seconds} seconds"
157
+
158
+
159
+ # ============================================================================
160
+ # Export Async Tools
161
+ # ============================================================================
162
+
163
+ ASYNC_TOOLS = [
164
+ fetch_url,
165
+ post_json,
166
+ read_file_async,
167
+ write_file_async,
168
+ async_sleep,
169
+ ]
170
+