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.
Files changed (46) hide show
  1. chuk_ai_session_manager/__init__.py +84 -40
  2. chuk_ai_session_manager/api/__init__.py +1 -1
  3. chuk_ai_session_manager/api/simple_api.py +53 -59
  4. chuk_ai_session_manager/exceptions.py +31 -17
  5. chuk_ai_session_manager/guards/__init__.py +118 -0
  6. chuk_ai_session_manager/guards/bindings.py +217 -0
  7. chuk_ai_session_manager/guards/cache.py +163 -0
  8. chuk_ai_session_manager/guards/manager.py +819 -0
  9. chuk_ai_session_manager/guards/models.py +498 -0
  10. chuk_ai_session_manager/guards/ungrounded.py +159 -0
  11. chuk_ai_session_manager/infinite_conversation.py +86 -79
  12. chuk_ai_session_manager/memory/__init__.py +247 -0
  13. chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
  14. chuk_ai_session_manager/memory/context_packer.py +347 -0
  15. chuk_ai_session_manager/memory/fault_handler.py +507 -0
  16. chuk_ai_session_manager/memory/manifest.py +307 -0
  17. chuk_ai_session_manager/memory/models.py +1084 -0
  18. chuk_ai_session_manager/memory/mutation_log.py +186 -0
  19. chuk_ai_session_manager/memory/pack_cache.py +206 -0
  20. chuk_ai_session_manager/memory/page_table.py +275 -0
  21. chuk_ai_session_manager/memory/prefetcher.py +192 -0
  22. chuk_ai_session_manager/memory/tlb.py +247 -0
  23. chuk_ai_session_manager/memory/vm_prompts.py +238 -0
  24. chuk_ai_session_manager/memory/working_set.py +574 -0
  25. chuk_ai_session_manager/models/__init__.py +21 -9
  26. chuk_ai_session_manager/models/event_source.py +3 -1
  27. chuk_ai_session_manager/models/event_type.py +10 -1
  28. chuk_ai_session_manager/models/session.py +103 -68
  29. chuk_ai_session_manager/models/session_event.py +69 -68
  30. chuk_ai_session_manager/models/session_metadata.py +9 -10
  31. chuk_ai_session_manager/models/session_run.py +21 -22
  32. chuk_ai_session_manager/models/token_usage.py +76 -76
  33. chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
  34. chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
  35. chuk_ai_session_manager/procedural_memory/manager.py +523 -0
  36. chuk_ai_session_manager/procedural_memory/models.py +371 -0
  37. chuk_ai_session_manager/sample_tools.py +79 -46
  38. chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
  39. chuk_ai_session_manager/session_manager.py +238 -197
  40. chuk_ai_session_manager/session_prompt_builder.py +163 -111
  41. chuk_ai_session_manager/session_storage.py +45 -52
  42. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/METADATA +79 -3
  43. chuk_ai_session_manager-0.8.dist-info/RECORD +45 -0
  44. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/WHEEL +1 -1
  45. chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
  46. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.dist-info}/top_level.txt +0 -0
@@ -10,9 +10,9 @@ and hierarchical context awareness.
10
10
  from __future__ import annotations
11
11
  import json
12
12
  import logging
13
- from typing import List, Dict, Any, Optional, Literal, Union
13
+ from typing import List, Dict, Any, Optional, Union
14
14
  from enum import Enum
15
- import asyncio
15
+ import asyncio
16
16
 
17
17
  from chuk_ai_session_manager.models.session import Session
18
18
  from chuk_ai_session_manager.models.event_type import EventType
@@ -22,11 +22,13 @@ from chuk_ai_session_manager.session_storage import get_backend, ChukSessionsSto
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
+
25
26
  class PromptStrategy(str, Enum):
26
27
  """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
28
+
29
+ MINIMAL = "minimal" # Original minimal approach
30
+ TASK_FOCUSED = "task" # Focus on the task with minimal context
31
+ TOOL_FOCUSED = "tool" # Emphasize tool usage and results
30
32
  CONVERSATION = "conversation" # Include more conversation history
31
33
  HIERARCHICAL = "hierarchical" # Include parent session context
32
34
 
@@ -38,11 +40,11 @@ async def build_prompt_from_session(
38
40
  model: str = "gpt-3.5-turbo",
39
41
  include_parent_context: bool = False,
40
42
  current_query: Optional[str] = None,
41
- max_history: int = 5 # Add this parameter for conversation strategy
43
+ max_history: int = 5, # Add this parameter for conversation strategy
42
44
  ) -> List[Dict[str, str]]:
43
45
  """
44
46
  Build a prompt for the next LLM call from a Session asynchronously.
45
-
47
+
46
48
  Args:
47
49
  session: The session to build a prompt from
48
50
  strategy: Prompt building strategy to use
@@ -51,13 +53,13 @@ async def build_prompt_from_session(
51
53
  include_parent_context: Whether to include context from parent sessions
52
54
  current_query: Current user query for relevance-based context selection
53
55
  max_history: Maximum number of messages to include for conversation strategy
54
-
56
+
55
57
  Returns:
56
58
  A list of message dictionaries suitable for LLM API calls
57
59
  """
58
60
  if not session.events:
59
61
  return []
60
-
62
+
61
63
  # Convert string strategy to enum if needed
62
64
  if isinstance(strategy, str):
63
65
  try:
@@ -65,7 +67,7 @@ async def build_prompt_from_session(
65
67
  except ValueError:
66
68
  logger.warning(f"Unknown strategy '{strategy}', falling back to MINIMAL")
67
69
  strategy = PromptStrategy.MINIMAL
68
-
70
+
69
71
  # Use the appropriate strategy
70
72
  if strategy == PromptStrategy.MINIMAL:
71
73
  return await _build_minimal_prompt(session)
@@ -85,7 +87,7 @@ async def build_prompt_from_session(
85
87
  async def _build_minimal_prompt(session: Session) -> List[Dict[str, str]]:
86
88
  """
87
89
  Build a minimal prompt from a session.
88
-
90
+
89
91
  This follows the original implementation's approach:
90
92
  - Include the first USER message (task)
91
93
  - Include the latest assistant MESSAGE with content set to None
@@ -111,10 +113,14 @@ async def _build_minimal_prompt(session: Session) -> List[Dict[str, str]]:
111
113
  ),
112
114
  None,
113
115
  )
114
-
116
+
115
117
  if assistant_msg is None:
116
118
  # Only the user message exists so far
117
- return [{"role": "user", "content": _extract_content(first_user.message)}] if first_user else []
119
+ return (
120
+ [{"role": "user", "content": _extract_content(first_user.message)}]
121
+ if first_user
122
+ else []
123
+ )
118
124
 
119
125
  # Children of that assistant
120
126
  children = [
@@ -138,7 +144,9 @@ async def _build_minimal_prompt(session: Session) -> List[Dict[str, str]]:
138
144
  # Extract relevant information from the tool call
139
145
  # Handle both new and legacy formats
140
146
  if isinstance(tc.message, dict):
141
- tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
147
+ tool_name = tc.message.get(
148
+ "tool_name", tc.message.get("tool", "unknown")
149
+ )
142
150
  tool_result = tc.message.get("result", {})
143
151
  else:
144
152
  # Legacy format or unexpected type
@@ -167,10 +175,10 @@ async def _build_minimal_prompt(session: Session) -> List[Dict[str, str]]:
167
175
  def _extract_content(message: Any) -> str:
168
176
  """
169
177
  Extract content string from a message that could be a string or dict.
170
-
178
+
171
179
  Args:
172
180
  message: The message content (string, dict, or other)
173
-
181
+
174
182
  Returns:
175
183
  The extracted content as a string
176
184
  """
@@ -185,7 +193,7 @@ def _extract_content(message: Any) -> str:
185
193
  async def _build_task_focused_prompt(session: Session) -> List[Dict[str, str]]:
186
194
  """
187
195
  Build a task-focused prompt.
188
-
196
+
189
197
  This strategy emphasizes the original task and latest context:
190
198
  - Includes the first USER message as the main task
191
199
  - Includes the most recent USER message for current context
@@ -193,16 +201,17 @@ async def _build_task_focused_prompt(session: Session) -> List[Dict[str, str]]:
193
201
  """
194
202
  # Get first and most recent user messages
195
203
  user_messages = [
196
- e for e in session.events
204
+ e
205
+ for e in session.events
197
206
  if e.type == EventType.MESSAGE and e.source == EventSource.USER
198
207
  ]
199
-
208
+
200
209
  if not user_messages:
201
210
  return []
202
-
211
+
203
212
  first_user = user_messages[0]
204
213
  latest_user = user_messages[-1] if len(user_messages) > 1 else None
205
-
214
+
206
215
  # Latest assistant MESSAGE
207
216
  assistant_msg = next(
208
217
  (
@@ -212,52 +221,62 @@ async def _build_task_focused_prompt(session: Session) -> List[Dict[str, str]]:
212
221
  ),
213
222
  None,
214
223
  )
215
-
224
+
216
225
  # Build prompt
217
226
  prompt = []
218
-
227
+
219
228
  # Always include the first user message (the main task)
220
229
  prompt.append({"role": "user", "content": _extract_content(first_user.message)})
221
-
230
+
222
231
  # Include the latest user message if different from the first
223
232
  if latest_user and latest_user.id != first_user.id:
224
- prompt.append({"role": "user", "content": _extract_content(latest_user.message)})
225
-
233
+ prompt.append(
234
+ {"role": "user", "content": _extract_content(latest_user.message)}
235
+ )
236
+
226
237
  # Include assistant response placeholder
227
238
  if assistant_msg:
228
239
  prompt.append({"role": "assistant", "content": None})
229
-
240
+
230
241
  # Find successful tool calls
231
242
  children = [
232
- e for e in session.events
243
+ e
244
+ for e in session.events
233
245
  if e.metadata.get("parent_event_id") == assistant_msg.id
234
246
  ]
235
247
  tool_calls = [c for c in children if c.type == EventType.TOOL_CALL]
236
-
248
+
237
249
  # Only include successful tool results
238
250
  for tc in tool_calls:
239
251
  # Extract and check if result indicates success
240
252
  if isinstance(tc.message, dict):
241
- tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
253
+ tool_name = tc.message.get(
254
+ "tool_name", tc.message.get("tool", "unknown")
255
+ )
242
256
  tool_result = tc.message.get("result", {})
243
-
257
+
244
258
  # Skip error results
245
- if isinstance(tool_result, dict) and tool_result.get("status") == "error":
259
+ if (
260
+ isinstance(tool_result, dict)
261
+ and tool_result.get("status") == "error"
262
+ ):
246
263
  continue
247
-
248
- prompt.append({
249
- "role": "tool",
250
- "name": tool_name,
251
- "content": json.dumps(tool_result, default=str),
252
- })
253
-
264
+
265
+ prompt.append(
266
+ {
267
+ "role": "tool",
268
+ "name": tool_name,
269
+ "content": json.dumps(tool_result, default=str),
270
+ }
271
+ )
272
+
254
273
  return prompt
255
274
 
256
275
 
257
276
  async def _build_tool_focused_prompt(session: Session) -> List[Dict[str, str]]:
258
277
  """
259
278
  Build a tool-focused prompt.
260
-
279
+
261
280
  This strategy emphasizes tool usage:
262
281
  - Includes the latest user query
263
282
  - Includes detailed information about tool calls and results
@@ -265,132 +284,149 @@ async def _build_tool_focused_prompt(session: Session) -> List[Dict[str, str]]:
265
284
  """
266
285
  # Get the latest user message
267
286
  latest_user = next(
268
- (e for e in reversed(session.events)
269
- if e.type == EventType.MESSAGE and e.source == EventSource.USER),
270
- None
287
+ (
288
+ e
289
+ for e in reversed(session.events)
290
+ if e.type == EventType.MESSAGE and e.source == EventSource.USER
291
+ ),
292
+ None,
271
293
  )
272
-
294
+
273
295
  if not latest_user:
274
296
  return []
275
-
297
+
276
298
  # Get the latest assistant message
277
299
  assistant_msg = next(
278
- (ev for ev in reversed(session.events)
279
- if ev.type == EventType.MESSAGE and ev.source != EventSource.USER),
280
- None
300
+ (
301
+ ev
302
+ for ev in reversed(session.events)
303
+ if ev.type == EventType.MESSAGE and ev.source != EventSource.USER
304
+ ),
305
+ None,
281
306
  )
282
-
307
+
283
308
  # Build prompt
284
309
  prompt = []
285
-
310
+
286
311
  # Include user message
287
312
  prompt.append({"role": "user", "content": _extract_content(latest_user.message)})
288
-
313
+
289
314
  # Include assistant placeholder
290
315
  if assistant_msg:
291
316
  prompt.append({"role": "assistant", "content": None})
292
-
317
+
293
318
  # Get all tool calls for this assistant
294
319
  children = [
295
- e for e in session.events
320
+ e
321
+ for e in session.events
296
322
  if e.metadata.get("parent_event_id") == assistant_msg.id
297
323
  ]
298
324
  tool_calls = [c for c in children if c.type == EventType.TOOL_CALL]
299
-
325
+
300
326
  # Add all tool calls with status information
301
327
  for tc in tool_calls:
302
328
  if isinstance(tc.message, dict):
303
- tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
329
+ tool_name = tc.message.get(
330
+ "tool_name", tc.message.get("tool", "unknown")
331
+ )
304
332
  tool_result = tc.message.get("result", {})
305
333
  error = tc.message.get("error", None)
306
-
334
+
307
335
  # Include status information in the tool response
308
336
  content = tool_result
309
337
  if error:
310
338
  content = {"error": error, "details": tool_result}
311
-
312
- prompt.append({
313
- "role": "tool",
314
- "name": tool_name,
315
- "content": json.dumps(content, default=str),
316
- })
317
-
339
+
340
+ prompt.append(
341
+ {
342
+ "role": "tool",
343
+ "name": tool_name,
344
+ "content": json.dumps(content, default=str),
345
+ }
346
+ )
347
+
318
348
  return prompt
319
349
 
320
350
 
321
351
  async def _build_conversation_prompt(
322
- session: Session,
323
- max_history: int = 5
352
+ session: Session, max_history: int = 5
324
353
  ) -> List[Dict[str, str]]:
325
354
  """
326
355
  Build a conversation-style prompt with recent history.
327
-
356
+
328
357
  This strategy creates a more natural conversation:
329
358
  - Includes up to max_history recent messages in order
330
359
  - Preserves conversation flow
331
360
  - Still handles tool calls appropriately
332
361
  """
333
362
  # Get relevant message events
334
- message_events = [
335
- e for e in session.events
336
- if e.type == EventType.MESSAGE
337
- ]
338
-
363
+ message_events = [e for e in session.events if e.type == EventType.MESSAGE]
364
+
339
365
  # Take the most recent messages
340
- recent_messages = message_events[-max_history:] if len(message_events) > max_history else message_events
341
-
366
+ recent_messages = (
367
+ message_events[-max_history:]
368
+ if len(message_events) > max_history
369
+ else message_events
370
+ )
371
+
342
372
  # Build the conversation history
343
373
  prompt = []
344
374
  for i, msg in enumerate(recent_messages):
345
375
  role = "user" if msg.source == EventSource.USER else "assistant"
346
376
  content = _extract_content(msg.message)
347
-
377
+
348
378
  # For the last assistant message, set content to None and add tool calls
349
- if (role == "assistant" and
350
- msg == recent_messages[-1] and
351
- msg.source != EventSource.USER):
352
-
379
+ if (
380
+ role == "assistant"
381
+ and msg == recent_messages[-1]
382
+ and msg.source != EventSource.USER
383
+ ):
353
384
  # Add the message first with None content
354
385
  prompt.append({"role": role, "content": None})
355
-
386
+
356
387
  # Add tool call results for this assistant message
357
388
  tool_calls = [
358
- e for e in session.events
359
- if e.type == EventType.TOOL_CALL and e.metadata.get("parent_event_id") == msg.id
389
+ e
390
+ for e in session.events
391
+ if e.type == EventType.TOOL_CALL
392
+ and e.metadata.get("parent_event_id") == msg.id
360
393
  ]
361
-
394
+
362
395
  # Add tool results
363
396
  for tc in tool_calls:
364
397
  if isinstance(tc.message, dict):
365
- tool_name = tc.message.get("tool_name", tc.message.get("tool", "unknown"))
398
+ tool_name = tc.message.get(
399
+ "tool_name", tc.message.get("tool", "unknown")
400
+ )
366
401
  tool_result = tc.message.get("result", {})
367
-
368
- prompt.append({
369
- "role": "tool",
370
- "name": tool_name,
371
- "content": json.dumps(tool_result, default=str),
372
- })
402
+
403
+ prompt.append(
404
+ {
405
+ "role": "tool",
406
+ "name": tool_name,
407
+ "content": json.dumps(tool_result, default=str),
408
+ }
409
+ )
373
410
  else:
374
411
  # Regular message
375
412
  prompt.append({"role": role, "content": content})
376
-
413
+
377
414
  return prompt
378
415
 
379
416
 
380
417
  async def _build_hierarchical_prompt(
381
- session: Session,
382
- include_parent_context: bool = True
418
+ session: Session, include_parent_context: bool = True
383
419
  ) -> List[Dict[str, str]]:
384
420
  """
385
421
  Build a prompt that includes hierarchical context.
386
-
422
+
387
423
  This strategy leverages the session hierarchy:
388
424
  - Starts with the minimal prompt
389
425
  - Includes summaries from parent sessions if available
390
426
  """
391
427
  # Start with the minimal prompt
392
428
  prompt = await _build_minimal_prompt(session)
393
-
429
+
394
430
  # If parent context is enabled and session has a parent
395
431
  if include_parent_context and session.parent_id:
396
432
  try:
@@ -398,36 +434,44 @@ async def _build_hierarchical_prompt(
398
434
  backend = get_backend()
399
435
  store = ChukSessionsStore(backend)
400
436
  parent = await store.get(session.parent_id)
401
-
437
+
402
438
  if parent:
403
439
  # Find the most recent summary in parent
404
440
  summary_event = next(
405
- (e for e in reversed(parent.events)
406
- if e.type == EventType.SUMMARY),
407
- None
441
+ (e for e in reversed(parent.events) if e.type == EventType.SUMMARY),
442
+ None,
408
443
  )
409
-
444
+
410
445
  if summary_event:
411
446
  # Extract summary content
412
447
  summary_content = summary_event.message
413
448
  if isinstance(summary_content, dict) and "note" in summary_content:
414
449
  summary_content = summary_content["note"]
415
- elif isinstance(summary_content, dict) and "content" in summary_content:
450
+ elif (
451
+ isinstance(summary_content, dict)
452
+ and "content" in summary_content
453
+ ):
416
454
  summary_content = summary_content["content"]
417
455
  else:
418
456
  summary_content = str(summary_content)
419
-
457
+
420
458
  # Add parent context at the beginning
421
- prompt.insert(0, {
422
- "role": "system",
423
- "content": f"Context from previous conversation: {summary_content}"
424
- })
459
+ prompt.insert(
460
+ 0,
461
+ {
462
+ "role": "system",
463
+ "content": f"Context from previous conversation: {summary_content}",
464
+ },
465
+ )
425
466
  except Exception as e:
426
467
  # If we can't load parent context, just continue with minimal prompt
427
- logger.warning(f"Could not load parent context for session {session.parent_id}: {e}")
428
-
468
+ logger.warning(
469
+ f"Could not load parent context for session {session.parent_id}: {e}"
470
+ )
471
+
429
472
  return prompt
430
473
 
474
+
431
475
  async def truncate_prompt_to_token_limit(
432
476
  prompt: List[Dict[str, str]],
433
477
  max_tokens: int,
@@ -448,7 +492,9 @@ async def truncate_prompt_to_token_limit(
448
492
 
449
493
  # ------------------------------------------------------------------ #
450
494
  # quick overall count
451
- text = "\n".join(f"{m.get('role', 'unknown')}: {m.get('content') or ''}" for m in prompt)
495
+ text = "\n".join(
496
+ f"{m.get('role', 'unknown')}: {m.get('content') or ''}" for m in prompt
497
+ )
452
498
  total = TokenUsage.count_tokens(text, model)
453
499
  total = await total if asyncio.iscoroutine(total) else total
454
500
  if total <= max_tokens:
@@ -456,9 +502,15 @@ async def truncate_prompt_to_token_limit(
456
502
 
457
503
  # ------------------------------------------------------------------ #
458
504
  # decide which messages to keep
459
- first_user_idx = next((i for i, m in enumerate(prompt) if m["role"] == "user"), None)
505
+ first_user_idx = next(
506
+ (i for i, m in enumerate(prompt) if m["role"] == "user"), None
507
+ )
460
508
  last_asst_idx = next(
461
- (len(prompt) - 1 - i for i, m in enumerate(reversed(prompt)) if m["role"] == "assistant"),
509
+ (
510
+ len(prompt) - 1 - i
511
+ for i, m in enumerate(reversed(prompt))
512
+ if m["role"] == "assistant"
513
+ ),
462
514
  None,
463
515
  )
464
516
 
@@ -481,4 +533,4 @@ async def truncate_prompt_to_token_limit(
481
533
  if first_tool:
482
534
  kept.append(first_tool)
483
535
 
484
- return kept
536
+ return kept