mem-llm 2.0.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.
- mem_llm/__init__.py +98 -0
- mem_llm/api_server.py +595 -0
- mem_llm/base_llm_client.py +201 -0
- mem_llm/builtin_tools.py +311 -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 +52 -0
- mem_llm/config_from_docs.py +180 -0
- mem_llm/config_manager.py +231 -0
- mem_llm/conversation_summarizer.py +372 -0
- mem_llm/data_export_import.py +640 -0
- mem_llm/dynamic_prompt.py +298 -0
- mem_llm/knowledge_loader.py +88 -0
- mem_llm/llm_client.py +225 -0
- mem_llm/llm_client_factory.py +260 -0
- mem_llm/logger.py +129 -0
- mem_llm/mem_agent.py +1611 -0
- mem_llm/memory_db.py +612 -0
- mem_llm/memory_manager.py +321 -0
- mem_llm/memory_tools.py +253 -0
- 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 +429 -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.0.0.dist-info/METADATA +667 -0
- mem_llm-2.0.0.dist-info/RECORD +39 -0
- mem_llm-2.0.0.dist-info/WHEEL +5 -0
- mem_llm-2.0.0.dist-info/entry_points.txt +3 -0
- mem_llm-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama LLM Client
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
Client for local Ollama service.
|
|
6
|
+
Supports all Ollama models (Llama3, Granite, Qwen3, DeepSeek, etc.)
|
|
7
|
+
|
|
8
|
+
Author: C. Emre Karataş
|
|
9
|
+
Version: 1.3.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
import time
|
|
14
|
+
import json
|
|
15
|
+
from typing import List, Dict, Optional, Iterator
|
|
16
|
+
import sys
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
# Add parent directory to path for imports
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
|
|
22
|
+
from base_llm_client import BaseLLMClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OllamaClient(BaseLLMClient):
|
|
26
|
+
"""
|
|
27
|
+
Ollama LLM client implementation
|
|
28
|
+
|
|
29
|
+
Supports:
|
|
30
|
+
- All Ollama models
|
|
31
|
+
- Chat and generate modes
|
|
32
|
+
- Thinking mode detection (Qwen3, DeepSeek)
|
|
33
|
+
- Automatic retry with exponential backoff
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self,
|
|
37
|
+
model: str = "granite4:3b",
|
|
38
|
+
base_url: str = "http://localhost:11434",
|
|
39
|
+
**kwargs):
|
|
40
|
+
"""
|
|
41
|
+
Initialize Ollama client
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model: Model name (e.g., "llama3", "granite4:3b")
|
|
45
|
+
base_url: Ollama API URL
|
|
46
|
+
**kwargs: Additional configuration
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(model=model, **kwargs)
|
|
49
|
+
self.base_url = base_url
|
|
50
|
+
self.api_url = f"{base_url}/api/generate"
|
|
51
|
+
self.chat_url = f"{base_url}/api/chat"
|
|
52
|
+
self.tags_url = f"{base_url}/api/tags"
|
|
53
|
+
|
|
54
|
+
self.logger.debug(f"Initialized Ollama client: {base_url}, model: {model}")
|
|
55
|
+
|
|
56
|
+
def check_connection(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if Ollama service is running
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if service is available
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
response = requests.get(self.tags_url, timeout=5)
|
|
65
|
+
return response.status_code == 200
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.debug(f"Ollama connection check failed: {e}")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def list_models(self) -> List[str]:
|
|
71
|
+
"""
|
|
72
|
+
List available Ollama models
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of model names
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
response = requests.get(self.tags_url, timeout=5)
|
|
79
|
+
if response.status_code == 200:
|
|
80
|
+
data = response.json()
|
|
81
|
+
return [model['name'] for model in data.get('models', [])]
|
|
82
|
+
return []
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.logger.error(f"Failed to list models: {e}")
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def chat(self,
|
|
88
|
+
messages: List[Dict[str, str]],
|
|
89
|
+
temperature: float = 0.7,
|
|
90
|
+
max_tokens: int = 2000,
|
|
91
|
+
**kwargs) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Send chat request to Ollama
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
messages: Message history
|
|
97
|
+
temperature: Sampling temperature (0.0-1.0)
|
|
98
|
+
max_tokens: Maximum tokens in response
|
|
99
|
+
**kwargs: Additional Ollama-specific options
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Model response text
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ConnectionError: If cannot connect to Ollama
|
|
106
|
+
ValueError: If invalid parameters
|
|
107
|
+
"""
|
|
108
|
+
# Validate messages
|
|
109
|
+
self._validate_messages(messages)
|
|
110
|
+
|
|
111
|
+
# Build payload
|
|
112
|
+
payload = {
|
|
113
|
+
"model": self.model,
|
|
114
|
+
"messages": messages,
|
|
115
|
+
"stream": False,
|
|
116
|
+
"options": {
|
|
117
|
+
"temperature": temperature,
|
|
118
|
+
"num_predict": max_tokens,
|
|
119
|
+
"num_ctx": kwargs.get("num_ctx", 4096),
|
|
120
|
+
"top_k": kwargs.get("top_k", 40),
|
|
121
|
+
"top_p": kwargs.get("top_p", 0.9),
|
|
122
|
+
"num_thread": kwargs.get("num_thread", 8)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Disable thinking mode for thinking-enabled models
|
|
127
|
+
# (Qwen3, DeepSeek) to get direct answers
|
|
128
|
+
if any(name in self.model.lower() for name in ['qwen', 'deepseek', 'qwq']):
|
|
129
|
+
payload["options"]["enable_thinking"] = False
|
|
130
|
+
|
|
131
|
+
# Send request with retry logic
|
|
132
|
+
max_retries = kwargs.get("max_retries", 3)
|
|
133
|
+
for attempt in range(max_retries):
|
|
134
|
+
try:
|
|
135
|
+
response = requests.post(
|
|
136
|
+
self.chat_url,
|
|
137
|
+
json=payload,
|
|
138
|
+
timeout=kwargs.get("timeout", 120)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if response.status_code == 200:
|
|
142
|
+
response_data = response.json()
|
|
143
|
+
message = response_data.get('message', {})
|
|
144
|
+
|
|
145
|
+
# Get content - primary response field
|
|
146
|
+
result = message.get('content', '').strip()
|
|
147
|
+
|
|
148
|
+
# Fallback: Extract from thinking if content is empty
|
|
149
|
+
if not result and message.get('thinking'):
|
|
150
|
+
result = self._extract_from_thinking(message.get('thinking', ''))
|
|
151
|
+
|
|
152
|
+
if not result:
|
|
153
|
+
self.logger.warning("Empty response from Ollama")
|
|
154
|
+
if attempt < max_retries - 1:
|
|
155
|
+
time.sleep(1.0 * (2 ** attempt))
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
else:
|
|
160
|
+
error_msg = f"Ollama API error: {response.status_code} - {response.text}"
|
|
161
|
+
self.logger.error(error_msg)
|
|
162
|
+
if attempt < max_retries - 1:
|
|
163
|
+
time.sleep(1.0 * (2 ** attempt))
|
|
164
|
+
continue
|
|
165
|
+
raise ConnectionError(error_msg)
|
|
166
|
+
|
|
167
|
+
except requests.exceptions.Timeout:
|
|
168
|
+
self.logger.warning(f"Ollama request timeout (attempt {attempt + 1}/{max_retries})")
|
|
169
|
+
if attempt < max_retries - 1:
|
|
170
|
+
time.sleep(2.0 * (2 ** attempt))
|
|
171
|
+
continue
|
|
172
|
+
raise ConnectionError("Ollama request timeout. Check if service is running.")
|
|
173
|
+
|
|
174
|
+
except requests.exceptions.ConnectionError as e:
|
|
175
|
+
self.logger.warning(f"Cannot connect to Ollama (attempt {attempt + 1}/{max_retries})")
|
|
176
|
+
if attempt < max_retries - 1:
|
|
177
|
+
time.sleep(1.0 * (2 ** attempt))
|
|
178
|
+
continue
|
|
179
|
+
raise ConnectionError(f"Cannot connect to Ollama at {self.base_url}. Make sure service is running.") from e
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.logger.error(f"Unexpected error: {e}")
|
|
183
|
+
if attempt < max_retries - 1:
|
|
184
|
+
time.sleep(1.0 * (2 ** attempt))
|
|
185
|
+
continue
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
raise ConnectionError("Failed to get response after maximum retries")
|
|
189
|
+
|
|
190
|
+
def _extract_from_thinking(self, thinking: str) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Extract actual answer from thinking process
|
|
193
|
+
|
|
194
|
+
Some models output reasoning process instead of direct answer.
|
|
195
|
+
This extracts the final answer from that process.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
thinking: Thinking process text
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Extracted answer
|
|
202
|
+
"""
|
|
203
|
+
if not thinking:
|
|
204
|
+
return ""
|
|
205
|
+
|
|
206
|
+
# Try to find answer after common separators
|
|
207
|
+
for separator in ['\n\nAnswer:', '\n\nFinal answer:',
|
|
208
|
+
'\n\nResponse:', '\n\nSo the answer is:',
|
|
209
|
+
'\n\n---\n', '\n\nOkay,', '\n\nTherefore,']:
|
|
210
|
+
if separator in thinking:
|
|
211
|
+
parts = thinking.split(separator)
|
|
212
|
+
if len(parts) > 1:
|
|
213
|
+
return parts[-1].strip()
|
|
214
|
+
|
|
215
|
+
# Fallback: Get last meaningful paragraph
|
|
216
|
+
paragraphs = [p.strip() for p in thinking.split('\n\n') if p.strip()]
|
|
217
|
+
if paragraphs:
|
|
218
|
+
last_para = paragraphs[-1]
|
|
219
|
+
# Avoid meta-commentary
|
|
220
|
+
if not any(word in last_para.lower()
|
|
221
|
+
for word in ['wait', 'hmm', 'let me', 'thinking', 'okay']):
|
|
222
|
+
return last_para
|
|
223
|
+
|
|
224
|
+
# If nothing else works, return the whole thinking
|
|
225
|
+
return thinking
|
|
226
|
+
|
|
227
|
+
def chat_stream(self,
|
|
228
|
+
messages: List[Dict[str, str]],
|
|
229
|
+
temperature: float = 0.7,
|
|
230
|
+
max_tokens: int = 2000,
|
|
231
|
+
**kwargs) -> Iterator[str]:
|
|
232
|
+
"""
|
|
233
|
+
Send chat request to Ollama with streaming response
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
messages: Message history
|
|
237
|
+
temperature: Sampling temperature (0.0-1.0)
|
|
238
|
+
max_tokens: Maximum tokens in response
|
|
239
|
+
**kwargs: Additional Ollama-specific options
|
|
240
|
+
|
|
241
|
+
Yields:
|
|
242
|
+
Response text chunks as they arrive
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ConnectionError: If cannot connect to Ollama
|
|
246
|
+
ValueError: If invalid parameters
|
|
247
|
+
"""
|
|
248
|
+
# Validate messages
|
|
249
|
+
self._validate_messages(messages)
|
|
250
|
+
|
|
251
|
+
# Build payload
|
|
252
|
+
payload = {
|
|
253
|
+
"model": self.model,
|
|
254
|
+
"messages": messages,
|
|
255
|
+
"stream": True, # Enable streaming
|
|
256
|
+
"options": {
|
|
257
|
+
"temperature": temperature,
|
|
258
|
+
"num_predict": max_tokens,
|
|
259
|
+
"num_ctx": kwargs.get("num_ctx", 4096),
|
|
260
|
+
"top_k": kwargs.get("top_k", 40),
|
|
261
|
+
"top_p": kwargs.get("top_p", 0.9),
|
|
262
|
+
"num_thread": kwargs.get("num_thread", 8)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Disable thinking mode for thinking-enabled models
|
|
267
|
+
if any(name in self.model.lower() for name in ['qwen', 'deepseek', 'qwq']):
|
|
268
|
+
payload["options"]["enable_thinking"] = False
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
response = requests.post(
|
|
272
|
+
self.chat_url,
|
|
273
|
+
json=payload,
|
|
274
|
+
stream=True, # Enable streaming
|
|
275
|
+
timeout=kwargs.get("timeout", 120)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if response.status_code == 200:
|
|
279
|
+
# Process streaming response
|
|
280
|
+
for line in response.iter_lines():
|
|
281
|
+
if line:
|
|
282
|
+
try:
|
|
283
|
+
chunk_data = json.loads(line.decode('utf-8'))
|
|
284
|
+
|
|
285
|
+
# Get message content
|
|
286
|
+
message = chunk_data.get('message', {})
|
|
287
|
+
content = message.get('content', '')
|
|
288
|
+
|
|
289
|
+
if content:
|
|
290
|
+
yield content
|
|
291
|
+
|
|
292
|
+
# Check if this is the final chunk
|
|
293
|
+
if chunk_data.get('done', False):
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
except json.JSONDecodeError as e:
|
|
297
|
+
self.logger.warning(f"Failed to parse streaming chunk: {e}")
|
|
298
|
+
continue
|
|
299
|
+
else:
|
|
300
|
+
error_msg = f"Ollama API error: {response.status_code} - {response.text}"
|
|
301
|
+
self.logger.error(error_msg)
|
|
302
|
+
raise ConnectionError(error_msg)
|
|
303
|
+
|
|
304
|
+
except requests.exceptions.Timeout:
|
|
305
|
+
raise ConnectionError("Ollama request timeout. Check if service is running.")
|
|
306
|
+
except requests.exceptions.ConnectionError as e:
|
|
307
|
+
raise ConnectionError(f"Cannot connect to Ollama at {self.base_url}. Make sure service is running.") from e
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.error(f"Unexpected error in streaming: {e}")
|
|
310
|
+
raise
|
|
311
|
+
|
|
312
|
+
def generate_with_memory_context(self,
|
|
313
|
+
user_message: str,
|
|
314
|
+
memory_summary: str,
|
|
315
|
+
recent_conversations: List[Dict]) -> str:
|
|
316
|
+
"""
|
|
317
|
+
Generate response with memory context
|
|
318
|
+
|
|
319
|
+
This is a specialized method for MemAgent integration.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
user_message: User's message
|
|
323
|
+
memory_summary: Summary of past interactions
|
|
324
|
+
recent_conversations: Recent conversation history
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Context-aware response
|
|
328
|
+
"""
|
|
329
|
+
# Build system prompt
|
|
330
|
+
system_prompt = """You are a helpful customer service assistant.
|
|
331
|
+
You can remember past conversations with users.
|
|
332
|
+
Give short, clear and professional answers.
|
|
333
|
+
Use past interactions intelligently."""
|
|
334
|
+
|
|
335
|
+
# Build message history
|
|
336
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
337
|
+
|
|
338
|
+
# Add memory summary
|
|
339
|
+
if memory_summary and memory_summary != "No interactions with this user yet.":
|
|
340
|
+
messages.append({
|
|
341
|
+
"role": "system",
|
|
342
|
+
"content": f"User history:\n{memory_summary}"
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
# Add recent conversations (last 3)
|
|
346
|
+
for conv in recent_conversations[-3:]:
|
|
347
|
+
messages.append({"role": "user", "content": conv.get('user_message', '')})
|
|
348
|
+
messages.append({"role": "assistant", "content": conv.get('bot_response', '')})
|
|
349
|
+
|
|
350
|
+
# Add current message
|
|
351
|
+
messages.append({"role": "user", "content": user_message})
|
|
352
|
+
|
|
353
|
+
return self.chat(messages, temperature=0.7)
|
|
354
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Memory-LLM Configuration File
|
|
2
|
+
# Copy this file to config.yaml and edit as needed
|
|
3
|
+
|
|
4
|
+
# Usage Mode: "personal" or "business"
|
|
5
|
+
usage_mode: "personal"
|
|
6
|
+
|
|
7
|
+
# LLM Settings
|
|
8
|
+
llm:
|
|
9
|
+
model: "granite4:3b"
|
|
10
|
+
base_url: "http://localhost:11434"
|
|
11
|
+
temperature: 0.7
|
|
12
|
+
max_tokens: 500
|
|
13
|
+
|
|
14
|
+
# Memory Settings
|
|
15
|
+
memory:
|
|
16
|
+
backend: "json" # "json" or "sql"
|
|
17
|
+
json_dir: "memories"
|
|
18
|
+
db_path: "memories.db"
|
|
19
|
+
|
|
20
|
+
# System Prompt Template
|
|
21
|
+
prompt:
|
|
22
|
+
template: "personal_assistant"
|
|
23
|
+
variables:
|
|
24
|
+
user_name: "User"
|
|
25
|
+
tone: "friendly"
|
|
26
|
+
|
|
27
|
+
# Knowledge Base
|
|
28
|
+
knowledge_base:
|
|
29
|
+
enabled: true
|
|
30
|
+
auto_load: true
|
|
31
|
+
default_kb: "ecommerce"
|
|
32
|
+
search_limit: 5
|
|
33
|
+
|
|
34
|
+
# Response Settings
|
|
35
|
+
response:
|
|
36
|
+
use_knowledge_base: true
|
|
37
|
+
use_memory: true
|
|
38
|
+
recent_conversations_limit: 5
|
|
39
|
+
|
|
40
|
+
# Logging
|
|
41
|
+
logging:
|
|
42
|
+
enabled: true
|
|
43
|
+
level: "INFO"
|
|
44
|
+
file: "mem_agent.log"
|
|
45
|
+
|
|
46
|
+
# Security
|
|
47
|
+
security:
|
|
48
|
+
filter_sensitive_data: true
|
|
49
|
+
rate_limit:
|
|
50
|
+
enabled: true
|
|
51
|
+
max_requests_per_minute: 60
|
|
52
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config Generator from Documents (PDF, DOCX, TXT)
|
|
3
|
+
Automatically creates config.yaml from business documents
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_text_from_file(file_path: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Extract text from PDF, DOCX, or TXT files
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_path: Path to document
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Extracted text
|
|
20
|
+
"""
|
|
21
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
|
22
|
+
|
|
23
|
+
if file_ext == '.txt':
|
|
24
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
25
|
+
return f.read()
|
|
26
|
+
|
|
27
|
+
elif file_ext == '.pdf':
|
|
28
|
+
try:
|
|
29
|
+
import PyPDF2
|
|
30
|
+
text = []
|
|
31
|
+
with open(file_path, 'rb') as f:
|
|
32
|
+
reader = PyPDF2.PdfReader(f)
|
|
33
|
+
for page in reader.pages:
|
|
34
|
+
text.append(page.extract_text())
|
|
35
|
+
return '\n'.join(text)
|
|
36
|
+
except ImportError:
|
|
37
|
+
return "⚠️ PyPDF2 not installed. Run: pip install PyPDF2"
|
|
38
|
+
|
|
39
|
+
elif file_ext in ['.docx', '.doc']:
|
|
40
|
+
try:
|
|
41
|
+
import docx
|
|
42
|
+
doc = docx.Document(file_path)
|
|
43
|
+
text = []
|
|
44
|
+
for paragraph in doc.paragraphs:
|
|
45
|
+
text.append(paragraph.text)
|
|
46
|
+
return '\n'.join(text)
|
|
47
|
+
except ImportError:
|
|
48
|
+
return "⚠️ python-docx not installed. Run: pip install python-docx"
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
return f"⚠️ Unsupported file format: {file_ext}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_config_from_text(text: str, company_name: Optional[str] = None) -> Dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Generate config.yaml structure from text
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
text: Extracted text from document
|
|
60
|
+
company_name: Company name (optional)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Config dictionary
|
|
64
|
+
"""
|
|
65
|
+
# Simple config template
|
|
66
|
+
config = {
|
|
67
|
+
"usage_mode": "business", # or "personal"
|
|
68
|
+
|
|
69
|
+
"llm": {
|
|
70
|
+
"model": "granite4:3b",
|
|
71
|
+
"temperature": 0.3,
|
|
72
|
+
"max_tokens": 300,
|
|
73
|
+
"ollama_url": "http://localhost:11434"
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
"memory": {
|
|
77
|
+
"use_sql": True,
|
|
78
|
+
"db_path": "memories.db",
|
|
79
|
+
"json_dir": "memories"
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
"response": {
|
|
83
|
+
"use_knowledge_base": True,
|
|
84
|
+
"recent_conversations_limit": 5
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
"business": {
|
|
88
|
+
"company_name": company_name or "Your Company",
|
|
89
|
+
"industry": "Technology",
|
|
90
|
+
"founded_year": "2024"
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
"knowledge_base": {
|
|
94
|
+
"auto_load": True,
|
|
95
|
+
"search_limit": 5
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"logging": {
|
|
99
|
+
"level": "INFO",
|
|
100
|
+
"file": "mem_agent.log"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Try to extract company name from text if not provided
|
|
105
|
+
if not company_name:
|
|
106
|
+
lines = text.split('\n')[:10] # First 10 lines
|
|
107
|
+
for line in lines:
|
|
108
|
+
if any(keyword in line.lower() for keyword in ['company', 'corp', 'inc', 'ltd']):
|
|
109
|
+
config["business"]["company_name"] = line.strip()[:50]
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
return config
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_config_from_document(
|
|
116
|
+
doc_path: str,
|
|
117
|
+
output_path: str = "config.yaml",
|
|
118
|
+
company_name: Optional[str] = None
|
|
119
|
+
) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Create config.yaml from a business document
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
doc_path: Path to PDF/DOCX/TXT document
|
|
125
|
+
output_path: Output config.yaml path
|
|
126
|
+
company_name: Company name (optional)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Success message
|
|
130
|
+
"""
|
|
131
|
+
if not os.path.exists(doc_path):
|
|
132
|
+
return f"❌ File not found: {doc_path}"
|
|
133
|
+
|
|
134
|
+
# Extract text
|
|
135
|
+
print(f"📄 Reading document: {doc_path}")
|
|
136
|
+
text = extract_text_from_file(doc_path)
|
|
137
|
+
|
|
138
|
+
if text.startswith("⚠️"):
|
|
139
|
+
return text # Error message
|
|
140
|
+
|
|
141
|
+
print(f"✅ Extracted {len(text)} characters")
|
|
142
|
+
|
|
143
|
+
# Generate config
|
|
144
|
+
config = generate_config_from_text(text, company_name)
|
|
145
|
+
|
|
146
|
+
# Save to YAML
|
|
147
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
148
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
|
149
|
+
|
|
150
|
+
print(f"✅ Config created: {output_path}")
|
|
151
|
+
print(f"📌 Company: {config['business']['company_name']}")
|
|
152
|
+
|
|
153
|
+
return f"✅ Config successfully created at {output_path}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Simple CLI
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
import sys
|
|
159
|
+
|
|
160
|
+
if len(sys.argv) < 2:
|
|
161
|
+
print("""
|
|
162
|
+
🔧 Config Generator from Documents
|
|
163
|
+
|
|
164
|
+
Usage:
|
|
165
|
+
python -m mem_llm.config_from_docs <document_path> [output_path] [company_name]
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
python -m mem_llm.config_from_docs company_info.pdf
|
|
169
|
+
python -m mem_llm.config_from_docs business.docx my_config.yaml "Acme Corp"
|
|
170
|
+
python -m mem_llm.config_from_docs info.txt
|
|
171
|
+
""")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
doc_path = sys.argv[1]
|
|
175
|
+
output_path = sys.argv[2] if len(sys.argv) > 2 else "config.yaml"
|
|
176
|
+
company_name = sys.argv[3] if len(sys.argv) > 3 else None
|
|
177
|
+
|
|
178
|
+
result = create_config_from_document(doc_path, output_path, company_name)
|
|
179
|
+
print(result)
|
|
180
|
+
|