kader 0.1.5__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.
@@ -0,0 +1,385 @@
1
+ """
2
+ Session management for agents.
3
+
4
+ Provides session persistence with file-based storage for
5
+ agent state and conversation history.
6
+ """
7
+
8
+ import uuid
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from .state import AgentState
15
+ from .types import (
16
+ MemoryConfig,
17
+ SessionType,
18
+ get_timestamp,
19
+ load_json,
20
+ save_json,
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class Session:
26
+ """Session metadata.
27
+
28
+ Represents a unique session that groups together an agent's
29
+ state and conversation history.
30
+
31
+ Attributes:
32
+ session_id: Unique identifier for the session
33
+ agent_id: Associated agent identifier
34
+ session_type: Type of session (AGENT, MULTI_AGENT)
35
+ created_at: ISO timestamp when session was created
36
+ updated_at: ISO timestamp when session was last updated
37
+ """
38
+
39
+ session_id: str
40
+ agent_id: str
41
+ session_type: SessionType = SessionType.AGENT
42
+ created_at: str = field(default_factory=get_timestamp)
43
+ updated_at: str = field(default_factory=get_timestamp)
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ """Convert session to dictionary for serialization.
47
+
48
+ Returns:
49
+ Dictionary representation of the session
50
+ """
51
+ return {
52
+ "session_id": self.session_id,
53
+ "agent_id": self.agent_id,
54
+ "session_type": self.session_type.value,
55
+ "created_at": self.created_at,
56
+ "updated_at": self.updated_at,
57
+ }
58
+
59
+ @classmethod
60
+ def from_dict(cls, data: dict[str, Any]) -> "Session":
61
+ """Create Session from dictionary.
62
+
63
+ Args:
64
+ data: Dictionary representation
65
+
66
+ Returns:
67
+ New Session instance
68
+ """
69
+ return cls(
70
+ session_id=data.get("session_id", ""),
71
+ agent_id=data.get("agent_id", ""),
72
+ session_type=SessionType(data.get("session_type", "AGENT")),
73
+ created_at=data.get("created_at", get_timestamp()),
74
+ updated_at=data.get("updated_at", get_timestamp()),
75
+ )
76
+
77
+
78
+ class SessionManager(ABC):
79
+ """Abstract base class for session management.
80
+
81
+ Provides interface for persisting agent sessions, state,
82
+ and conversation history.
83
+ """
84
+
85
+ @abstractmethod
86
+ def create_session(self, agent_id: str) -> Session:
87
+ """Create a new session for an agent.
88
+
89
+ Args:
90
+ agent_id: Unique identifier for the agent
91
+
92
+ Returns:
93
+ New Session instance
94
+ """
95
+ ...
96
+
97
+ @abstractmethod
98
+ def get_session(self, session_id: str) -> Session | None:
99
+ """Retrieve a session by ID.
100
+
101
+ Args:
102
+ session_id: Session identifier
103
+
104
+ Returns:
105
+ Session if found, None otherwise
106
+ """
107
+ ...
108
+
109
+ @abstractmethod
110
+ def list_sessions(self, agent_id: str | None = None) -> list[Session]:
111
+ """List all sessions, optionally filtered by agent.
112
+
113
+ Args:
114
+ agent_id: Optional agent ID to filter by
115
+
116
+ Returns:
117
+ List of sessions
118
+ """
119
+ ...
120
+
121
+ @abstractmethod
122
+ def delete_session(self, session_id: str) -> bool:
123
+ """Delete a session.
124
+
125
+ Args:
126
+ session_id: Session identifier
127
+
128
+ Returns:
129
+ True if deleted, False if not found
130
+ """
131
+ ...
132
+
133
+ @abstractmethod
134
+ def save_agent_state(self, session_id: str, state: AgentState) -> None:
135
+ """Save agent state for a session.
136
+
137
+ Args:
138
+ session_id: Session identifier
139
+ state: AgentState to persist
140
+ """
141
+ ...
142
+
143
+ @abstractmethod
144
+ def load_agent_state(self, session_id: str, agent_id: str) -> AgentState:
145
+ """Load agent state for a session.
146
+
147
+ Args:
148
+ session_id: Session identifier
149
+ agent_id: Agent identifier (used if state doesn't exist)
150
+
151
+ Returns:
152
+ AgentState (new if not found)
153
+ """
154
+ ...
155
+
156
+ @abstractmethod
157
+ def save_conversation(
158
+ self, session_id: str, messages: list[dict[str, Any]]
159
+ ) -> None:
160
+ """Save conversation history for a session.
161
+
162
+ Args:
163
+ session_id: Session identifier
164
+ messages: List of message dictionaries
165
+ """
166
+ ...
167
+
168
+ @abstractmethod
169
+ def load_conversation(self, session_id: str) -> list[dict[str, Any]]:
170
+ """Load conversation history for a session.
171
+
172
+ Args:
173
+ session_id: Session identifier
174
+
175
+ Returns:
176
+ List of message dictionaries
177
+ """
178
+ ...
179
+
180
+
181
+ class FileSessionManager(SessionManager):
182
+ """Filesystem-based session manager.
183
+
184
+ Stores session data as JSON files in the following structure:
185
+
186
+ $HOME/.kader/memory/sessions/
187
+ └── {session_id}/
188
+ ├── session.json # Session metadata
189
+ ├── state.json # Agent state
190
+ └── conversation.json # Conversation history
191
+
192
+ Attributes:
193
+ config: Memory configuration
194
+ """
195
+
196
+ def __init__(self, config: MemoryConfig | None = None) -> None:
197
+ """Initialize the file session manager.
198
+
199
+ Args:
200
+ config: Optional memory configuration
201
+ """
202
+ self.config = config or MemoryConfig()
203
+ self.config.ensure_directories()
204
+
205
+ @property
206
+ def sessions_dir(self) -> Path:
207
+ """Get the sessions directory path."""
208
+ return self.config.memory_dir / "sessions"
209
+
210
+ def _session_dir(self, session_id: str) -> Path:
211
+ """Get the directory path for a specific session."""
212
+ return self.sessions_dir / session_id
213
+
214
+ def _session_file(self, session_id: str) -> Path:
215
+ """Get the session metadata file path."""
216
+ return self._session_dir(session_id) / "session.json"
217
+
218
+ def _state_file(self, session_id: str) -> Path:
219
+ """Get the state file path."""
220
+ return self._session_dir(session_id) / "state.json"
221
+
222
+ def _conversation_file(self, session_id: str) -> Path:
223
+ """Get the conversation file path."""
224
+ return self._session_dir(session_id) / "conversation.json"
225
+
226
+ def create_session(self, agent_id: str) -> Session:
227
+ """Create a new session for an agent.
228
+
229
+ Args:
230
+ agent_id: Unique identifier for the agent
231
+
232
+ Returns:
233
+ New Session instance
234
+ """
235
+ session_id = str(uuid.uuid4())
236
+ session = Session(
237
+ session_id=session_id,
238
+ agent_id=agent_id,
239
+ )
240
+
241
+ # Create session directory and save metadata
242
+ session_dir = self._session_dir(session_id)
243
+ session_dir.mkdir(parents=True, exist_ok=True)
244
+ save_json(self._session_file(session_id), session.to_dict())
245
+
246
+ return session
247
+
248
+ def get_session(self, session_id: str) -> Session | None:
249
+ """Retrieve a session by ID.
250
+
251
+ Args:
252
+ session_id: Session identifier
253
+
254
+ Returns:
255
+ Session if found, None otherwise
256
+ """
257
+ session_file = self._session_file(session_id)
258
+ if not session_file.exists():
259
+ return None
260
+
261
+ data = load_json(session_file)
262
+ return Session.from_dict(data) if data else None
263
+
264
+ def list_sessions(self, agent_id: str | None = None) -> list[Session]:
265
+ """List all sessions, optionally filtered by agent.
266
+
267
+ Args:
268
+ agent_id: Optional agent ID to filter by
269
+
270
+ Returns:
271
+ List of sessions
272
+ """
273
+ sessions: list[Session] = []
274
+
275
+ if not self.sessions_dir.exists():
276
+ return sessions
277
+
278
+ for session_dir in self.sessions_dir.iterdir():
279
+ if not session_dir.is_dir():
280
+ continue
281
+
282
+ session_file = session_dir / "session.json"
283
+ if not session_file.exists():
284
+ continue
285
+
286
+ data = load_json(session_file)
287
+ if not data:
288
+ continue
289
+
290
+ session = Session.from_dict(data)
291
+
292
+ if agent_id is None or session.agent_id == agent_id:
293
+ sessions.append(session)
294
+
295
+ # Sort by created_at descending (newest first)
296
+ sessions.sort(key=lambda s: s.created_at, reverse=True)
297
+ return sessions
298
+
299
+ def delete_session(self, session_id: str) -> bool:
300
+ """Delete a session and all its data.
301
+
302
+ Args:
303
+ session_id: Session identifier
304
+
305
+ Returns:
306
+ True if deleted, False if not found
307
+ """
308
+ import shutil
309
+
310
+ session_dir = self._session_dir(session_id)
311
+ if not session_dir.exists():
312
+ return False
313
+
314
+ shutil.rmtree(session_dir)
315
+ return True
316
+
317
+ def save_agent_state(self, session_id: str, state: AgentState) -> None:
318
+ """Save agent state for a session.
319
+
320
+ Args:
321
+ session_id: Session identifier
322
+ state: AgentState to persist
323
+ """
324
+ state_file = self._state_file(session_id)
325
+ save_json(state_file, state.to_dict())
326
+
327
+ # Update session timestamp
328
+ self._update_session_timestamp(session_id)
329
+
330
+ def load_agent_state(self, session_id: str, agent_id: str) -> AgentState:
331
+ """Load agent state for a session.
332
+
333
+ Args:
334
+ session_id: Session identifier
335
+ agent_id: Agent identifier (used if state doesn't exist)
336
+
337
+ Returns:
338
+ AgentState (new if not found)
339
+ """
340
+ state_file = self._state_file(session_id)
341
+ data = load_json(state_file)
342
+
343
+ if data:
344
+ return AgentState.from_dict(data)
345
+
346
+ return AgentState(agent_id=agent_id)
347
+
348
+ def save_conversation(
349
+ self, session_id: str, messages: list[dict[str, Any]]
350
+ ) -> None:
351
+ """Save conversation history for a session.
352
+
353
+ Args:
354
+ session_id: Session identifier
355
+ messages: List of message dictionaries
356
+ """
357
+ conversation_file = self._conversation_file(session_id)
358
+ save_json(conversation_file, {"messages": messages})
359
+
360
+ # Update session timestamp
361
+ self._update_session_timestamp(session_id)
362
+
363
+ def load_conversation(self, session_id: str) -> list[dict[str, Any]]:
364
+ """Load conversation history for a session.
365
+
366
+ Args:
367
+ session_id: Session identifier
368
+
369
+ Returns:
370
+ List of message dictionaries
371
+ """
372
+ conversation_file = self._conversation_file(session_id)
373
+ data = load_json(conversation_file)
374
+ return data.get("messages", [])
375
+
376
+ def _update_session_timestamp(self, session_id: str) -> None:
377
+ """Update the session's updated_at timestamp.
378
+
379
+ Args:
380
+ session_id: Session identifier
381
+ """
382
+ session = self.get_session(session_id)
383
+ if session:
384
+ session.updated_at = get_timestamp()
385
+ save_json(self._session_file(session_id), session.to_dict())
kader/memory/state.py ADDED
@@ -0,0 +1,211 @@
1
+ """
2
+ State management for agents.
3
+
4
+ Provides key-value state storage for persisting agent state
5
+ that exists outside of conversation context.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from .types import get_timestamp
12
+
13
+
14
+ @dataclass
15
+ class AgentState:
16
+ """Key-value store for persistent agent state.
17
+
18
+ AgentState is used for storing stateful information that exists
19
+ outside the direct conversation context. Unlike conversation history,
20
+ agent state is not directly passed to the language model during inference
21
+ but can be accessed and modified by the agent's tools and application logic.
22
+
23
+ Attributes:
24
+ agent_id: Unique identifier for the agent
25
+ _state: Internal state dictionary
26
+ created_at: ISO timestamp when state was created
27
+ updated_at: ISO timestamp when state was last updated
28
+ """
29
+
30
+ agent_id: str
31
+ _state: dict[str, Any] = field(default_factory=dict)
32
+ created_at: str = field(default_factory=get_timestamp)
33
+ updated_at: str = field(default_factory=get_timestamp)
34
+
35
+ def get(self, key: str, default: Any = None) -> Any:
36
+ """Get a value from state.
37
+
38
+ Args:
39
+ key: The key to retrieve
40
+ default: Default value if key not found
41
+
42
+ Returns:
43
+ The value or default
44
+ """
45
+ return self._state.get(key, default)
46
+
47
+ def set(self, key: str, value: Any) -> None:
48
+ """Set a value in state.
49
+
50
+ Args:
51
+ key: The key to set
52
+ value: The value to store
53
+ """
54
+ self._state[key] = value
55
+ self.updated_at = get_timestamp()
56
+
57
+ def delete(self, key: str) -> bool:
58
+ """Delete a key from state.
59
+
60
+ Args:
61
+ key: The key to delete
62
+
63
+ Returns:
64
+ True if key existed and was deleted, False otherwise
65
+ """
66
+ if key in self._state:
67
+ del self._state[key]
68
+ self.updated_at = get_timestamp()
69
+ return True
70
+ return False
71
+
72
+ def get_all(self) -> dict[str, Any]:
73
+ """Get all state as a dictionary.
74
+
75
+ Returns:
76
+ Copy of the state dictionary
77
+ """
78
+ return dict(self._state)
79
+
80
+ def clear(self) -> None:
81
+ """Clear all state."""
82
+ self._state.clear()
83
+ self.updated_at = get_timestamp()
84
+
85
+ def update(self, data: dict[str, Any]) -> None:
86
+ """Update state with multiple key-value pairs.
87
+
88
+ Args:
89
+ data: Dictionary of key-value pairs to update
90
+ """
91
+ self._state.update(data)
92
+ self.updated_at = get_timestamp()
93
+
94
+ def __contains__(self, key: str) -> bool:
95
+ """Check if key exists in state."""
96
+ return key in self._state
97
+
98
+ def __len__(self) -> int:
99
+ """Return number of keys in state."""
100
+ return len(self._state)
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ """Convert state to dictionary for serialization.
104
+
105
+ Returns:
106
+ Dictionary representation of the state
107
+ """
108
+ return {
109
+ "agent_id": self.agent_id,
110
+ "state": self._state,
111
+ "created_at": self.created_at,
112
+ "updated_at": self.updated_at,
113
+ }
114
+
115
+ @classmethod
116
+ def from_dict(cls, data: dict[str, Any]) -> "AgentState":
117
+ """Create AgentState from dictionary.
118
+
119
+ Args:
120
+ data: Dictionary representation
121
+
122
+ Returns:
123
+ New AgentState instance
124
+ """
125
+ state = cls(
126
+ agent_id=data.get("agent_id", ""),
127
+ created_at=data.get("created_at", get_timestamp()),
128
+ updated_at=data.get("updated_at", get_timestamp()),
129
+ )
130
+ state._state = data.get("state", {})
131
+ return state
132
+
133
+
134
+ @dataclass
135
+ class RequestState:
136
+ """Request-scoped ephemeral state.
137
+
138
+ RequestState is used for storing context that is maintained
139
+ specifically within the scope of a single request. This state
140
+ is NOT persisted to disk.
141
+
142
+ Attributes:
143
+ request_id: Unique identifier for the request
144
+ _state: Internal state dictionary
145
+ """
146
+
147
+ request_id: str
148
+ _state: dict[str, Any] = field(default_factory=dict)
149
+
150
+ def get(self, key: str, default: Any = None) -> Any:
151
+ """Get a value from state.
152
+
153
+ Args:
154
+ key: The key to retrieve
155
+ default: Default value if key not found
156
+
157
+ Returns:
158
+ The value or default
159
+ """
160
+ return self._state.get(key, default)
161
+
162
+ def set(self, key: str, value: Any) -> None:
163
+ """Set a value in state.
164
+
165
+ Args:
166
+ key: The key to set
167
+ value: The value to store
168
+ """
169
+ self._state[key] = value
170
+
171
+ def delete(self, key: str) -> bool:
172
+ """Delete a key from state.
173
+
174
+ Args:
175
+ key: The key to delete
176
+
177
+ Returns:
178
+ True if key existed and was deleted, False otherwise
179
+ """
180
+ if key in self._state:
181
+ del self._state[key]
182
+ return True
183
+ return False
184
+
185
+ def get_all(self) -> dict[str, Any]:
186
+ """Get all state as a dictionary.
187
+
188
+ Returns:
189
+ Copy of the state dictionary
190
+ """
191
+ return dict(self._state)
192
+
193
+ def clear(self) -> None:
194
+ """Clear all state."""
195
+ self._state.clear()
196
+
197
+ def update(self, data: dict[str, Any]) -> None:
198
+ """Update state with multiple key-value pairs.
199
+
200
+ Args:
201
+ data: Dictionary of key-value pairs to update
202
+ """
203
+ self._state.update(data)
204
+
205
+ def __contains__(self, key: str) -> bool:
206
+ """Check if key exists in state."""
207
+ return key in self._state
208
+
209
+ def __len__(self) -> int:
210
+ """Return number of keys in state."""
211
+ return len(self._state)
kader/memory/types.py ADDED
@@ -0,0 +1,116 @@
1
+ """
2
+ Core types and dataclasses for the Memory module.
3
+
4
+ Provides common types used across state, session, and conversation management.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ class SessionType(str, Enum):
16
+ """Enumeration of session types."""
17
+
18
+ AGENT = "AGENT"
19
+ MULTI_AGENT = "MULTI_AGENT"
20
+
21
+
22
+ def get_default_memory_dir() -> Path:
23
+ """Get the default memory directory path ($HOME/.kader/memory)."""
24
+ home = Path.home()
25
+ return home / ".kader" / "memory"
26
+
27
+
28
+ @dataclass
29
+ class MemoryConfig:
30
+ """Configuration for memory management.
31
+
32
+ Attributes:
33
+ memory_dir: Root directory for memory storage
34
+ auto_save: Whether to automatically save state changes
35
+ max_conversation_length: Maximum messages to keep in conversation history
36
+ """
37
+
38
+ memory_dir: Path = field(default_factory=get_default_memory_dir)
39
+ auto_save: bool = True
40
+ max_conversation_length: int = 100
41
+
42
+ def __post_init__(self) -> None:
43
+ """Ensure memory_dir is a Path object."""
44
+ if isinstance(self.memory_dir, str):
45
+ self.memory_dir = Path(self.memory_dir)
46
+
47
+ def ensure_directories(self) -> None:
48
+ """Create memory directories if they don't exist."""
49
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
50
+ (self.memory_dir / "sessions").mkdir(exist_ok=True)
51
+
52
+
53
+ def encode_bytes_values(obj: Any) -> Any:
54
+ """Recursively encode any bytes values in an object to base64.
55
+
56
+ Handles dictionaries, lists, and nested structures.
57
+ """
58
+ import base64
59
+
60
+ if isinstance(obj, bytes):
61
+ return {"__bytes_encoded__": True, "data": base64.b64encode(obj).decode()}
62
+ elif isinstance(obj, dict):
63
+ return {k: encode_bytes_values(v) for k, v in obj.items()}
64
+ elif isinstance(obj, list):
65
+ return [encode_bytes_values(item) for item in obj]
66
+ else:
67
+ return obj
68
+
69
+
70
+ def decode_bytes_values(obj: Any) -> Any:
71
+ """Recursively decode any base64-encoded bytes values in an object.
72
+
73
+ Handles dictionaries, lists, and nested structures.
74
+ """
75
+ import base64
76
+
77
+ if isinstance(obj, dict):
78
+ if obj.get("__bytes_encoded__") is True and "data" in obj:
79
+ return base64.b64decode(obj["data"])
80
+ return {k: decode_bytes_values(v) for k, v in obj.items()}
81
+ elif isinstance(obj, list):
82
+ return [decode_bytes_values(item) for item in obj]
83
+ else:
84
+ return obj
85
+
86
+
87
+ def get_timestamp() -> str:
88
+ """Get current UTC timestamp in ISO format."""
89
+ return datetime.now(timezone.utc).isoformat()
90
+
91
+
92
+ def save_json(path: Path, data: dict[str, Any]) -> None:
93
+ """Save data to a JSON file.
94
+
95
+ Args:
96
+ path: Path to the JSON file
97
+ data: Data to save
98
+ """
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ with open(path, "w", encoding="utf-8") as f:
101
+ json.dump(encode_bytes_values(data), f, indent=2, ensure_ascii=False)
102
+
103
+
104
+ def load_json(path: Path) -> dict[str, Any]:
105
+ """Load data from a JSON file.
106
+
107
+ Args:
108
+ path: Path to the JSON file
109
+
110
+ Returns:
111
+ Loaded data, or empty dict if file doesn't exist
112
+ """
113
+ if not path.exists():
114
+ return {}
115
+ with open(path, "r", encoding="utf-8") as f:
116
+ return decode_bytes_values(json.load(f))