chuk-ai-session-manager 0.4__py3-none-any.whl → 0.4.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.
@@ -1,4 +1,4 @@
1
- # chuk_ai_session_manager/models/session.py
1
+ # src/chuk_ai_session_manager/models/session.py
2
2
  """
3
3
  Session model for the chuk session manager with improved async support.
4
4
  """
@@ -186,7 +186,8 @@ class Session(BaseModel, Generic[MessageT]):
186
186
  if not event.token_usage:
187
187
  continue
188
188
 
189
- source = event.source.value
189
+ # Use the string value of the enum for the key
190
+ source = event.source.value if hasattr(event.source, 'value') else str(event.source)
190
191
  if source not in result:
191
192
  result[source] = TokenSummary()
192
193
 
@@ -302,18 +303,24 @@ class Session(BaseModel, Generic[MessageT]):
302
303
  # await store.save(self)
303
304
 
304
305
  @classmethod
305
- async def create(cls, parent_id: Optional[str] = None, **kwargs) -> Session:
306
+ async def create(cls, session_id: Optional[str] = None, parent_id: Optional[str] = None, **kwargs) -> Session:
306
307
  """
307
308
  Create a new session asynchronously, handling parent-child relationships.
308
309
 
309
310
  Args:
311
+ session_id: Optional session ID to use (if not provided, generates a new one)
310
312
  parent_id: Optional parent session ID
311
313
  **kwargs: Additional arguments for Session initialization
312
314
 
313
315
  Returns:
314
316
  A new Session instance with parent-child relationships set up
315
317
  """
316
- session = cls(parent_id=parent_id, **kwargs)
318
+ # Allow passing a specific session ID
319
+ if session_id:
320
+ session = cls(id=session_id, parent_id=parent_id, **kwargs)
321
+ else:
322
+ session = cls(parent_id=parent_id, **kwargs)
323
+
317
324
  await session.async_init()
318
325
 
319
326
  # Save the new session
@@ -1,34 +1,41 @@
1
- # chuk_ai_session_manager/models/session_event.py
1
+ # src/chuk_ai_session_manager/models/session_event.py
2
2
  """
3
3
  Session event model for the chuk session manager with improved async support.
4
4
  """
5
5
  from __future__ import annotations
6
6
  from datetime import datetime, timezone
7
- from typing import Any, Dict, Generic, Optional, TypeVar, Union
7
+ from typing import Any, Dict, Generic, Optional, TypeVar
8
8
  from uuid import uuid4
9
9
  from pydantic import BaseModel, Field, ConfigDict
10
10
 
11
- # session manager
12
11
  from chuk_ai_session_manager.models.event_source import EventSource
13
12
  from chuk_ai_session_manager.models.event_type import EventType
14
13
  from chuk_ai_session_manager.models.token_usage import TokenUsage
15
14
 
16
- # Generic type for event message content
17
15
  MessageT = TypeVar('MessageT')
18
16
 
19
17
  class SessionEvent(BaseModel, Generic[MessageT]):
20
- """An event in a session."""
18
+ """
19
+ A single event within a session.
20
+
21
+ Events track all interactions in a session including messages,
22
+ tool calls, summaries, and other activities.
23
+ """
21
24
  model_config = ConfigDict(arbitrary_types_allowed=True)
22
-
25
+
23
26
  id: str = Field(default_factory=lambda: str(uuid4()))
27
+ message: MessageT
24
28
  timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
25
- message: Optional[MessageT] = None
26
- task_id: Optional[str] = None
27
- type: EventType = EventType.MESSAGE
28
- source: EventSource = EventSource.LLM
29
+
30
+ # Make source and type have defaults for backward compatibility with tests
31
+ source: EventSource = Field(default=EventSource.SYSTEM)
32
+ type: EventType = Field(default=EventType.MESSAGE)
33
+
29
34
  metadata: Dict[str, Any] = Field(default_factory=dict)
35
+ parent_event_id: Optional[str] = None
36
+ task_id: Optional[str] = None
30
37
 
31
- # Field for token usage tracking
38
+ # Token tracking
32
39
  token_usage: Optional[TokenUsage] = None
33
40
 
34
41
  @classmethod
@@ -38,116 +45,126 @@ class SessionEvent(BaseModel, Generic[MessageT]):
38
45
  prompt: str,
39
46
  completion: Optional[str] = None,
40
47
  model: str = "gpt-3.5-turbo",
41
- source: EventSource = EventSource.LLM,
48
+ source: EventSource = EventSource.SYSTEM,
42
49
  type: EventType = EventType.MESSAGE,
43
- task_id: Optional[str] = None,
44
- metadata: Optional[Dict[str, Any]] = None
50
+ **kwargs
45
51
  ) -> SessionEvent[MessageT]:
46
52
  """
47
- Create a session event with automatic token counting asynchronously.
53
+ Create a new SessionEvent with automatic token counting.
48
54
 
49
55
  Args:
50
56
  message: The message content
51
- prompt: The prompt text used (for token counting)
52
- completion: The completion text (for token counting)
53
- model: The model used for this interaction
54
- source: The source of this event
55
- type: The type of this event
56
- task_id: Optional task ID this event is associated with
57
- metadata: Optional additional metadata
57
+ prompt: The prompt text for token counting
58
+ completion: Optional completion text for token counting
59
+ model: The model to use for token counting
60
+ source: The event source
61
+ type: The event type
62
+ **kwargs: Additional fields for the event
58
63
 
59
64
  Returns:
60
- A new SessionEvent with token usage information
65
+ A new SessionEvent instance with token usage calculated
61
66
  """
62
- # Use the async method of TokenUsage
63
- token_usage = await TokenUsage.from_text(prompt, completion, model)
67
+ # Create token usage
68
+ token_usage = await TokenUsage.from_text(
69
+ prompt=prompt,
70
+ completion=completion,
71
+ model=model
72
+ )
64
73
 
65
74
  # Create the event
66
75
  event = cls(
67
76
  message=message,
68
- task_id=task_id,
69
- type=type,
70
77
  source=source,
71
- metadata=metadata or {},
72
- token_usage=token_usage
78
+ type=type,
79
+ token_usage=token_usage,
80
+ **kwargs
73
81
  )
74
82
 
75
83
  return event
76
84
 
77
85
  async def update_token_usage(
78
86
  self,
79
- prompt: Optional[str] = None,
87
+ prompt: Optional[str] = None,
80
88
  completion: Optional[str] = None,
81
- model: Optional[str] = None
89
+ prompt_tokens: Optional[int] = None,
90
+ completion_tokens: Optional[int] = None,
91
+ model: str = "gpt-3.5-turbo"
82
92
  ) -> None:
83
93
  """
84
- Update token usage information for this event.
94
+ Update the token usage for this event.
95
+
96
+ This method supports two modes:
97
+ 1. Pass prompt/completion strings to calculate tokens
98
+ 2. Pass prompt_tokens/completion_tokens directly
85
99
 
86
100
  Args:
87
- prompt: The prompt text to count tokens for
88
- completion: The completion text to count tokens for
89
- model: The model to use for counting tokens
101
+ prompt: Optional prompt text to calculate tokens from
102
+ completion: Optional completion text to calculate tokens from
103
+ prompt_tokens: Optional number of prompt tokens (if already calculated)
104
+ completion_tokens: Optional number of completion tokens (if already calculated)
105
+ model: The model used for token calculation
90
106
  """
91
- # If we don't have token_usage yet, create it
92
- if self.token_usage is None:
93
- self.token_usage = TokenUsage(model=model or "")
94
-
95
- # If model is provided, update it
96
- if model and not self.token_usage.model:
97
- self.token_usage.model = model
98
-
99
- # Calculate tokens if text is provided
100
- if prompt:
101
- # Use async method for token counting
102
- prompt_tokens = await TokenUsage.count_tokens(prompt, self.token_usage.model)
103
- self.token_usage.prompt_tokens = prompt_tokens
107
+ if prompt is not None or completion is not None:
108
+ # Calculate tokens from text
109
+ self.token_usage = await TokenUsage.from_text(
110
+ prompt=prompt or "",
111
+ completion=completion,
112
+ model=model
113
+ )
114
+ elif prompt_tokens is not None or completion_tokens is not None:
115
+ # Use provided token counts
116
+ if not self.token_usage:
117
+ self.token_usage = TokenUsage(model=model)
104
118
 
105
- if completion:
106
- # Use async method for token counting
107
- completion_tokens = await TokenUsage.count_tokens(completion, self.token_usage.model)
108
- self.token_usage.completion_tokens = completion_tokens
119
+ if prompt_tokens is not None:
120
+ self.token_usage.prompt_tokens = prompt_tokens
121
+ if completion_tokens is not None:
122
+ self.token_usage.completion_tokens = completion_tokens
109
123
 
110
- # Recalculate totals
111
- self.token_usage.total_tokens = self.token_usage.prompt_tokens + self.token_usage.completion_tokens
112
- if self.token_usage.model:
113
- # Use async method for cost calculation
114
- self.token_usage.estimated_cost_usd = await self.token_usage.calculate_cost()
124
+ # Update total
125
+ self.token_usage.total_tokens = self.token_usage.prompt_tokens + self.token_usage.completion_tokens
115
126
 
116
- # Metadata async methods with clean names
117
- async def get_metadata(self, key: str, default: Any = None) -> Any:
118
- """Get a metadata value.
119
-
120
- Args:
121
- key: The metadata key to retrieve
122
- default: Default value to return if key not found
123
-
124
- Returns:
125
- The metadata value or default if not found
126
- """
127
- return self.metadata.get(key, default)
128
-
127
+ # Recalculate cost
128
+ self.token_usage.estimated_cost_usd = self.token_usage._calculate_cost_sync()
129
+
129
130
  async def set_metadata(self, key: str, value: Any) -> None:
130
- """Set a metadata value.
131
+ """
132
+ Set a metadata value asynchronously.
131
133
 
132
134
  Args:
133
- key: The metadata key to set
135
+ key: The metadata key
134
136
  value: The value to set
135
137
  """
136
138
  self.metadata[key] = value
139
+
140
+ async def get_metadata(self, key: str, default: Any = None) -> Any:
141
+ """
142
+ Get a metadata value asynchronously.
137
143
 
144
+ Args:
145
+ key: The metadata key
146
+ default: Default value if key not found
147
+
148
+ Returns:
149
+ The metadata value or default
150
+ """
151
+ return self.metadata.get(key, default)
152
+
138
153
  async def has_metadata(self, key: str) -> bool:
139
- """Check if a metadata key exists.
154
+ """
155
+ Check if a metadata key exists asynchronously.
140
156
 
141
157
  Args:
142
158
  key: The metadata key to check
143
159
 
144
160
  Returns:
145
- True if the key exists in metadata
161
+ True if the key exists
146
162
  """
147
163
  return key in self.metadata
148
-
164
+
149
165
  async def remove_metadata(self, key: str) -> None:
150
- """Remove a metadata key-value pair.
166
+ """
167
+ Remove a metadata key-value pair asynchronously.
151
168
 
152
169
  Args:
153
170
  key: The metadata key to remove
@@ -155,12 +172,99 @@ class SessionEvent(BaseModel, Generic[MessageT]):
155
172
  if key in self.metadata:
156
173
  del self.metadata[key]
157
174
 
158
- # Alternative async method for updating metadata for backward compatibility
159
175
  async def update_metadata(self, key: str, value: Any) -> None:
160
- """Update a metadata value (alias for set_metadata).
176
+ """
177
+ Update or add a metadata value asynchronously.
161
178
 
162
179
  Args:
163
- key: The metadata key to set
164
- value: The value to set
180
+ key: The metadata key
181
+ value: The new value
165
182
  """
166
- await self.set_metadata(key, value)
183
+ self.metadata[key] = value
184
+
185
+ async def merge_metadata(self, new_metadata: Dict[str, Any]) -> None:
186
+ """
187
+ Merge new metadata with existing metadata asynchronously.
188
+
189
+ Args:
190
+ new_metadata: Dictionary of metadata to merge
191
+ """
192
+ self.metadata.update(new_metadata)
193
+
194
+ async def clear_metadata(self) -> None:
195
+ """Clear all metadata asynchronously."""
196
+ self.metadata.clear()
197
+
198
+ async def calculate_tokens(self, model: str = "gpt-3.5-turbo") -> int:
199
+ """
200
+ Calculate tokens for this event's message asynchronously.
201
+
202
+ Args:
203
+ model: The model to use for token counting
204
+
205
+ Returns:
206
+ The number of tokens in the message
207
+ """
208
+ if self.token_usage:
209
+ return self.token_usage.total_tokens
210
+
211
+ # Calculate tokens from message
212
+ message_str = str(self.message) if not isinstance(self.message, str) else self.message
213
+ return await TokenUsage.count_tokens(message_str, model)
214
+
215
+ def is_child_of(self, parent_event_id: str) -> bool:
216
+ """
217
+ Check if this event is a child of another event.
218
+
219
+ Args:
220
+ parent_event_id: The ID of the potential parent event
221
+
222
+ Returns:
223
+ True if this event is a child of the specified event
224
+ """
225
+ return self.parent_event_id == parent_event_id
226
+
227
+ def is_part_of_task(self, task_id: str) -> bool:
228
+ """
229
+ Check if this event is part of a specific task.
230
+
231
+ Args:
232
+ task_id: The task ID to check
233
+
234
+ Returns:
235
+ True if this event is part of the specified task
236
+ """
237
+ return self.task_id == task_id
238
+
239
+ async def to_dict(self) -> Dict[str, Any]:
240
+ """
241
+ Convert the event to a dictionary asynchronously.
242
+
243
+ Returns:
244
+ Dictionary representation of the event
245
+ """
246
+ result = {
247
+ "id": self.id,
248
+ "message": self.message,
249
+ "timestamp": self.timestamp.isoformat(),
250
+ "source": self.source.value,
251
+ "type": self.type.value,
252
+ "metadata": self.metadata
253
+ }
254
+
255
+ if self.parent_event_id:
256
+ result["parent_event_id"] = self.parent_event_id
257
+
258
+ if self.task_id:
259
+ result["task_id"] = self.task_id
260
+
261
+ if self.token_usage:
262
+ result["token_usage"] = {
263
+ "prompt_tokens": self.token_usage.prompt_tokens,
264
+ "completion_tokens": self.token_usage.completion_tokens,
265
+ "total_tokens": self.token_usage.total_tokens,
266
+ "model": self.token_usage.model,
267
+ "estimated_cost_usd": self.token_usage.estimated_cost_usd
268
+ }
269
+
270
+ return result