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.
@@ -0,0 +1,372 @@
1
+ """JSON-Based Chat History Manager.
2
+
3
+ This module provides a ChatHistoryComponent implementation that stores
4
+ chat history in a JSON file. Suitable for development, testing, and
5
+ single-instance deployments.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from rakam_systems_core.ai_core.interfaces.chat_history import \
15
+ ChatHistoryComponent
16
+
17
+ # Optional pydantic-ai integration
18
+ try:
19
+ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
20
+ from pydantic_core import to_jsonable_python
21
+ PYDANTIC_AI_AVAILABLE = True
22
+ except ImportError:
23
+ PYDANTIC_AI_AVAILABLE = False
24
+ ModelMessagesTypeAdapter = None # type: ignore
25
+ ModelMessage = None # type: ignore
26
+ to_jsonable_python = None # type: ignore
27
+
28
+
29
+ class JSONChatHistory(ChatHistoryComponent):
30
+ """Chat history manager using JSON file storage.
31
+
32
+ This implementation stores all chat histories in a single JSON file.
33
+ It's suitable for:
34
+ - Development and testing
35
+ - Single-instance deployments
36
+ - Small to medium scale applications
37
+
38
+ For production with multiple instances, consider using a database-backed
39
+ implementation instead.
40
+
41
+ Config options:
42
+ storage_path: Path to the JSON file (default: "./chat_history.json")
43
+ auto_save: Whether to save after each modification (default: True)
44
+ indent: JSON indentation for readability (default: 4)
45
+
46
+ Example:
47
+ >>> history = JSONChatHistory(config={"storage_path": "./data/chats.json"})
48
+ >>> history.add_message("chat123", {"role": "user", "content": "Hello"})
49
+ >>> history.add_message("chat123", {"role": "assistant", "content": "Hi there!"})
50
+ >>> messages = history.get_chat_history("chat123")
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ name: str = "json_chat_history",
56
+ config: Optional[Dict[str, Any]] = None,
57
+ storage_path: Optional[str] = None,
58
+ ) -> None:
59
+ """Initialize the JSON chat history manager.
60
+
61
+ Args:
62
+ name: Component name for identification.
63
+ config: Configuration dictionary. Supports:
64
+ - storage_path: Path to JSON file
65
+ - auto_save: Save after each modification (default: True)
66
+ - indent: JSON indentation (default: 4)
67
+ storage_path: Direct path override (takes precedence over config).
68
+ """
69
+ super().__init__(name, config)
70
+
71
+ # Get storage path from argument, config, or default
72
+ self.storage_path = storage_path or self.config.get(
73
+ "storage_path", "./chat_history.json"
74
+ )
75
+ self.auto_save = self.config.get("auto_save", True)
76
+ self.indent = self.config.get("indent", 4)
77
+
78
+ # In-memory cache of chat history
79
+ self._chat_history: Dict[str, List[Dict[str, Any]]] = {}
80
+
81
+ def setup(self) -> None:
82
+ """Initialize storage and load existing history."""
83
+ self._initialize_storage()
84
+ super().setup()
85
+
86
+ def shutdown(self) -> None:
87
+ """Save and cleanup resources."""
88
+ self._save()
89
+ super().shutdown()
90
+
91
+ def _initialize_storage(self) -> None:
92
+ """Initialize storage: create directory and load existing data."""
93
+ # Ensure directory exists
94
+ storage_dir = os.path.dirname(self.storage_path)
95
+ if storage_dir:
96
+ os.makedirs(storage_dir, exist_ok=True)
97
+
98
+ # Load existing data or create new file
99
+ if os.path.exists(self.storage_path):
100
+ try:
101
+ with open(self.storage_path, 'r', encoding='utf-8') as f:
102
+ content = f.read().strip()
103
+ if content:
104
+ self._chat_history = json.loads(content)
105
+ else:
106
+ self._chat_history = {}
107
+ except (json.JSONDecodeError, IOError):
108
+ # Invalid JSON or IO error - start fresh
109
+ self._chat_history = {}
110
+ self._save()
111
+ else:
112
+ # Create new file
113
+ self._chat_history = {}
114
+ self._save()
115
+
116
+ def _save(self) -> None:
117
+ """Save current chat history to JSON file."""
118
+ try:
119
+ with open(self.storage_path, 'w', encoding='utf-8') as f:
120
+ json.dump(self._chat_history, f,
121
+ indent=self.indent, ensure_ascii=False)
122
+ except IOError as e:
123
+ raise IOError(
124
+ f"Failed to save chat history to {self.storage_path}: {e}")
125
+
126
+ def _ensure_initialized(self) -> None:
127
+ """Ensure the component is initialized before operations."""
128
+ if not self.initialized:
129
+ self.setup()
130
+
131
+ def add_message(self, chat_id: str, message: Dict[str, Any]) -> None:
132
+ """Add a single message to a chat session.
133
+
134
+ Args:
135
+ chat_id: Unique identifier for the chat session.
136
+ message: Message object (dict with role, content, timestamp, etc.).
137
+ """
138
+ self._ensure_initialized()
139
+
140
+ if chat_id not in self._chat_history:
141
+ self._chat_history[chat_id] = []
142
+
143
+ self._chat_history[chat_id].append(message)
144
+
145
+ if self.auto_save:
146
+ self._save()
147
+
148
+ def set_messages(self, chat_id: str, messages: List[Dict[str, Any]]) -> None:
149
+ """Set/replace all messages for a chat session.
150
+
151
+ Args:
152
+ chat_id: Unique identifier for the chat session.
153
+ messages: List of message objects to store.
154
+ """
155
+ self._ensure_initialized()
156
+ # Copy to avoid reference issues
157
+ self._chat_history[chat_id] = list(messages)
158
+
159
+ if self.auto_save:
160
+ self._save()
161
+
162
+ def get_chat_history(self, chat_id: str) -> List[Dict[str, Any]]:
163
+ """Retrieve all messages for a chat session.
164
+
165
+ Args:
166
+ chat_id: Unique identifier for the chat session.
167
+
168
+ Returns:
169
+ List of message objects, or empty list if chat doesn't exist.
170
+ """
171
+ self._ensure_initialized()
172
+ return self._chat_history.get(chat_id, [])
173
+
174
+ def get_all_chat_ids(self) -> List[str]:
175
+ """Get all chat IDs currently stored.
176
+
177
+ Returns:
178
+ List of all chat session identifiers.
179
+ """
180
+ self._ensure_initialized()
181
+ return list(self._chat_history.keys())
182
+
183
+ def delete_chat_history(self, chat_id: str) -> bool:
184
+ """Delete all messages for a chat session.
185
+
186
+ Args:
187
+ chat_id: Unique identifier for the chat session to delete.
188
+
189
+ Returns:
190
+ True if deletion was successful, False if chat_id didn't exist.
191
+ """
192
+ self._ensure_initialized()
193
+
194
+ if chat_id not in self._chat_history:
195
+ return False
196
+
197
+ del self._chat_history[chat_id]
198
+
199
+ if self.auto_save:
200
+ self._save()
201
+
202
+ return True
203
+
204
+ def clear_all(self) -> None:
205
+ """Delete all chat histories."""
206
+ self._ensure_initialized()
207
+ self._chat_history = {}
208
+
209
+ if self.auto_save:
210
+ self._save()
211
+
212
+ def get_readable_chat_history(
213
+ self,
214
+ chat_id: str,
215
+ user_role: str = "user",
216
+ assistant_role: str = "assistant",
217
+ ) -> List[Dict[str, Any]]:
218
+ """Get chat history in a human-readable format.
219
+
220
+ This method transforms the raw message format into a display-friendly
221
+ format with 'from', 'message', and optional 'timestamp' keys.
222
+
223
+ Args:
224
+ chat_id: Unique identifier for the chat session.
225
+ user_role: The role name for user messages (default: "user").
226
+ assistant_role: The role name for assistant messages (default: "assistant").
227
+
228
+ Returns:
229
+ List of formatted message dictionaries with:
230
+ - 'from': "user" or "assistant"
231
+ - 'message': The message content
232
+ - 'timestamp': Message timestamp (if available)
233
+ """
234
+ self._ensure_initialized()
235
+
236
+ messages = self.get_chat_history(chat_id)
237
+ readable_messages = []
238
+
239
+ for msg in messages:
240
+ role = msg.get("role", "")
241
+ content = msg.get("content", "")
242
+ timestamp = msg.get("timestamp")
243
+
244
+ # Determine the 'from' field based on role
245
+ if role == user_role:
246
+ from_field = "user"
247
+ elif role == assistant_role:
248
+ from_field = "assistant"
249
+ else:
250
+ # Skip system messages or unknown roles
251
+ continue
252
+
253
+ formatted = {
254
+ "from": from_field,
255
+ "message": content,
256
+ }
257
+
258
+ if timestamp:
259
+ formatted["timestamp"] = timestamp
260
+
261
+ readable_messages.append(formatted)
262
+
263
+ return readable_messages
264
+
265
+ def save(self) -> None:
266
+ """Manually save the current state to disk.
267
+
268
+ Useful when auto_save is disabled.
269
+ """
270
+ self._ensure_initialized()
271
+ self._save()
272
+
273
+ def reload(self) -> None:
274
+ """Reload chat history from disk, discarding in-memory changes."""
275
+ self._initialize_storage()
276
+
277
+ # ==================== Pydantic-AI Integration ====================
278
+
279
+ def get_message_history(self, chat_id: str) -> Optional[List[Any]]:
280
+ """Get chat history in pydantic-ai compatible format.
281
+
282
+ This method converts the stored JSON history to pydantic-ai's
283
+ ModelMessage format, ready to be passed to agent.run() or
284
+ agent.run_stream() as message_history.
285
+
286
+ Args:
287
+ chat_id: Unique identifier for the chat session.
288
+
289
+ Returns:
290
+ List of ModelMessage objects for pydantic-ai, or None if:
291
+ - Chat doesn't exist or is empty
292
+ - pydantic-ai is not installed
293
+
294
+ Example:
295
+ >>> history = JSONChatHistory()
296
+ >>> message_history = history.get_message_history("chat123")
297
+ >>> result = await agent.run("Hello", message_history=message_history)
298
+ """
299
+ if not PYDANTIC_AI_AVAILABLE:
300
+ raise ImportError(
301
+ "pydantic-ai is not installed. Install it with: pip install pydantic-ai"
302
+ )
303
+
304
+ self._ensure_initialized()
305
+ raw_history = self._chat_history.get(chat_id, [])
306
+
307
+ if not raw_history:
308
+ return None
309
+
310
+ return ModelMessagesTypeAdapter.validate_python(raw_history)
311
+
312
+ def save_messages(self, chat_id: str, messages: List[Any]) -> None:
313
+ """Save pydantic-ai messages to history.
314
+
315
+ This method converts pydantic-ai's ModelMessage objects to JSON
316
+ and stores them. Typically called with result.all_messages() after
317
+ an agent run.
318
+
319
+ Args:
320
+ chat_id: Unique identifier for the chat session.
321
+ messages: List of pydantic-ai ModelMessage objects
322
+ (e.g., from result.all_messages()).
323
+
324
+ Example:
325
+ >>> result = await agent.run("Hello", message_history=history.get_message_history("chat123"))
326
+ >>> history.save_messages("chat123", result.all_messages())
327
+ """
328
+ if not PYDANTIC_AI_AVAILABLE:
329
+ raise ImportError(
330
+ "pydantic-ai is not installed. Install it with: pip install pydantic-ai"
331
+ )
332
+
333
+ self._ensure_initialized()
334
+
335
+ # Convert pydantic-ai messages to JSON-serializable format
336
+ json_messages = to_jsonable_python(messages)
337
+ self._chat_history[chat_id] = json_messages
338
+
339
+ if self.auto_save:
340
+ self._save()
341
+
342
+
343
+ if __name__ == "__main__":
344
+ # Example usage
345
+ history = JSONChatHistory(
346
+ config={"storage_path": "./test_chat_history.json"}
347
+ )
348
+
349
+ # Add messages
350
+ history.add_message("chat123", {
351
+ "role": "user",
352
+ "content": "Hello!",
353
+ "timestamp": "2025-03-18 10:00:00"
354
+ })
355
+ history.add_message("chat123", {
356
+ "role": "assistant",
357
+ "content": "Hi there! How can I help?",
358
+ "timestamp": "2025-03-18 10:00:05"
359
+ })
360
+
361
+ # Retrieve history
362
+ print("Chat history:", history.get_chat_history("chat123"))
363
+ print("All chat IDs:", history.get_all_chat_ids())
364
+ print("Readable format:", history.get_readable_chat_history("chat123"))
365
+
366
+ # Clean up
367
+ history.delete_chat_history("chat123")
368
+ print("After deletion:", history.get_all_chat_ids())
369
+
370
+ # Remove test file
371
+ import os
372
+ os.remove("./test_chat_history.json")