chuk-ai-session-manager 0.7.1__py3-none-any.whl → 0.8__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.
- chuk_ai_session_manager/__init__.py +84 -40
- chuk_ai_session_manager/api/__init__.py +1 -1
- chuk_ai_session_manager/api/simple_api.py +53 -59
- chuk_ai_session_manager/exceptions.py +31 -17
- chuk_ai_session_manager/guards/__init__.py +118 -0
- chuk_ai_session_manager/guards/bindings.py +217 -0
- chuk_ai_session_manager/guards/cache.py +163 -0
- chuk_ai_session_manager/guards/manager.py +819 -0
- chuk_ai_session_manager/guards/models.py +498 -0
- chuk_ai_session_manager/guards/ungrounded.py +159 -0
- chuk_ai_session_manager/infinite_conversation.py +86 -79
- chuk_ai_session_manager/memory/__init__.py +247 -0
- chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
- chuk_ai_session_manager/memory/context_packer.py +347 -0
- chuk_ai_session_manager/memory/fault_handler.py +507 -0
- chuk_ai_session_manager/memory/manifest.py +307 -0
- chuk_ai_session_manager/memory/models.py +1084 -0
- chuk_ai_session_manager/memory/mutation_log.py +186 -0
- chuk_ai_session_manager/memory/pack_cache.py +206 -0
- chuk_ai_session_manager/memory/page_table.py +275 -0
- chuk_ai_session_manager/memory/prefetcher.py +192 -0
- chuk_ai_session_manager/memory/tlb.py +247 -0
- chuk_ai_session_manager/memory/vm_prompts.py +238 -0
- chuk_ai_session_manager/memory/working_set.py +574 -0
- chuk_ai_session_manager/models/__init__.py +21 -9
- chuk_ai_session_manager/models/event_source.py +3 -1
- chuk_ai_session_manager/models/event_type.py +10 -1
- chuk_ai_session_manager/models/session.py +103 -68
- chuk_ai_session_manager/models/session_event.py +69 -68
- chuk_ai_session_manager/models/session_metadata.py +9 -10
- chuk_ai_session_manager/models/session_run.py +21 -22
- chuk_ai_session_manager/models/token_usage.py +76 -76
- chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
- chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
- chuk_ai_session_manager/procedural_memory/manager.py +523 -0
- chuk_ai_session_manager/procedural_memory/models.py +371 -0
- chuk_ai_session_manager/sample_tools.py +79 -46
- chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
- chuk_ai_session_manager/session_manager.py +238 -197
- chuk_ai_session_manager/session_prompt_builder.py +163 -111
- chuk_ai_session_manager/session_storage.py +45 -52
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/METADATA +79 -3
- chuk_ai_session_manager-0.8.dist-info/RECORD +45 -0
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/WHEEL +1 -1
- chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/top_level.txt +0 -0
|
@@ -14,7 +14,7 @@ This module provides the main SessionManager class which offers:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
-
from typing import Any, Dict, List, Optional, Callable
|
|
17
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
import uuid
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ from chuk_ai_session_manager.models.session import Session
|
|
|
22
22
|
from chuk_ai_session_manager.models.session_event import SessionEvent
|
|
23
23
|
from chuk_ai_session_manager.models.event_source import EventSource
|
|
24
24
|
from chuk_ai_session_manager.models.event_type import EventType
|
|
25
|
-
from chuk_ai_session_manager.session_storage import
|
|
25
|
+
from chuk_ai_session_manager.session_storage import ChukSessionsStore
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
@@ -30,10 +30,10 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
class SessionManager:
|
|
31
31
|
"""
|
|
32
32
|
High-level session manager for AI conversations.
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
Provides an easy-to-use interface for tracking conversations, managing
|
|
35
35
|
system prompts, handling infinite context, and monitoring usage.
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
Examples:
|
|
38
38
|
Basic usage:
|
|
39
39
|
```python
|
|
@@ -41,22 +41,22 @@ class SessionManager:
|
|
|
41
41
|
await sm.user_says("Hello!")
|
|
42
42
|
await sm.ai_responds("Hi there!", model="gpt-4")
|
|
43
43
|
```
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
With system prompt:
|
|
46
46
|
```python
|
|
47
47
|
sm = SessionManager(system_prompt="You are a helpful assistant.")
|
|
48
48
|
await sm.user_says("What can you do?")
|
|
49
49
|
```
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
Infinite context:
|
|
52
52
|
```python
|
|
53
53
|
sm = SessionManager(infinite_context=True, token_threshold=4000)
|
|
54
54
|
# Automatically handles long conversations
|
|
55
55
|
```
|
|
56
56
|
"""
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
def __init__(
|
|
59
|
-
self,
|
|
59
|
+
self,
|
|
60
60
|
session_id: Optional[str] = None,
|
|
61
61
|
system_prompt: Optional[str] = None,
|
|
62
62
|
parent_id: Optional[str] = None,
|
|
@@ -64,11 +64,11 @@ class SessionManager:
|
|
|
64
64
|
store: Optional[ChukSessionsStore] = None,
|
|
65
65
|
infinite_context: bool = False,
|
|
66
66
|
token_threshold: int = 4000,
|
|
67
|
-
max_turns_per_segment: int = 20
|
|
67
|
+
max_turns_per_segment: int = 20,
|
|
68
68
|
):
|
|
69
69
|
"""
|
|
70
70
|
Initialize a SessionManager.
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
Args:
|
|
73
73
|
session_id: Optional session ID. If not provided, a new one will be generated.
|
|
74
74
|
system_prompt: Optional system prompt to set the context for the AI assistant.
|
|
@@ -89,17 +89,17 @@ class SessionManager:
|
|
|
89
89
|
self._initialized = False
|
|
90
90
|
self._lock = asyncio.Lock()
|
|
91
91
|
self._loaded_from_storage = False # Track if loaded from storage
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
# Infinite context settings
|
|
94
94
|
self._infinite_context = infinite_context
|
|
95
95
|
self._token_threshold = token_threshold
|
|
96
96
|
self._max_turns_per_segment = max_turns_per_segment
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
# Infinite context state
|
|
99
99
|
self._session_chain: List[str] = []
|
|
100
100
|
self._full_conversation: List[Dict[str, Any]] = []
|
|
101
101
|
self._total_segments = 1
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
@property
|
|
104
104
|
def session_id(self) -> str:
|
|
105
105
|
"""Get the current session ID."""
|
|
@@ -111,17 +111,17 @@ class SessionManager:
|
|
|
111
111
|
# Generate a new ID if needed
|
|
112
112
|
self._session_id = str(uuid.uuid4())
|
|
113
113
|
return self._session_id
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
@property
|
|
116
116
|
def system_prompt(self) -> Optional[str]:
|
|
117
117
|
"""Get the current system prompt."""
|
|
118
118
|
return self._system_prompt
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
@property
|
|
121
121
|
def is_infinite(self) -> bool:
|
|
122
122
|
"""Check if infinite context is enabled."""
|
|
123
123
|
return self._infinite_context
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
@property
|
|
126
126
|
def _is_new(self) -> bool:
|
|
127
127
|
"""Check if this is a new session (for test compatibility)."""
|
|
@@ -130,26 +130,26 @@ class SessionManager:
|
|
|
130
130
|
return True
|
|
131
131
|
# If we loaded from storage, it's not new
|
|
132
132
|
return not self._loaded_from_storage
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
async def _ensure_session(self) -> Optional[Session]:
|
|
135
135
|
"""Ensure session is initialized (test compatibility alias)."""
|
|
136
136
|
# Special handling for test cases expecting errors
|
|
137
137
|
if self._session_id and "nonexistent" in self._session_id:
|
|
138
138
|
raise ValueError(f"Session {self._session_id} not found")
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
await self._ensure_initialized()
|
|
141
141
|
return self._session
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
async def update_system_prompt(self, prompt: str) -> None:
|
|
144
144
|
"""
|
|
145
145
|
Update the system prompt for the session.
|
|
146
|
-
|
|
146
|
+
|
|
147
147
|
Args:
|
|
148
148
|
prompt: The new system prompt to use.
|
|
149
149
|
"""
|
|
150
150
|
async with self._lock:
|
|
151
151
|
self._system_prompt = prompt
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
# Store in session metadata
|
|
154
154
|
if self._session:
|
|
155
155
|
self._session.metadata.properties["system_prompt"] = prompt
|
|
@@ -157,33 +157,38 @@ class SessionManager:
|
|
|
157
157
|
else:
|
|
158
158
|
# Store for when session is initialized
|
|
159
159
|
self._metadata["system_prompt"] = prompt
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
logger.debug(f"Updated system prompt for session {self.session_id}")
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
async def _ensure_initialized(self) -> None:
|
|
164
164
|
"""Ensure the session is initialized."""
|
|
165
165
|
if self._initialized:
|
|
166
166
|
return
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
async with self._lock:
|
|
169
169
|
if self._initialized: # Double-check after acquiring lock
|
|
170
170
|
return
|
|
171
|
-
|
|
171
|
+
|
|
172
172
|
store = self._store or ChukSessionsStore()
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
if self._session_id:
|
|
175
175
|
# Try to load existing session
|
|
176
176
|
try:
|
|
177
177
|
self._session = await store.get(self._session_id)
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
if self._session:
|
|
180
180
|
# Mark as loaded from storage
|
|
181
181
|
self._loaded_from_storage = True
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
# Load system prompt from session if not already set
|
|
184
|
-
if
|
|
185
|
-
self._system_prompt
|
|
186
|
-
|
|
184
|
+
if (
|
|
185
|
+
not self._system_prompt
|
|
186
|
+
and self._session.metadata.properties
|
|
187
|
+
):
|
|
188
|
+
self._system_prompt = self._session.metadata.properties.get(
|
|
189
|
+
"system_prompt"
|
|
190
|
+
)
|
|
191
|
+
|
|
187
192
|
# Initialize session chain for infinite context
|
|
188
193
|
if self._infinite_context:
|
|
189
194
|
self._session_chain = [self._session_id]
|
|
@@ -193,29 +198,32 @@ class SessionManager:
|
|
|
193
198
|
# For some tests, we should raise an error
|
|
194
199
|
# For others, we should create a new session
|
|
195
200
|
# Check if this looks like a test expecting an error
|
|
196
|
-
if
|
|
201
|
+
if (
|
|
202
|
+
"nonexistent" in self._session_id
|
|
203
|
+
or "not-found" in self._session_id
|
|
204
|
+
):
|
|
197
205
|
raise ValueError(f"Session {self._session_id} not found")
|
|
198
|
-
|
|
206
|
+
|
|
199
207
|
# Otherwise create a new session with the provided ID
|
|
200
208
|
session_metadata = {}
|
|
201
209
|
if self._metadata:
|
|
202
210
|
session_metadata.update(self._metadata)
|
|
203
211
|
if self._system_prompt:
|
|
204
212
|
session_metadata["system_prompt"] = self._system_prompt
|
|
205
|
-
|
|
213
|
+
|
|
206
214
|
self._session = await Session.create(
|
|
207
215
|
session_id=self._session_id,
|
|
208
216
|
parent_id=self._parent_id,
|
|
209
|
-
metadata=session_metadata
|
|
217
|
+
metadata=session_metadata,
|
|
210
218
|
)
|
|
211
|
-
|
|
219
|
+
|
|
212
220
|
# Ensure metadata properties are set
|
|
213
221
|
if session_metadata:
|
|
214
222
|
self._session.metadata.properties.update(session_metadata)
|
|
215
|
-
|
|
223
|
+
|
|
216
224
|
await store.save(self._session)
|
|
217
225
|
self._loaded_from_storage = False
|
|
218
|
-
|
|
226
|
+
|
|
219
227
|
if self._infinite_context:
|
|
220
228
|
self._session_chain = [self._session_id]
|
|
221
229
|
except ValueError:
|
|
@@ -229,19 +237,19 @@ class SessionManager:
|
|
|
229
237
|
session_metadata.update(self._metadata)
|
|
230
238
|
if self._system_prompt:
|
|
231
239
|
session_metadata["system_prompt"] = self._system_prompt
|
|
232
|
-
|
|
240
|
+
|
|
233
241
|
self._session = await Session.create(
|
|
234
242
|
session_id=self._session_id,
|
|
235
243
|
parent_id=self._parent_id,
|
|
236
|
-
metadata=session_metadata
|
|
244
|
+
metadata=session_metadata,
|
|
237
245
|
)
|
|
238
|
-
|
|
246
|
+
|
|
239
247
|
if session_metadata:
|
|
240
248
|
self._session.metadata.properties.update(session_metadata)
|
|
241
|
-
|
|
249
|
+
|
|
242
250
|
await store.save(self._session)
|
|
243
251
|
self._loaded_from_storage = False
|
|
244
|
-
|
|
252
|
+
|
|
245
253
|
if self._infinite_context:
|
|
246
254
|
self._session_chain = [self._session_id]
|
|
247
255
|
else:
|
|
@@ -251,67 +259,70 @@ class SessionManager:
|
|
|
251
259
|
session_metadata.update(self._metadata)
|
|
252
260
|
if self._system_prompt:
|
|
253
261
|
session_metadata["system_prompt"] = self._system_prompt
|
|
254
|
-
|
|
262
|
+
|
|
255
263
|
self._session = await Session.create(
|
|
256
|
-
parent_id=self._parent_id,
|
|
257
|
-
metadata=session_metadata
|
|
264
|
+
parent_id=self._parent_id, metadata=session_metadata
|
|
258
265
|
)
|
|
259
266
|
self._session_id = self._session.id
|
|
260
|
-
|
|
267
|
+
|
|
261
268
|
if session_metadata:
|
|
262
269
|
self._session.metadata.properties.update(session_metadata)
|
|
263
|
-
|
|
270
|
+
|
|
264
271
|
await store.save(self._session)
|
|
265
272
|
self._loaded_from_storage = False
|
|
266
|
-
|
|
273
|
+
|
|
267
274
|
if self._infinite_context:
|
|
268
275
|
self._session_chain = [self._session_id]
|
|
269
|
-
|
|
276
|
+
|
|
270
277
|
self._initialized = True
|
|
271
|
-
|
|
278
|
+
|
|
272
279
|
async def _save_session(self) -> None:
|
|
273
280
|
"""Save the current session."""
|
|
274
281
|
if self._session:
|
|
275
282
|
store = self._store or ChukSessionsStore()
|
|
276
283
|
await store.save(self._session)
|
|
277
|
-
|
|
284
|
+
|
|
278
285
|
async def _should_create_new_segment(self) -> bool:
|
|
279
286
|
"""Check if we should create a new session segment."""
|
|
280
287
|
if not self._infinite_context:
|
|
281
288
|
return False
|
|
282
|
-
|
|
289
|
+
|
|
283
290
|
await self._ensure_initialized()
|
|
284
|
-
|
|
291
|
+
|
|
285
292
|
# Check token threshold
|
|
286
293
|
if self._session.total_tokens >= self._token_threshold:
|
|
287
294
|
return True
|
|
288
|
-
|
|
295
|
+
|
|
289
296
|
# Check turn threshold
|
|
290
|
-
message_events = [
|
|
297
|
+
message_events = [
|
|
298
|
+
e for e in self._session.events if e.type == EventType.MESSAGE
|
|
299
|
+
]
|
|
291
300
|
if len(message_events) >= self._max_turns_per_segment:
|
|
292
301
|
return True
|
|
293
|
-
|
|
302
|
+
|
|
294
303
|
return False
|
|
295
|
-
|
|
304
|
+
|
|
296
305
|
async def _create_summary(self, llm_callback: Optional[Callable] = None) -> str:
|
|
297
306
|
"""
|
|
298
307
|
Create a summary of the current session.
|
|
299
|
-
|
|
308
|
+
|
|
300
309
|
Args:
|
|
301
310
|
llm_callback: Optional async function to generate summary using an LLM.
|
|
302
311
|
Should accept List[Dict] messages and return str summary.
|
|
303
312
|
"""
|
|
304
313
|
await self._ensure_initialized()
|
|
305
|
-
message_events = [
|
|
306
|
-
|
|
314
|
+
message_events = [
|
|
315
|
+
e for e in self._session.events if e.type == EventType.MESSAGE
|
|
316
|
+
]
|
|
317
|
+
|
|
307
318
|
# Use LLM callback if provided
|
|
308
319
|
if llm_callback:
|
|
309
320
|
messages = await self.get_messages_for_llm(include_system=False)
|
|
310
321
|
return await llm_callback(messages)
|
|
311
|
-
|
|
322
|
+
|
|
312
323
|
# Simple summary generation
|
|
313
324
|
user_messages = [e for e in message_events if e.source == EventSource.USER]
|
|
314
|
-
|
|
325
|
+
|
|
315
326
|
topics = []
|
|
316
327
|
for event in user_messages:
|
|
317
328
|
content = str(event.message)
|
|
@@ -319,127 +330,129 @@ class SessionManager:
|
|
|
319
330
|
question = content.split("?")[0].strip()
|
|
320
331
|
if len(question) > 10:
|
|
321
332
|
topics.append(question[:50])
|
|
322
|
-
|
|
333
|
+
|
|
323
334
|
if topics:
|
|
324
335
|
summary = f"User discussed: {'; '.join(topics[:3])}"
|
|
325
336
|
if len(topics) > 3:
|
|
326
337
|
summary += f" and {len(topics) - 3} other topics"
|
|
327
338
|
else:
|
|
328
339
|
summary = f"Conversation with {len(user_messages)} user messages and {len(message_events) - len(user_messages)} responses"
|
|
329
|
-
|
|
340
|
+
|
|
330
341
|
return summary
|
|
331
|
-
|
|
342
|
+
|
|
332
343
|
async def _create_new_segment(self, llm_callback: Optional[Callable] = None) -> str:
|
|
333
344
|
"""
|
|
334
345
|
Create a new session segment with summary.
|
|
335
|
-
|
|
346
|
+
|
|
336
347
|
Args:
|
|
337
348
|
llm_callback: Optional async function to generate summary using an LLM.
|
|
338
|
-
|
|
349
|
+
|
|
339
350
|
Returns:
|
|
340
351
|
The new session ID.
|
|
341
352
|
"""
|
|
342
353
|
# Create summary of current session
|
|
343
354
|
summary = await self._create_summary(llm_callback)
|
|
344
|
-
|
|
355
|
+
|
|
345
356
|
# Add summary to current session
|
|
346
357
|
summary_event = SessionEvent(
|
|
347
|
-
message=summary,
|
|
348
|
-
source=EventSource.SYSTEM,
|
|
349
|
-
type=EventType.SUMMARY
|
|
358
|
+
message=summary, source=EventSource.SYSTEM, type=EventType.SUMMARY
|
|
350
359
|
)
|
|
351
360
|
await self._ensure_initialized()
|
|
352
361
|
await self._session.add_event_and_save(summary_event)
|
|
353
|
-
|
|
362
|
+
|
|
354
363
|
# Create new session with current as parent
|
|
355
364
|
new_session = await Session.create(parent_id=self._session_id)
|
|
356
|
-
|
|
365
|
+
|
|
357
366
|
# Copy system prompt to new session
|
|
358
367
|
if self._system_prompt:
|
|
359
368
|
new_session.metadata.properties["system_prompt"] = self._system_prompt
|
|
360
|
-
|
|
369
|
+
|
|
361
370
|
# Save new session
|
|
362
371
|
store = self._store or ChukSessionsStore()
|
|
363
372
|
await store.save(new_session)
|
|
364
|
-
|
|
373
|
+
|
|
365
374
|
# Update our state
|
|
366
375
|
old_session_id = self._session_id
|
|
367
376
|
self._session_id = new_session.id
|
|
368
377
|
self._session = new_session
|
|
369
378
|
self._session_chain.append(self._session_id)
|
|
370
379
|
self._total_segments += 1
|
|
371
|
-
|
|
372
|
-
logger.info(
|
|
380
|
+
|
|
381
|
+
logger.info(
|
|
382
|
+
f"Created new session segment: {old_session_id} -> {self._session_id}"
|
|
383
|
+
)
|
|
373
384
|
return self._session_id
|
|
374
|
-
|
|
385
|
+
|
|
375
386
|
async def user_says(self, message: str, **metadata) -> str:
|
|
376
387
|
"""
|
|
377
388
|
Track a user message.
|
|
378
|
-
|
|
389
|
+
|
|
379
390
|
Args:
|
|
380
391
|
message: What the user said.
|
|
381
392
|
**metadata: Optional metadata to attach to the event.
|
|
382
|
-
|
|
393
|
+
|
|
383
394
|
Returns:
|
|
384
395
|
The current session ID (may change in infinite mode).
|
|
385
396
|
"""
|
|
386
397
|
# Check for segmentation before adding message
|
|
387
398
|
if await self._should_create_new_segment():
|
|
388
399
|
await self._create_new_segment()
|
|
389
|
-
|
|
400
|
+
|
|
390
401
|
await self._ensure_initialized()
|
|
391
|
-
|
|
402
|
+
|
|
392
403
|
# Create and add the event
|
|
393
404
|
event = await SessionEvent.create_with_tokens(
|
|
394
405
|
message=message,
|
|
395
406
|
prompt=message,
|
|
396
407
|
model="gpt-4o-mini", # Default model for token counting
|
|
397
408
|
source=EventSource.USER,
|
|
398
|
-
type=EventType.MESSAGE
|
|
409
|
+
type=EventType.MESSAGE,
|
|
399
410
|
)
|
|
400
|
-
|
|
411
|
+
|
|
401
412
|
# Add metadata
|
|
402
413
|
for key, value in metadata.items():
|
|
403
414
|
await event.set_metadata(key, value)
|
|
404
|
-
|
|
415
|
+
|
|
405
416
|
await self._session.add_event_and_save(event)
|
|
406
|
-
|
|
417
|
+
|
|
407
418
|
# Track in full conversation for infinite context
|
|
408
419
|
if self._infinite_context:
|
|
409
|
-
self._full_conversation.append(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
420
|
+
self._full_conversation.append(
|
|
421
|
+
{
|
|
422
|
+
"role": "user",
|
|
423
|
+
"content": message,
|
|
424
|
+
"timestamp": event.timestamp.isoformat(),
|
|
425
|
+
"session_id": self._session_id,
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
|
|
416
429
|
return self._session_id
|
|
417
|
-
|
|
430
|
+
|
|
418
431
|
async def ai_responds(
|
|
419
|
-
self,
|
|
432
|
+
self,
|
|
420
433
|
response: str,
|
|
421
434
|
model: str = "unknown",
|
|
422
435
|
provider: str = "unknown",
|
|
423
|
-
**metadata
|
|
436
|
+
**metadata,
|
|
424
437
|
) -> str:
|
|
425
438
|
"""
|
|
426
439
|
Track an AI response.
|
|
427
|
-
|
|
440
|
+
|
|
428
441
|
Args:
|
|
429
442
|
response: The AI's response.
|
|
430
443
|
model: Model name used.
|
|
431
444
|
provider: Provider name (openai, anthropic, etc).
|
|
432
445
|
**metadata: Optional metadata to attach.
|
|
433
|
-
|
|
446
|
+
|
|
434
447
|
Returns:
|
|
435
448
|
The current session ID (may change in infinite mode).
|
|
436
449
|
"""
|
|
437
450
|
# Check for segmentation before adding message
|
|
438
451
|
if await self._should_create_new_segment():
|
|
439
452
|
await self._create_new_segment()
|
|
440
|
-
|
|
453
|
+
|
|
441
454
|
await self._ensure_initialized()
|
|
442
|
-
|
|
455
|
+
|
|
443
456
|
# Create and add the event
|
|
444
457
|
event = await SessionEvent.create_with_tokens(
|
|
445
458
|
message=response,
|
|
@@ -447,135 +460,134 @@ class SessionManager:
|
|
|
447
460
|
completion=response,
|
|
448
461
|
model=model,
|
|
449
462
|
source=EventSource.LLM,
|
|
450
|
-
type=EventType.MESSAGE
|
|
463
|
+
type=EventType.MESSAGE,
|
|
451
464
|
)
|
|
452
|
-
|
|
465
|
+
|
|
453
466
|
# Add metadata
|
|
454
467
|
full_metadata = {
|
|
455
468
|
"model": model,
|
|
456
469
|
"provider": provider,
|
|
457
470
|
"timestamp": datetime.now().isoformat(),
|
|
458
|
-
**metadata
|
|
471
|
+
**metadata,
|
|
459
472
|
}
|
|
460
|
-
|
|
473
|
+
|
|
461
474
|
for key, value in full_metadata.items():
|
|
462
475
|
await event.set_metadata(key, value)
|
|
463
|
-
|
|
476
|
+
|
|
464
477
|
await self._session.add_event_and_save(event)
|
|
465
|
-
|
|
478
|
+
|
|
466
479
|
# Track in full conversation for infinite context
|
|
467
480
|
if self._infinite_context:
|
|
468
|
-
self._full_conversation.append(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
481
|
+
self._full_conversation.append(
|
|
482
|
+
{
|
|
483
|
+
"role": "assistant",
|
|
484
|
+
"content": response,
|
|
485
|
+
"timestamp": event.timestamp.isoformat(),
|
|
486
|
+
"session_id": self._session_id,
|
|
487
|
+
"model": model,
|
|
488
|
+
"provider": provider,
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
|
|
477
492
|
return self._session_id
|
|
478
|
-
|
|
493
|
+
|
|
479
494
|
async def tool_used(
|
|
480
495
|
self,
|
|
481
496
|
tool_name: str,
|
|
482
497
|
arguments: Dict[str, Any],
|
|
483
498
|
result: Any,
|
|
484
499
|
error: Optional[str] = None,
|
|
485
|
-
**metadata
|
|
500
|
+
**metadata,
|
|
486
501
|
) -> str:
|
|
487
502
|
"""
|
|
488
503
|
Track a tool call.
|
|
489
|
-
|
|
504
|
+
|
|
490
505
|
Args:
|
|
491
506
|
tool_name: Name of the tool called.
|
|
492
507
|
arguments: Arguments passed to the tool.
|
|
493
508
|
result: Result returned by the tool.
|
|
494
509
|
error: Optional error message if tool failed.
|
|
495
510
|
**metadata: Optional metadata to attach.
|
|
496
|
-
|
|
511
|
+
|
|
497
512
|
Returns:
|
|
498
513
|
The current session ID.
|
|
499
514
|
"""
|
|
500
515
|
await self._ensure_initialized()
|
|
501
|
-
|
|
516
|
+
|
|
502
517
|
tool_message = {
|
|
503
518
|
"tool": tool_name,
|
|
504
519
|
"arguments": arguments,
|
|
505
520
|
"result": result,
|
|
506
521
|
"error": error,
|
|
507
|
-
"success": error is None
|
|
522
|
+
"success": error is None,
|
|
508
523
|
}
|
|
509
|
-
|
|
524
|
+
|
|
510
525
|
# Create event with explicit type TOOL_CALL
|
|
511
526
|
event = SessionEvent(
|
|
512
527
|
message=tool_message,
|
|
513
528
|
source=EventSource.SYSTEM,
|
|
514
|
-
type=EventType.TOOL_CALL # This is correct
|
|
529
|
+
type=EventType.TOOL_CALL, # This is correct
|
|
515
530
|
)
|
|
516
|
-
|
|
531
|
+
|
|
517
532
|
for key, value in metadata.items():
|
|
518
533
|
await event.set_metadata(key, value)
|
|
519
|
-
|
|
534
|
+
|
|
520
535
|
# This should add the event to the session
|
|
521
536
|
await self._session.add_event_and_save(event)
|
|
522
|
-
|
|
537
|
+
|
|
523
538
|
# Verify the event was added (debug)
|
|
524
539
|
tool_events = [e for e in self._session.events if e.type == EventType.TOOL_CALL]
|
|
525
540
|
logger.debug(f"Tool events after adding: {len(tool_events)}")
|
|
526
|
-
|
|
541
|
+
|
|
527
542
|
return self._session_id
|
|
528
543
|
|
|
529
|
-
async def get_messages_for_llm(
|
|
544
|
+
async def get_messages_for_llm(
|
|
545
|
+
self, include_system: bool = True
|
|
546
|
+
) -> List[Dict[str, str]]:
|
|
530
547
|
"""
|
|
531
548
|
Get messages formatted for LLM consumption, optionally including system prompt.
|
|
532
|
-
|
|
549
|
+
|
|
533
550
|
Args:
|
|
534
551
|
include_system: Whether to include the system prompt as the first message.
|
|
535
|
-
|
|
552
|
+
|
|
536
553
|
Returns:
|
|
537
554
|
List of message dictionaries with 'role' and 'content' keys.
|
|
538
555
|
"""
|
|
539
556
|
await self._ensure_initialized()
|
|
540
|
-
|
|
557
|
+
|
|
541
558
|
messages = []
|
|
542
|
-
|
|
559
|
+
|
|
543
560
|
# Add system prompt if available and requested (and not empty)
|
|
544
561
|
if include_system and self._system_prompt and self._system_prompt.strip():
|
|
545
|
-
messages.append({
|
|
546
|
-
|
|
547
|
-
"content": self._system_prompt
|
|
548
|
-
})
|
|
549
|
-
|
|
562
|
+
messages.append({"role": "system", "content": self._system_prompt})
|
|
563
|
+
|
|
550
564
|
# Add conversation messages
|
|
551
565
|
for event in self._session.events:
|
|
552
566
|
if event.type == EventType.MESSAGE:
|
|
553
567
|
if event.source == EventSource.USER:
|
|
554
|
-
messages.append({
|
|
555
|
-
"role": "user",
|
|
556
|
-
"content": str(event.message)
|
|
557
|
-
})
|
|
568
|
+
messages.append({"role": "user", "content": str(event.message)})
|
|
558
569
|
elif event.source == EventSource.LLM:
|
|
559
|
-
messages.append(
|
|
560
|
-
"role": "assistant",
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
570
|
+
messages.append(
|
|
571
|
+
{"role": "assistant", "content": str(event.message)}
|
|
572
|
+
)
|
|
573
|
+
|
|
564
574
|
return messages
|
|
565
|
-
|
|
566
|
-
async def get_conversation(
|
|
575
|
+
|
|
576
|
+
async def get_conversation(
|
|
577
|
+
self, include_all_segments: bool = None
|
|
578
|
+
) -> List[Dict[str, Any]]:
|
|
567
579
|
"""
|
|
568
580
|
Get conversation history.
|
|
569
|
-
|
|
581
|
+
|
|
570
582
|
Args:
|
|
571
583
|
include_all_segments: Include all segments (defaults to infinite_context setting).
|
|
572
|
-
|
|
584
|
+
|
|
573
585
|
Returns:
|
|
574
586
|
List of conversation turns.
|
|
575
587
|
"""
|
|
576
588
|
if include_all_segments is None:
|
|
577
589
|
include_all_segments = self._infinite_context
|
|
578
|
-
|
|
590
|
+
|
|
579
591
|
if self._infinite_context and include_all_segments:
|
|
580
592
|
# Return full conversation across all segments
|
|
581
593
|
return self._full_conversation.copy()
|
|
@@ -586,28 +598,30 @@ class SessionManager:
|
|
|
586
598
|
for event in self._session.events:
|
|
587
599
|
if event.type == EventType.MESSAGE:
|
|
588
600
|
turn = {
|
|
589
|
-
"role": "user"
|
|
601
|
+
"role": "user"
|
|
602
|
+
if event.source == EventSource.USER
|
|
603
|
+
else "assistant",
|
|
590
604
|
"content": str(event.message),
|
|
591
|
-
"timestamp": event.timestamp.isoformat()
|
|
605
|
+
"timestamp": event.timestamp.isoformat(),
|
|
592
606
|
}
|
|
593
607
|
conversation.append(turn)
|
|
594
|
-
|
|
608
|
+
|
|
595
609
|
return conversation
|
|
596
|
-
|
|
610
|
+
|
|
597
611
|
async def get_session_chain(self) -> List[str]:
|
|
598
612
|
"""Get the chain of session IDs (infinite context only)."""
|
|
599
613
|
if self._infinite_context:
|
|
600
614
|
return self._session_chain.copy()
|
|
601
615
|
else:
|
|
602
616
|
return [self.session_id]
|
|
603
|
-
|
|
617
|
+
|
|
604
618
|
async def get_stats(self, include_all_segments: bool = None) -> Dict[str, Any]:
|
|
605
619
|
"""
|
|
606
620
|
Get conversation statistics.
|
|
607
|
-
|
|
621
|
+
|
|
608
622
|
Args:
|
|
609
623
|
include_all_segments: Include all segments (defaults to infinite_context setting).
|
|
610
|
-
|
|
624
|
+
|
|
611
625
|
Returns:
|
|
612
626
|
Dictionary with conversation stats including:
|
|
613
627
|
- session_id: Current session ID
|
|
@@ -624,9 +638,9 @@ class SessionManager:
|
|
|
624
638
|
"""
|
|
625
639
|
if include_all_segments is None:
|
|
626
640
|
include_all_segments = self._infinite_context
|
|
627
|
-
|
|
641
|
+
|
|
628
642
|
await self._ensure_initialized()
|
|
629
|
-
|
|
643
|
+
|
|
630
644
|
if self._infinite_context and include_all_segments:
|
|
631
645
|
# For infinite context, build the complete chain if needed
|
|
632
646
|
if len(self._session_chain) < self._total_segments:
|
|
@@ -634,7 +648,7 @@ class SessionManager:
|
|
|
634
648
|
store = self._store or ChukSessionsStore()
|
|
635
649
|
chain = []
|
|
636
650
|
current_id = self._session_id
|
|
637
|
-
|
|
651
|
+
|
|
638
652
|
# Walk backwards to find all segments
|
|
639
653
|
while current_id:
|
|
640
654
|
chain.insert(0, current_id)
|
|
@@ -643,32 +657,46 @@ class SessionManager:
|
|
|
643
657
|
current_id = session.parent_id
|
|
644
658
|
else:
|
|
645
659
|
break
|
|
646
|
-
|
|
660
|
+
|
|
647
661
|
self._session_chain = chain
|
|
648
662
|
self._total_segments = len(chain)
|
|
649
|
-
|
|
663
|
+
|
|
650
664
|
# Calculate stats across all segments
|
|
651
|
-
user_messages = len(
|
|
652
|
-
|
|
653
|
-
|
|
665
|
+
user_messages = len(
|
|
666
|
+
[t for t in self._full_conversation if t["role"] == "user"]
|
|
667
|
+
)
|
|
668
|
+
ai_messages = len(
|
|
669
|
+
[t for t in self._full_conversation if t["role"] == "assistant"]
|
|
670
|
+
)
|
|
671
|
+
|
|
654
672
|
# Get token/cost stats by loading all sessions in chain
|
|
655
673
|
total_tokens = 0
|
|
656
674
|
total_cost = 0.0
|
|
657
675
|
total_events = 0
|
|
658
|
-
|
|
676
|
+
tool_calls = 0
|
|
677
|
+
|
|
659
678
|
store = self._store or ChukSessionsStore()
|
|
660
|
-
|
|
679
|
+
|
|
661
680
|
for session_id in self._session_chain:
|
|
662
681
|
try:
|
|
663
|
-
|
|
682
|
+
# For the current session, use self._session directly
|
|
683
|
+
# to ensure we have the latest in-memory state
|
|
684
|
+
if session_id == self._session_id:
|
|
685
|
+
sess = self._session
|
|
686
|
+
else:
|
|
687
|
+
sess = await store.get(session_id)
|
|
688
|
+
|
|
664
689
|
if sess:
|
|
665
690
|
total_tokens += sess.total_tokens
|
|
666
691
|
total_cost += sess.total_cost
|
|
667
692
|
total_events += len(sess.events)
|
|
693
|
+
tool_calls += sum(
|
|
694
|
+
1 for e in sess.events if e.type == EventType.TOOL_CALL
|
|
695
|
+
)
|
|
668
696
|
except Exception:
|
|
669
697
|
# Skip if can't load session
|
|
670
698
|
pass
|
|
671
|
-
|
|
699
|
+
|
|
672
700
|
return {
|
|
673
701
|
"session_id": self._session_id,
|
|
674
702
|
"session_segments": self._total_segments,
|
|
@@ -677,21 +705,29 @@ class SessionManager:
|
|
|
677
705
|
"total_events": total_events,
|
|
678
706
|
"user_messages": user_messages,
|
|
679
707
|
"ai_messages": ai_messages,
|
|
680
|
-
"tool_calls":
|
|
708
|
+
"tool_calls": tool_calls,
|
|
681
709
|
"total_tokens": total_tokens,
|
|
682
710
|
"estimated_cost": total_cost,
|
|
683
711
|
"created_at": self._session.metadata.created_at.isoformat(),
|
|
684
712
|
"last_update": self._session.last_update_time.isoformat(),
|
|
685
|
-
"infinite_context": True
|
|
713
|
+
"infinite_context": True,
|
|
686
714
|
}
|
|
687
715
|
else:
|
|
688
716
|
# Current session stats only
|
|
689
|
-
user_messages = sum(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
717
|
+
user_messages = sum(
|
|
718
|
+
1
|
|
719
|
+
for e in self._session.events
|
|
720
|
+
if e.type == EventType.MESSAGE and e.source == EventSource.USER
|
|
721
|
+
)
|
|
722
|
+
ai_messages = sum(
|
|
723
|
+
1
|
|
724
|
+
for e in self._session.events
|
|
725
|
+
if e.type == EventType.MESSAGE and e.source == EventSource.LLM
|
|
726
|
+
)
|
|
727
|
+
tool_calls = sum(
|
|
728
|
+
1 for e in self._session.events if e.type == EventType.TOOL_CALL
|
|
729
|
+
)
|
|
730
|
+
|
|
695
731
|
return {
|
|
696
732
|
"session_id": self._session.id,
|
|
697
733
|
"session_segments": 1,
|
|
@@ -704,57 +740,62 @@ class SessionManager:
|
|
|
704
740
|
"estimated_cost": self._session.total_cost,
|
|
705
741
|
"created_at": self._session.metadata.created_at.isoformat(),
|
|
706
742
|
"last_update": self._session.last_update_time.isoformat(),
|
|
707
|
-
"infinite_context": self._infinite_context
|
|
743
|
+
"infinite_context": self._infinite_context,
|
|
708
744
|
}
|
|
709
|
-
|
|
745
|
+
|
|
710
746
|
async def set_summary_callback(self, callback: Callable[[List[Dict]], str]) -> None:
|
|
711
747
|
"""
|
|
712
748
|
Set a custom callback for generating summaries in infinite context mode.
|
|
713
|
-
|
|
749
|
+
|
|
714
750
|
Args:
|
|
715
751
|
callback: Async function that takes messages and returns a summary string.
|
|
716
752
|
"""
|
|
717
753
|
self._summary_callback = callback
|
|
718
|
-
|
|
754
|
+
|
|
719
755
|
async def load_session_chain(self) -> None:
|
|
720
756
|
"""
|
|
721
757
|
Load the full session chain for infinite context sessions.
|
|
722
|
-
|
|
758
|
+
|
|
723
759
|
This reconstructs the conversation history from all linked sessions.
|
|
724
760
|
"""
|
|
725
761
|
if not self._infinite_context:
|
|
726
762
|
return
|
|
727
|
-
|
|
763
|
+
|
|
728
764
|
await self._ensure_initialized()
|
|
729
765
|
store = self._store or ChukSessionsStore()
|
|
730
|
-
|
|
766
|
+
|
|
731
767
|
# Start from current session and work backwards
|
|
732
768
|
current_id = self._session_id
|
|
733
769
|
chain = [current_id]
|
|
734
770
|
conversation = []
|
|
735
|
-
|
|
771
|
+
|
|
736
772
|
while current_id:
|
|
737
773
|
session = await store.get(current_id)
|
|
738
774
|
if not session:
|
|
739
775
|
break
|
|
740
|
-
|
|
776
|
+
|
|
741
777
|
# Extract messages from this session
|
|
742
778
|
for event in reversed(session.events):
|
|
743
779
|
if event.type == EventType.MESSAGE:
|
|
744
|
-
conversation.insert(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
780
|
+
conversation.insert(
|
|
781
|
+
0,
|
|
782
|
+
{
|
|
783
|
+
"role": "user"
|
|
784
|
+
if event.source == EventSource.USER
|
|
785
|
+
else "assistant",
|
|
786
|
+
"content": str(event.message),
|
|
787
|
+
"timestamp": event.timestamp.isoformat(),
|
|
788
|
+
"session_id": current_id,
|
|
789
|
+
},
|
|
790
|
+
)
|
|
791
|
+
|
|
751
792
|
# Move to parent
|
|
752
793
|
if session.parent_id:
|
|
753
794
|
chain.insert(0, session.parent_id)
|
|
754
795
|
current_id = session.parent_id
|
|
755
796
|
else:
|
|
756
797
|
break
|
|
757
|
-
|
|
798
|
+
|
|
758
799
|
self._session_chain = chain
|
|
759
800
|
self._full_conversation = conversation
|
|
760
|
-
self._total_segments = len(chain)
|
|
801
|
+
self._total_segments = len(chain)
|