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