rakam-systems-agent 0.1.1rc7__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.
- rakam_systems_agent/__init__.py +35 -0
- rakam_systems_agent/components/__init__.py +26 -0
- rakam_systems_agent/components/base_agent.py +358 -0
- rakam_systems_agent/components/chat_history/__init__.py +10 -0
- rakam_systems_agent/components/chat_history/json_chat_history.py +372 -0
- rakam_systems_agent/components/chat_history/postgres_chat_history.py +668 -0
- rakam_systems_agent/components/chat_history/sql_chat_history.py +446 -0
- rakam_systems_agent/components/llm_gateway/README.md +505 -0
- rakam_systems_agent/components/llm_gateway/__init__.py +16 -0
- rakam_systems_agent/components/llm_gateway/gateway_factory.py +313 -0
- rakam_systems_agent/components/llm_gateway/mistral_gateway.py +287 -0
- rakam_systems_agent/components/llm_gateway/openai_gateway.py +295 -0
- rakam_systems_agent/components/tools/LLM_GATEWAY_TOOLS_README.md +533 -0
- rakam_systems_agent/components/tools/__init__.py +46 -0
- rakam_systems_agent/components/tools/example_tools.py +431 -0
- rakam_systems_agent/components/tools/llm_gateway_tools.py +605 -0
- rakam_systems_agent/components/tools/search_tool.py +14 -0
- rakam_systems_agent/server/README.md +375 -0
- rakam_systems_agent/server/__init__.py +12 -0
- rakam_systems_agent/server/mcp_server_agent.py +127 -0
- rakam_systems_agent-0.1.1rc7.dist-info/METADATA +367 -0
- rakam_systems_agent-0.1.1rc7.dist-info/RECORD +23 -0
- rakam_systems_agent-0.1.1rc7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""SQL-Based Chat History Manager.
|
|
2
|
+
|
|
3
|
+
This module provides a ChatHistoryComponent implementation that stores
|
|
4
|
+
chat history in a SQLite database. Suitable for production deployments
|
|
5
|
+
requiring persistent, structured storage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from rakam_systems_core.ai_core.interfaces.chat_history import ChatHistoryComponent
|
|
15
|
+
|
|
16
|
+
# Optional pydantic-ai integration
|
|
17
|
+
try:
|
|
18
|
+
from pydantic_ai.messages import ModelMessagesTypeAdapter, ModelMessage
|
|
19
|
+
from pydantic_core import to_jsonable_python
|
|
20
|
+
PYDANTIC_AI_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
PYDANTIC_AI_AVAILABLE = False
|
|
23
|
+
ModelMessagesTypeAdapter = None # type: ignore
|
|
24
|
+
ModelMessage = None # type: ignore
|
|
25
|
+
to_jsonable_python = None # type: ignore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SQLChatHistory(ChatHistoryComponent):
|
|
29
|
+
"""Chat history manager using SQLite database storage.
|
|
30
|
+
|
|
31
|
+
This implementation stores all chat histories in a SQLite database.
|
|
32
|
+
It's suitable for:
|
|
33
|
+
- Production deployments
|
|
34
|
+
- Multi-instance applications (with proper connection handling)
|
|
35
|
+
- Applications requiring structured queries
|
|
36
|
+
- Medium to large scale applications
|
|
37
|
+
|
|
38
|
+
Config options:
|
|
39
|
+
db_path: Path to the SQLite database file (default: "./chat_history.db")
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> history = SQLChatHistory(config={"db_path": "./data/chats.db"})
|
|
43
|
+
>>> history.add_message("chat123", {"role": "user", "content": "Hello"})
|
|
44
|
+
>>> history.add_message("chat123", {"role": "assistant", "content": "Hi there!"})
|
|
45
|
+
>>> messages = history.get_chat_history("chat123")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
name: str = "sql_chat_history",
|
|
51
|
+
config: Optional[Dict[str, Any]] = None,
|
|
52
|
+
db_path: Optional[str] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize the SQL chat history manager.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: Component name for identification.
|
|
58
|
+
config: Configuration dictionary. Supports:
|
|
59
|
+
- db_path: Path to SQLite database file
|
|
60
|
+
db_path: Direct path override (takes precedence over config).
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(name, config)
|
|
63
|
+
|
|
64
|
+
# Get db path from argument, config, or default
|
|
65
|
+
self.db_path = db_path or self.config.get(
|
|
66
|
+
"db_path", "./chat_history.db"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def setup(self) -> None:
|
|
70
|
+
"""Initialize database and create tables."""
|
|
71
|
+
self._initialize_database()
|
|
72
|
+
super().setup()
|
|
73
|
+
|
|
74
|
+
def shutdown(self) -> None:
|
|
75
|
+
"""Cleanup resources."""
|
|
76
|
+
super().shutdown()
|
|
77
|
+
|
|
78
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
79
|
+
"""Get a database connection with foreign keys enabled.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
SQLite connection object.
|
|
83
|
+
"""
|
|
84
|
+
conn = sqlite3.connect(self.db_path)
|
|
85
|
+
conn.execute('PRAGMA foreign_keys = ON;')
|
|
86
|
+
return conn
|
|
87
|
+
|
|
88
|
+
def _initialize_database(self) -> None:
|
|
89
|
+
"""Initialize SQLite database and create necessary tables.
|
|
90
|
+
|
|
91
|
+
Creates the chats and messages tables if they don't exist.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
Exception: If database initialization fails.
|
|
95
|
+
"""
|
|
96
|
+
# Ensure directory exists
|
|
97
|
+
db_dir = os.path.dirname(self.db_path)
|
|
98
|
+
if db_dir:
|
|
99
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
with self._get_connection() as conn:
|
|
102
|
+
cursor = conn.cursor()
|
|
103
|
+
|
|
104
|
+
# Create tables
|
|
105
|
+
cursor.execute('''
|
|
106
|
+
CREATE TABLE IF NOT EXISTS chats (
|
|
107
|
+
chat_id TEXT PRIMARY KEY
|
|
108
|
+
)
|
|
109
|
+
''')
|
|
110
|
+
|
|
111
|
+
cursor.execute('''
|
|
112
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
+
chat_id TEXT NOT NULL,
|
|
115
|
+
message_order INTEGER NOT NULL,
|
|
116
|
+
message_data TEXT NOT NULL,
|
|
117
|
+
FOREIGN KEY (chat_id) REFERENCES chats (chat_id) ON DELETE CASCADE
|
|
118
|
+
)
|
|
119
|
+
''')
|
|
120
|
+
|
|
121
|
+
# Create index for faster lookups
|
|
122
|
+
cursor.execute('''
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_messages_chat_id
|
|
124
|
+
ON messages (chat_id, message_order)
|
|
125
|
+
''')
|
|
126
|
+
|
|
127
|
+
conn.commit()
|
|
128
|
+
|
|
129
|
+
def _ensure_initialized(self) -> None:
|
|
130
|
+
"""Ensure the component is initialized before operations."""
|
|
131
|
+
if not self.initialized:
|
|
132
|
+
self.setup()
|
|
133
|
+
|
|
134
|
+
def add_message(self, chat_id: str, message: Dict[str, Any]) -> None:
|
|
135
|
+
"""Add a single message to a chat session.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
chat_id: Unique identifier for the chat session.
|
|
139
|
+
message: Message object (dict with role, content, timestamp, etc.).
|
|
140
|
+
"""
|
|
141
|
+
self._ensure_initialized()
|
|
142
|
+
|
|
143
|
+
with self._get_connection() as conn:
|
|
144
|
+
cursor = conn.cursor()
|
|
145
|
+
|
|
146
|
+
# Ensure chat exists
|
|
147
|
+
cursor.execute(
|
|
148
|
+
'INSERT OR IGNORE INTO chats (chat_id) VALUES (?)',
|
|
149
|
+
(chat_id,)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Get next message order
|
|
153
|
+
cursor.execute(
|
|
154
|
+
'SELECT COALESCE(MAX(message_order), -1) + 1 FROM messages WHERE chat_id = ?',
|
|
155
|
+
(chat_id,)
|
|
156
|
+
)
|
|
157
|
+
next_order = cursor.fetchone()[0]
|
|
158
|
+
|
|
159
|
+
# Insert message
|
|
160
|
+
message_json = json.dumps(message, ensure_ascii=False)
|
|
161
|
+
cursor.execute(
|
|
162
|
+
'''
|
|
163
|
+
INSERT INTO messages (chat_id, message_order, message_data)
|
|
164
|
+
VALUES (?, ?, ?)
|
|
165
|
+
''',
|
|
166
|
+
(chat_id, next_order, message_json)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
conn.commit()
|
|
170
|
+
|
|
171
|
+
def set_messages(self, chat_id: str, messages: List[Dict[str, Any]]) -> None:
|
|
172
|
+
"""Set/replace all messages for a chat session.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
chat_id: Unique identifier for the chat session.
|
|
176
|
+
messages: List of message objects to store.
|
|
177
|
+
"""
|
|
178
|
+
self._ensure_initialized()
|
|
179
|
+
|
|
180
|
+
with self._get_connection() as conn:
|
|
181
|
+
cursor = conn.cursor()
|
|
182
|
+
|
|
183
|
+
# Ensure chat exists
|
|
184
|
+
cursor.execute(
|
|
185
|
+
'INSERT OR IGNORE INTO chats (chat_id) VALUES (?)',
|
|
186
|
+
(chat_id,)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Delete existing messages
|
|
190
|
+
cursor.execute(
|
|
191
|
+
'DELETE FROM messages WHERE chat_id = ?', (chat_id,))
|
|
192
|
+
|
|
193
|
+
# Insert new messages with order
|
|
194
|
+
for order, message in enumerate(messages):
|
|
195
|
+
message_json = json.dumps(message, ensure_ascii=False)
|
|
196
|
+
cursor.execute(
|
|
197
|
+
'''
|
|
198
|
+
INSERT INTO messages (chat_id, message_order, message_data)
|
|
199
|
+
VALUES (?, ?, ?)
|
|
200
|
+
''',
|
|
201
|
+
(chat_id, order, message_json)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
conn.commit()
|
|
205
|
+
|
|
206
|
+
def get_chat_history(self, chat_id: str) -> List[Dict[str, Any]]:
|
|
207
|
+
"""Retrieve all messages for a chat session.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
chat_id: Unique identifier for the chat session.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of message objects, or empty list if chat doesn't exist.
|
|
214
|
+
"""
|
|
215
|
+
self._ensure_initialized()
|
|
216
|
+
|
|
217
|
+
with self._get_connection() as conn:
|
|
218
|
+
cursor = conn.cursor()
|
|
219
|
+
cursor.execute(
|
|
220
|
+
'''
|
|
221
|
+
SELECT message_data
|
|
222
|
+
FROM messages
|
|
223
|
+
WHERE chat_id = ?
|
|
224
|
+
ORDER BY message_order ASC
|
|
225
|
+
''',
|
|
226
|
+
(chat_id,)
|
|
227
|
+
)
|
|
228
|
+
rows = cursor.fetchall()
|
|
229
|
+
|
|
230
|
+
return [json.loads(row[0]) for row in rows]
|
|
231
|
+
|
|
232
|
+
def get_all_chat_ids(self) -> List[str]:
|
|
233
|
+
"""Get all chat IDs currently stored.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of all chat session identifiers.
|
|
237
|
+
"""
|
|
238
|
+
self._ensure_initialized()
|
|
239
|
+
|
|
240
|
+
with self._get_connection() as conn:
|
|
241
|
+
cursor = conn.cursor()
|
|
242
|
+
cursor.execute('SELECT chat_id FROM chats')
|
|
243
|
+
return [row[0] for row in cursor.fetchall()]
|
|
244
|
+
|
|
245
|
+
def delete_chat_history(self, chat_id: str) -> bool:
|
|
246
|
+
"""Delete all messages for a chat session.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
chat_id: Unique identifier for the chat session to delete.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if deletion was successful, False if chat_id didn't exist.
|
|
253
|
+
"""
|
|
254
|
+
self._ensure_initialized()
|
|
255
|
+
|
|
256
|
+
with self._get_connection() as conn:
|
|
257
|
+
cursor = conn.cursor()
|
|
258
|
+
|
|
259
|
+
# Check if chat exists
|
|
260
|
+
cursor.execute('SELECT 1 FROM chats WHERE chat_id = ?', (chat_id,))
|
|
261
|
+
exists = cursor.fetchone() is not None
|
|
262
|
+
|
|
263
|
+
if not exists:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
# Delete messages (cascades from foreign key, but explicit is safer)
|
|
267
|
+
cursor.execute(
|
|
268
|
+
'DELETE FROM messages WHERE chat_id = ?', (chat_id,))
|
|
269
|
+
cursor.execute('DELETE FROM chats WHERE chat_id = ?', (chat_id,))
|
|
270
|
+
|
|
271
|
+
conn.commit()
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
def clear_all(self) -> None:
|
|
275
|
+
"""Delete all chat histories."""
|
|
276
|
+
self._ensure_initialized()
|
|
277
|
+
|
|
278
|
+
with self._get_connection() as conn:
|
|
279
|
+
cursor = conn.cursor()
|
|
280
|
+
cursor.execute('DELETE FROM messages')
|
|
281
|
+
cursor.execute('DELETE FROM chats')
|
|
282
|
+
conn.commit()
|
|
283
|
+
|
|
284
|
+
def get_readable_chat_history(
|
|
285
|
+
self,
|
|
286
|
+
chat_id: str,
|
|
287
|
+
user_role: str = "user",
|
|
288
|
+
assistant_role: str = "assistant",
|
|
289
|
+
) -> List[Dict[str, Any]]:
|
|
290
|
+
"""Get chat history in a human-readable format.
|
|
291
|
+
|
|
292
|
+
This method transforms the raw message format into a display-friendly
|
|
293
|
+
format with 'from', 'message', and optional 'timestamp' keys.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
chat_id: Unique identifier for the chat session.
|
|
297
|
+
user_role: The role name for user messages (default: "user").
|
|
298
|
+
assistant_role: The role name for assistant messages (default: "assistant").
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of formatted message dictionaries with:
|
|
302
|
+
- 'from': "user" or "assistant"
|
|
303
|
+
- 'message': The message content
|
|
304
|
+
- 'timestamp': Message timestamp (if available)
|
|
305
|
+
"""
|
|
306
|
+
self._ensure_initialized()
|
|
307
|
+
|
|
308
|
+
messages = self.get_chat_history(chat_id)
|
|
309
|
+
readable_messages = []
|
|
310
|
+
|
|
311
|
+
for msg in messages:
|
|
312
|
+
role = msg.get("role", "")
|
|
313
|
+
content = msg.get("content", "")
|
|
314
|
+
timestamp = msg.get("timestamp")
|
|
315
|
+
|
|
316
|
+
# Determine the 'from' field based on role
|
|
317
|
+
if role == user_role:
|
|
318
|
+
from_field = "user"
|
|
319
|
+
elif role == assistant_role:
|
|
320
|
+
from_field = "assistant"
|
|
321
|
+
else:
|
|
322
|
+
# Skip system messages or unknown roles
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
formatted = {
|
|
326
|
+
"from": from_field,
|
|
327
|
+
"message": content,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if timestamp:
|
|
331
|
+
formatted["timestamp"] = timestamp
|
|
332
|
+
|
|
333
|
+
readable_messages.append(formatted)
|
|
334
|
+
|
|
335
|
+
return readable_messages
|
|
336
|
+
|
|
337
|
+
# ==================== Pydantic-AI Integration ====================
|
|
338
|
+
|
|
339
|
+
def get_message_history(self, chat_id: str) -> Optional[List[Any]]:
|
|
340
|
+
"""Get chat history in pydantic-ai compatible format.
|
|
341
|
+
|
|
342
|
+
This method converts the stored database history to pydantic-ai's
|
|
343
|
+
ModelMessage format, ready to be passed to agent.run() or
|
|
344
|
+
agent.run_stream() as message_history.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
chat_id: Unique identifier for the chat session.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of ModelMessage objects for pydantic-ai, or None if:
|
|
351
|
+
- Chat doesn't exist or is empty
|
|
352
|
+
- pydantic-ai is not installed
|
|
353
|
+
|
|
354
|
+
Example:
|
|
355
|
+
>>> history = SQLChatHistory()
|
|
356
|
+
>>> message_history = history.get_message_history("chat123")
|
|
357
|
+
>>> result = await agent.run("Hello", message_history=message_history)
|
|
358
|
+
"""
|
|
359
|
+
if not PYDANTIC_AI_AVAILABLE:
|
|
360
|
+
raise ImportError(
|
|
361
|
+
"pydantic-ai is not installed. Install it with: pip install pydantic-ai"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
self._ensure_initialized()
|
|
365
|
+
raw_history = self.get_chat_history(chat_id)
|
|
366
|
+
|
|
367
|
+
if not raw_history:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
return ModelMessagesTypeAdapter.validate_python(raw_history)
|
|
371
|
+
|
|
372
|
+
def save_messages(self, chat_id: str, messages: List[Any]) -> None:
|
|
373
|
+
"""Save pydantic-ai messages to history.
|
|
374
|
+
|
|
375
|
+
This method converts pydantic-ai's ModelMessage objects to JSON
|
|
376
|
+
and stores them. Typically called with result.all_messages() after
|
|
377
|
+
an agent run.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
chat_id: Unique identifier for the chat session.
|
|
381
|
+
messages: List of pydantic-ai ModelMessage objects
|
|
382
|
+
(e.g., from result.all_messages()).
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> result = await agent.run("Hello", message_history=history.get_message_history("chat123"))
|
|
386
|
+
>>> history.save_messages("chat123", result.all_messages())
|
|
387
|
+
"""
|
|
388
|
+
if not PYDANTIC_AI_AVAILABLE:
|
|
389
|
+
raise ImportError(
|
|
390
|
+
"pydantic-ai is not installed. Install it with: pip install pydantic-ai"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
self._ensure_initialized()
|
|
394
|
+
|
|
395
|
+
# Convert pydantic-ai messages to JSON-serializable format
|
|
396
|
+
json_messages = to_jsonable_python(messages)
|
|
397
|
+
self.set_messages(chat_id, json_messages)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
import tempfile
|
|
402
|
+
|
|
403
|
+
# Create a temporary database for testing
|
|
404
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
405
|
+
db_path = os.path.join(tmpdir, "test_chat_history.db")
|
|
406
|
+
|
|
407
|
+
# Example usage
|
|
408
|
+
history = SQLChatHistory(
|
|
409
|
+
config={"db_path": db_path}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Add messages
|
|
413
|
+
history.add_message("chat123", {
|
|
414
|
+
"role": "user",
|
|
415
|
+
"content": "Hello!",
|
|
416
|
+
"timestamp": "2025-03-18 10:00:00"
|
|
417
|
+
})
|
|
418
|
+
history.add_message("chat123", {
|
|
419
|
+
"role": "assistant",
|
|
420
|
+
"content": "Hi there! How can I help?",
|
|
421
|
+
"timestamp": "2025-03-18 10:00:05"
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
# Retrieve history
|
|
425
|
+
print("Chat history:", history.get_chat_history("chat123"))
|
|
426
|
+
print("All chat IDs:", history.get_all_chat_ids())
|
|
427
|
+
print("Readable format:", history.get_readable_chat_history("chat123"))
|
|
428
|
+
|
|
429
|
+
# Test set_messages
|
|
430
|
+
history.set_messages("chat456", [
|
|
431
|
+
{"role": "user", "content": "Test message",
|
|
432
|
+
"timestamp": "2025-03-18 10:02:00"}
|
|
433
|
+
])
|
|
434
|
+
print("Chat history for chat456:", history.get_chat_history("chat456"))
|
|
435
|
+
print("All chat IDs after adding chat456:", history.get_all_chat_ids())
|
|
436
|
+
|
|
437
|
+
# Clean up
|
|
438
|
+
deleted = history.delete_chat_history("chat123")
|
|
439
|
+
print(f"Deleted chat123: {deleted}")
|
|
440
|
+
print("After deletion:", history.get_all_chat_ids())
|
|
441
|
+
|
|
442
|
+
# Clear all
|
|
443
|
+
history.clear_all()
|
|
444
|
+
print("After clear_all:", history.get_all_chat_ids())
|
|
445
|
+
|
|
446
|
+
print("\nAll tests passed!")
|