chuk-ai-session-manager 0.1.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 +57 -0
- chuk_ai_session_manager/exceptions.py +129 -0
- chuk_ai_session_manager/infinite_conversation.py +316 -0
- chuk_ai_session_manager/models/__init__.py +44 -0
- chuk_ai_session_manager/models/event_source.py +8 -0
- chuk_ai_session_manager/models/event_type.py +9 -0
- chuk_ai_session_manager/models/session.py +316 -0
- chuk_ai_session_manager/models/session_event.py +166 -0
- chuk_ai_session_manager/models/session_metadata.py +37 -0
- chuk_ai_session_manager/models/session_run.py +115 -0
- chuk_ai_session_manager/models/token_usage.py +316 -0
- chuk_ai_session_manager/sample_tools.py +194 -0
- chuk_ai_session_manager/session_aware_tool_processor.py +178 -0
- chuk_ai_session_manager/session_prompt_builder.py +474 -0
- chuk_ai_session_manager/storage/__init__.py +44 -0
- chuk_ai_session_manager/storage/base.py +50 -0
- chuk_ai_session_manager/storage/providers/__init__.py +0 -0
- chuk_ai_session_manager/storage/providers/file.py +348 -0
- chuk_ai_session_manager/storage/providers/memory.py +96 -0
- chuk_ai_session_manager/storage/providers/redis.py +295 -0
- chuk_ai_session_manager-0.1.1.dist-info/METADATA +501 -0
- chuk_ai_session_manager-0.1.1.dist-info/RECORD +24 -0
- chuk_ai_session_manager-0.1.1.dist-info/WHEEL +5 -0
- chuk_ai_session_manager-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# chuk_ai_session_manager/session_prompt_builder.py
|
|
2
|
+
"""
|
|
3
|
+
Build optimized prompts for LLM calls from Session objects with async support.
|
|
4
|
+
|
|
5
|
+
This module provides flexible prompt construction from session data,
|
|
6
|
+
with support for token management, relevance-based selection,
|
|
7
|
+
and hierarchical context awareness.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from typing import List, Dict, Any, Optional, Literal, Union
|
|
14
|
+
from enum import Enum
|
|
15
|
+
import asyncio
|
|
16
|
+
|
|
17
|
+
from chuk_ai_session_manager.models.session import Session
|
|
18
|
+
from chuk_ai_session_manager.models.event_type import EventType
|
|
19
|
+
from chuk_ai_session_manager.models.event_source import EventSource
|
|
20
|
+
from chuk_ai_session_manager.models.token_usage import TokenUsage
|
|
21
|
+
from chuk_ai_session_manager.storage import SessionStoreProvider
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
class PromptStrategy(str, Enum):
|
|
26
|
+
"""Different strategies for building prompts."""
|
|
27
|
+
MINIMAL = "minimal" # Original minimal approach
|
|
28
|
+
TASK_FOCUSED = "task" # Focus on the task with minimal context
|
|
29
|
+
TOOL_FOCUSED = "tool" # Emphasize tool usage and results
|
|
30
|
+
CONVERSATION = "conversation" # Include more conversation history
|
|
31
|
+
HIERARCHICAL = "hierarchical" # Include parent session context
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def build_prompt_from_session(
|
|
35
|
+
session: Session,
|
|
36
|
+
strategy: Union[PromptStrategy, str] = PromptStrategy.MINIMAL,
|
|
37
|
+
max_tokens: Optional[int] = None,
|
|
38
|
+
model: str = "gpt-3.5-turbo",
|
|
39
|
+
include_parent_context: bool = False,
|
|
40
|
+
current_query: Optional[str] = None
|
|
41
|
+
) -> List[Dict[str, str]]:
|
|
42
|
+
"""
|
|
43
|
+
Build a prompt for the next LLM call from a Session asynchronously.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
session: The session to build a prompt from
|
|
47
|
+
strategy: Prompt building strategy to use
|
|
48
|
+
max_tokens: Maximum tokens to include (if specified)
|
|
49
|
+
model: Model to use for token counting
|
|
50
|
+
include_parent_context: Whether to include context from parent sessions
|
|
51
|
+
current_query: Current user query for relevance-based context selection
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A list of message dictionaries suitable for LLM API calls
|
|
55
|
+
"""
|
|
56
|
+
if not session.events:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
# Convert string strategy to enum if needed
|
|
60
|
+
if isinstance(strategy, str):
|
|
61
|
+
try:
|
|
62
|
+
strategy = PromptStrategy(strategy)
|
|
63
|
+
except ValueError:
|
|
64
|
+
logger.warning(f"Unknown strategy '{strategy}', falling back to MINIMAL")
|
|
65
|
+
strategy = PromptStrategy.MINIMAL
|
|
66
|
+
|
|
67
|
+
# Use the appropriate strategy
|
|
68
|
+
if strategy == PromptStrategy.MINIMAL:
|
|
69
|
+
return await _build_minimal_prompt(session)
|
|
70
|
+
elif strategy == PromptStrategy.TASK_FOCUSED:
|
|
71
|
+
return await _build_task_focused_prompt(session)
|
|
72
|
+
elif strategy == PromptStrategy.TOOL_FOCUSED:
|
|
73
|
+
return await _build_tool_focused_prompt(session)
|
|
74
|
+
elif strategy == PromptStrategy.CONVERSATION:
|
|
75
|
+
return await _build_conversation_prompt(session, max_history=5)
|
|
76
|
+
elif strategy == PromptStrategy.HIERARCHICAL:
|
|
77
|
+
return await _build_hierarchical_prompt(session, include_parent_context)
|
|
78
|
+
else:
|
|
79
|
+
# Default to minimal
|
|
80
|
+
return await _build_minimal_prompt(session)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _build_minimal_prompt(session: Session) -> List[Dict[str, str]]:
|
|
84
|
+
"""
|
|
85
|
+
Build a minimal prompt from a session.
|
|
86
|
+
|
|
87
|
+
This follows the original implementation's approach:
|
|
88
|
+
- Include the first USER message (task)
|
|
89
|
+
- Include the latest assistant MESSAGE with content set to None
|
|
90
|
+
- Include TOOL_CALL children as tool role messages
|
|
91
|
+
- Fall back to SUMMARY retry note if no TOOL_CALL children exist
|
|
92
|
+
"""
|
|
93
|
+
# First USER message
|
|
94
|
+
first_user = next(
|
|
95
|
+
(
|
|
96
|
+
e
|
|
97
|
+
for e in session.events
|
|
98
|
+
if e.type == EventType.MESSAGE and e.source == EventSource.USER
|
|
99
|
+
),
|
|
100
|
+
None,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Latest assistant MESSAGE
|
|
104
|
+
assistant_msg = next(
|
|
105
|
+
(
|
|
106
|
+
ev
|
|
107
|
+
for ev in reversed(session.events)
|
|
108
|
+
if ev.type == EventType.MESSAGE and ev.source != EventSource.USER
|
|
109
|
+
),
|
|
110
|
+
None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if assistant_msg is None:
|
|
114
|
+
# Only the user message exists so far
|
|
115
|
+
return [{"role": "user", "content": first_user.message}] if first_user else []
|
|
116
|
+
|
|
117
|
+
# Children of that assistant
|
|
118
|
+
children = [
|
|
119
|
+
e
|
|
120
|
+
for e in session.events
|
|
121
|
+
if e.metadata.get("parent_event_id") == assistant_msg.id
|
|
122
|
+
]
|
|
123
|
+
tool_calls = [c for c in children if c.type == EventType.TOOL_CALL]
|
|
124
|
+
summaries = [c for c in children if c.type == EventType.SUMMARY]
|
|
125
|
+
|
|
126
|
+
# Assemble prompt
|
|
127
|
+
prompt: List[Dict[str, str]] = []
|
|
128
|
+
if first_user:
|
|
129
|
+
# Handle both string messages and dict messages
|
|
130
|
+
user_content = first_user.message
|
|
131
|
+
if isinstance(user_content, dict) and "content" in user_content:
|
|
132
|
+
user_content = user_content["content"]
|
|
133
|
+
prompt.append({"role": "user", "content": user_content})
|
|
134
|
+
|
|
135
|
+
# ALWAYS add the assistant marker - but strip its free text
|
|
136
|
+
prompt.append({"role": "assistant", "content": None})
|
|
137
|
+
|
|
138
|
+
if tool_calls:
|
|
139
|
+
for tc in tool_calls:
|
|
140
|
+
# Extract relevant information from the tool call
|
|
141
|
+
# Handle both new and legacy formats
|
|
142
|
+
if isinstance(tc.message, dict):
|
|
143
|
+
tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
|
|
144
|
+
tool_result = tc.message.get("result", {})
|
|
145
|
+
else:
|
|
146
|
+
# Legacy format or unexpected type
|
|
147
|
+
tool_name = "unknown"
|
|
148
|
+
tool_result = tc.message
|
|
149
|
+
|
|
150
|
+
prompt.append(
|
|
151
|
+
{
|
|
152
|
+
"role": "tool",
|
|
153
|
+
"name": tool_name,
|
|
154
|
+
"content": json.dumps(tool_result, default=str),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
elif summaries:
|
|
158
|
+
# Use the latest summary
|
|
159
|
+
summary = summaries[-1]
|
|
160
|
+
if isinstance(summary.message, dict) and "note" in summary.message:
|
|
161
|
+
prompt.append({"role": "system", "content": summary.message["note"]})
|
|
162
|
+
else:
|
|
163
|
+
# Handle legacy or unexpected format
|
|
164
|
+
prompt.append({"role": "system", "content": str(summary.message)})
|
|
165
|
+
|
|
166
|
+
return prompt
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _build_task_focused_prompt(session: Session) -> List[Dict[str, str]]:
|
|
170
|
+
"""
|
|
171
|
+
Build a task-focused prompt.
|
|
172
|
+
|
|
173
|
+
This strategy emphasizes the original task and latest context:
|
|
174
|
+
- Includes the first USER message as the main task
|
|
175
|
+
- Includes the most recent USER message for current context
|
|
176
|
+
- Includes only the most recent and successful tool results
|
|
177
|
+
"""
|
|
178
|
+
# Get first and most recent user messages
|
|
179
|
+
user_messages = [
|
|
180
|
+
e for e in session.events
|
|
181
|
+
if e.type == EventType.MESSAGE and e.source == EventSource.USER
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
if not user_messages:
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
first_user = user_messages[0]
|
|
188
|
+
latest_user = user_messages[-1] if len(user_messages) > 1 else None
|
|
189
|
+
|
|
190
|
+
# Latest assistant MESSAGE
|
|
191
|
+
assistant_msg = next(
|
|
192
|
+
(
|
|
193
|
+
ev
|
|
194
|
+
for ev in reversed(session.events)
|
|
195
|
+
if ev.type == EventType.MESSAGE and ev.source != EventSource.USER
|
|
196
|
+
),
|
|
197
|
+
None,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Build prompt
|
|
201
|
+
prompt = []
|
|
202
|
+
|
|
203
|
+
# Always include the first user message (the main task)
|
|
204
|
+
first_content = first_user.message
|
|
205
|
+
if isinstance(first_content, dict) and "content" in first_content:
|
|
206
|
+
first_content = first_content["content"]
|
|
207
|
+
prompt.append({"role": "user", "content": first_content})
|
|
208
|
+
|
|
209
|
+
# Include the latest user message if different from the first
|
|
210
|
+
if latest_user and latest_user.id != first_user.id:
|
|
211
|
+
latest_content = latest_user.message
|
|
212
|
+
if isinstance(latest_content, dict) and "content" in latest_content:
|
|
213
|
+
latest_content = latest_content["content"]
|
|
214
|
+
prompt.append({"role": "user", "content": latest_content})
|
|
215
|
+
|
|
216
|
+
# Include assistant response placeholder
|
|
217
|
+
if assistant_msg:
|
|
218
|
+
prompt.append({"role": "assistant", "content": None})
|
|
219
|
+
|
|
220
|
+
# Find successful tool calls
|
|
221
|
+
children = [
|
|
222
|
+
e for e in session.events
|
|
223
|
+
if e.metadata.get("parent_event_id") == assistant_msg.id
|
|
224
|
+
]
|
|
225
|
+
tool_calls = [c for c in children if c.type == EventType.TOOL_CALL]
|
|
226
|
+
|
|
227
|
+
# Only include successful tool results
|
|
228
|
+
for tc in tool_calls:
|
|
229
|
+
# Extract and check if result indicates success
|
|
230
|
+
if isinstance(tc.message, dict):
|
|
231
|
+
tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
|
|
232
|
+
tool_result = tc.message.get("result", {})
|
|
233
|
+
|
|
234
|
+
# Skip error results
|
|
235
|
+
if isinstance(tool_result, dict) and tool_result.get("status") == "error":
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
prompt.append({
|
|
239
|
+
"role": "tool",
|
|
240
|
+
"name": tool_name,
|
|
241
|
+
"content": json.dumps(tool_result, default=str),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return prompt
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _build_tool_focused_prompt(session: Session) -> List[Dict[str, str]]:
|
|
248
|
+
"""
|
|
249
|
+
Build a tool-focused prompt.
|
|
250
|
+
|
|
251
|
+
This strategy emphasizes tool usage:
|
|
252
|
+
- Includes the latest user query
|
|
253
|
+
- Includes detailed information about tool calls and results
|
|
254
|
+
- Includes error information from failed tool calls
|
|
255
|
+
"""
|
|
256
|
+
# Get the latest user message
|
|
257
|
+
latest_user = next(
|
|
258
|
+
(e for e in reversed(session.events)
|
|
259
|
+
if e.type == EventType.MESSAGE and e.source == EventSource.USER),
|
|
260
|
+
None
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if not latest_user:
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
# Get the latest assistant message
|
|
267
|
+
assistant_msg = next(
|
|
268
|
+
(ev for ev in reversed(session.events)
|
|
269
|
+
if ev.type == EventType.MESSAGE and ev.source != EventSource.USER),
|
|
270
|
+
None
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Build prompt
|
|
274
|
+
prompt = []
|
|
275
|
+
|
|
276
|
+
# Include user message
|
|
277
|
+
user_content = latest_user.message
|
|
278
|
+
if isinstance(user_content, dict) and "content" in user_content:
|
|
279
|
+
user_content = user_content["content"]
|
|
280
|
+
prompt.append({"role": "user", "content": user_content})
|
|
281
|
+
|
|
282
|
+
# Include assistant placeholder
|
|
283
|
+
if assistant_msg:
|
|
284
|
+
prompt.append({"role": "assistant", "content": None})
|
|
285
|
+
|
|
286
|
+
# Get all tool calls for this assistant
|
|
287
|
+
children = [
|
|
288
|
+
e for e in session.events
|
|
289
|
+
if e.metadata.get("parent_event_id") == assistant_msg.id
|
|
290
|
+
]
|
|
291
|
+
tool_calls = [c for c in children if c.type == EventType.TOOL_CALL]
|
|
292
|
+
|
|
293
|
+
# Add all tool calls with status information
|
|
294
|
+
for tc in tool_calls:
|
|
295
|
+
if isinstance(tc.message, dict):
|
|
296
|
+
tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
|
|
297
|
+
tool_result = tc.message.get("result", {})
|
|
298
|
+
error = tc.message.get("error", None)
|
|
299
|
+
|
|
300
|
+
# Include status information in the tool response
|
|
301
|
+
content = tool_result
|
|
302
|
+
if error:
|
|
303
|
+
content = {"error": error, "details": tool_result}
|
|
304
|
+
|
|
305
|
+
prompt.append({
|
|
306
|
+
"role": "tool",
|
|
307
|
+
"name": tool_name,
|
|
308
|
+
"content": json.dumps(content, default=str),
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return prompt
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _build_conversation_prompt(
|
|
315
|
+
session: Session,
|
|
316
|
+
max_history: int = 5
|
|
317
|
+
) -> List[Dict[str, str]]:
|
|
318
|
+
"""
|
|
319
|
+
Build a conversation-style prompt with recent history.
|
|
320
|
+
|
|
321
|
+
This strategy creates a more natural conversation:
|
|
322
|
+
- Includes up to max_history recent messages in order
|
|
323
|
+
- Preserves conversation flow
|
|
324
|
+
- Still handles tool calls appropriately
|
|
325
|
+
"""
|
|
326
|
+
# Get relevant message events
|
|
327
|
+
message_events = [
|
|
328
|
+
e for e in session.events
|
|
329
|
+
if e.type == EventType.MESSAGE
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
# Take the most recent messages
|
|
333
|
+
recent_messages = message_events[-max_history:] if len(message_events) > max_history else message_events
|
|
334
|
+
|
|
335
|
+
# Build the conversation history
|
|
336
|
+
prompt = []
|
|
337
|
+
for msg in recent_messages:
|
|
338
|
+
role = "user" if msg.source == EventSource.USER else "assistant"
|
|
339
|
+
content = msg.message
|
|
340
|
+
|
|
341
|
+
# Handle different message formats
|
|
342
|
+
if isinstance(content, dict) and "content" in content:
|
|
343
|
+
content = content["content"]
|
|
344
|
+
|
|
345
|
+
# For the last assistant message, set content to None
|
|
346
|
+
if role == "assistant" and msg == recent_messages[-1] and msg.source != EventSource.USER:
|
|
347
|
+
content = None
|
|
348
|
+
|
|
349
|
+
# Add tool call results for this assistant message
|
|
350
|
+
tool_calls = [
|
|
351
|
+
e for e in session.events
|
|
352
|
+
if e.type == EventType.TOOL_CALL and e.metadata.get("parent_event_id") == msg.id
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
# Add the message first, then tools
|
|
356
|
+
prompt.append({"role": role, "content": content})
|
|
357
|
+
|
|
358
|
+
# Add tool results
|
|
359
|
+
for tc in tool_calls:
|
|
360
|
+
if isinstance(tc.message, dict):
|
|
361
|
+
tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
|
|
362
|
+
tool_result = tc.message.get("result", {})
|
|
363
|
+
|
|
364
|
+
prompt.append({
|
|
365
|
+
"role": "tool",
|
|
366
|
+
"name": tool_name,
|
|
367
|
+
"content": json.dumps(tool_result, default=str),
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
# Skip adding this message again
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
prompt.append({"role": role, "content": content})
|
|
374
|
+
|
|
375
|
+
return prompt
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async def _build_hierarchical_prompt(
|
|
379
|
+
session: Session,
|
|
380
|
+
include_parent_context: bool = True
|
|
381
|
+
) -> List[Dict[str, str]]:
|
|
382
|
+
"""
|
|
383
|
+
Build a prompt that includes hierarchical context.
|
|
384
|
+
|
|
385
|
+
This strategy leverages the session hierarchy:
|
|
386
|
+
- Starts with the minimal prompt
|
|
387
|
+
- Includes summaries from parent sessions if available
|
|
388
|
+
"""
|
|
389
|
+
# Start with the minimal prompt
|
|
390
|
+
prompt = await _build_minimal_prompt(session)
|
|
391
|
+
|
|
392
|
+
# If parent context is enabled and session has a parent
|
|
393
|
+
if include_parent_context and session.parent_id:
|
|
394
|
+
store = SessionStoreProvider.get_store()
|
|
395
|
+
parent = await store.get(session.parent_id)
|
|
396
|
+
|
|
397
|
+
if parent:
|
|
398
|
+
# Find the most recent summary in parent
|
|
399
|
+
summary_event = next(
|
|
400
|
+
(e for e in reversed(parent.events)
|
|
401
|
+
if e.type == EventType.SUMMARY),
|
|
402
|
+
None
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if summary_event:
|
|
406
|
+
# Extract summary content
|
|
407
|
+
summary_content = summary_event.message
|
|
408
|
+
if isinstance(summary_content, dict) and "note" in summary_content:
|
|
409
|
+
summary_content = summary_content["note"]
|
|
410
|
+
elif isinstance(summary_content, dict) and "content" in summary_content:
|
|
411
|
+
summary_content = summary_content["content"]
|
|
412
|
+
|
|
413
|
+
# Add parent context at the beginning
|
|
414
|
+
prompt.insert(0, {
|
|
415
|
+
"role": "system",
|
|
416
|
+
"content": f"Context from previous conversation: {summary_content}"
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
return prompt
|
|
420
|
+
|
|
421
|
+
async def truncate_prompt_to_token_limit(
|
|
422
|
+
prompt: List[Dict[str, str]],
|
|
423
|
+
max_tokens: int,
|
|
424
|
+
model: str = "gpt-3.5-turbo",
|
|
425
|
+
) -> List[Dict[str, str]]:
|
|
426
|
+
"""
|
|
427
|
+
Trim a prompt so its total token count is ≤ `max_tokens`.
|
|
428
|
+
|
|
429
|
+
Strategy:
|
|
430
|
+
• If already within limit → return unchanged
|
|
431
|
+
• Otherwise keep:
|
|
432
|
+
- the very first user message
|
|
433
|
+
- everything from the last assistant message onward
|
|
434
|
+
- (optionally) one tool message so the model still sees a result
|
|
435
|
+
"""
|
|
436
|
+
if not prompt:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
# ------------------------------------------------------------------ #
|
|
440
|
+
# quick overall count
|
|
441
|
+
text = "\n".join(f"{m.get('role', 'unknown')}: {m.get('content') or ''}" for m in prompt)
|
|
442
|
+
total = TokenUsage.count_tokens(text, model)
|
|
443
|
+
total = await total if asyncio.iscoroutine(total) else total
|
|
444
|
+
if total <= max_tokens:
|
|
445
|
+
return prompt
|
|
446
|
+
|
|
447
|
+
# ------------------------------------------------------------------ #
|
|
448
|
+
# decide which messages to keep
|
|
449
|
+
first_user_idx = next((i for i, m in enumerate(prompt) if m["role"] == "user"), None)
|
|
450
|
+
last_asst_idx = next(
|
|
451
|
+
(len(prompt) - 1 - i for i, m in enumerate(reversed(prompt)) if m["role"] == "assistant"),
|
|
452
|
+
None,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
kept: List[Dict[str, str]] = []
|
|
456
|
+
if first_user_idx is not None:
|
|
457
|
+
kept.append(prompt[first_user_idx])
|
|
458
|
+
if last_asst_idx is not None:
|
|
459
|
+
kept.extend(prompt[last_asst_idx:])
|
|
460
|
+
|
|
461
|
+
# ------------------------------------------------------------------ #
|
|
462
|
+
# re-count and maybe drop / add tool messages
|
|
463
|
+
remaining = TokenUsage.count_tokens(str(kept), model)
|
|
464
|
+
remaining = await remaining if asyncio.iscoroutine(remaining) else remaining
|
|
465
|
+
|
|
466
|
+
if remaining > max_tokens:
|
|
467
|
+
# remove any tool messages we just added
|
|
468
|
+
kept = [m for m in kept if m["role"] != "tool"]
|
|
469
|
+
# but guarantee at least one tool message (the first) if it’ll fit
|
|
470
|
+
first_tool = next((m for m in prompt if m["role"] == "tool"), None)
|
|
471
|
+
if first_tool:
|
|
472
|
+
kept.append(first_tool)
|
|
473
|
+
|
|
474
|
+
return kept
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# chuk_ai_session_manager/storage/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Storage module for the chuk session manager.
|
|
4
|
+
"""
|
|
5
|
+
# Import base components first to avoid circular imports
|
|
6
|
+
try:
|
|
7
|
+
from chuk_ai_session_manager.storage.base import SessionStoreInterface, SessionStoreProvider
|
|
8
|
+
except ImportError:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
# Try to import providers if available
|
|
12
|
+
try:
|
|
13
|
+
from chuk_ai_session_manager.storage.providers.memory import InMemorySessionStore
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from chuk_ai_session_manager.storage.providers.file import FileSessionStore, create_file_session_store
|
|
19
|
+
except ImportError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# Try to import Redis - this is optional
|
|
23
|
+
try:
|
|
24
|
+
from chuk_ai_session_manager.storage.providers.redis import RedisSessionStore, create_redis_session_store
|
|
25
|
+
_has_redis = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
_has_redis = False
|
|
28
|
+
|
|
29
|
+
# Define __all__ based on what was successfully imported
|
|
30
|
+
__all__ = []
|
|
31
|
+
|
|
32
|
+
# Basic components
|
|
33
|
+
for name in ['SessionStoreInterface', 'SessionStoreProvider', 'InMemorySessionStore']:
|
|
34
|
+
if name in globals():
|
|
35
|
+
__all__.append(name)
|
|
36
|
+
|
|
37
|
+
# File store
|
|
38
|
+
for name in ['FileSessionStore', 'create_file_session_store']:
|
|
39
|
+
if name in globals():
|
|
40
|
+
__all__.append(name)
|
|
41
|
+
|
|
42
|
+
# Redis store (optional)
|
|
43
|
+
if _has_redis:
|
|
44
|
+
__all__.extend(['RedisSessionStore', 'create_redis_session_store'])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# chuk_ai_session_manager/storage/base.py
|
|
2
|
+
"""
|
|
3
|
+
Base interfaces and providers for async session storage.
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, List, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T')
|
|
9
|
+
|
|
10
|
+
class SessionStoreInterface(ABC):
|
|
11
|
+
"""Interface for pluggable async session stores."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def get(self, session_id: str) -> Optional[Any]:
|
|
15
|
+
"""Retrieve a session by its ID, or None if not found."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def save(self, session: Any) -> None:
|
|
20
|
+
"""Save or update a session object in the store."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def delete(self, session_id: str) -> None:
|
|
25
|
+
"""Delete a session by its ID."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def list_sessions(self, prefix: str = "") -> List[str]:
|
|
30
|
+
"""List all session IDs, optionally filtered by prefix."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionStoreProvider:
|
|
35
|
+
"""Provider for a globally-shared async session store."""
|
|
36
|
+
_store: Optional[SessionStoreInterface] = None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_store(cls) -> SessionStoreInterface:
|
|
40
|
+
"""Get the currently configured session store."""
|
|
41
|
+
if cls._store is None:
|
|
42
|
+
# Defer import to avoid circular imports
|
|
43
|
+
from chuk_ai_session_manager.storage.providers.memory import InMemorySessionStore
|
|
44
|
+
cls._store = InMemorySessionStore()
|
|
45
|
+
return cls._store
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def set_store(cls, store: SessionStoreInterface) -> None:
|
|
49
|
+
"""Set a new session store implementation."""
|
|
50
|
+
cls._store = store
|
|
File without changes
|