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.
- cli/README.md +169 -0
- cli/__init__.py +5 -0
- cli/__main__.py +6 -0
- cli/app.py +707 -0
- cli/app.tcss +664 -0
- cli/utils.py +68 -0
- cli/widgets/__init__.py +13 -0
- cli/widgets/confirmation.py +309 -0
- cli/widgets/conversation.py +55 -0
- cli/widgets/loading.py +59 -0
- kader/__init__.py +22 -0
- kader/agent/__init__.py +8 -0
- kader/agent/agents.py +126 -0
- kader/agent/base.py +927 -0
- kader/agent/logger.py +170 -0
- kader/config.py +139 -0
- kader/memory/__init__.py +66 -0
- kader/memory/conversation.py +409 -0
- kader/memory/session.py +385 -0
- kader/memory/state.py +211 -0
- kader/memory/types.py +116 -0
- kader/prompts/__init__.py +9 -0
- kader/prompts/agent_prompts.py +27 -0
- kader/prompts/base.py +81 -0
- kader/prompts/templates/planning_agent.j2 +26 -0
- kader/prompts/templates/react_agent.j2 +18 -0
- kader/providers/__init__.py +9 -0
- kader/providers/base.py +581 -0
- kader/providers/mock.py +96 -0
- kader/providers/ollama.py +447 -0
- kader/tools/README.md +483 -0
- kader/tools/__init__.py +130 -0
- kader/tools/base.py +955 -0
- kader/tools/exec_commands.py +249 -0
- kader/tools/filesys.py +650 -0
- kader/tools/filesystem.py +607 -0
- kader/tools/protocol.py +456 -0
- kader/tools/rag.py +555 -0
- kader/tools/todo.py +210 -0
- kader/tools/utils.py +456 -0
- kader/tools/web.py +246 -0
- kader-0.1.5.dist-info/METADATA +321 -0
- kader-0.1.5.dist-info/RECORD +45 -0
- kader-0.1.5.dist-info/WHEEL +4 -0
- kader-0.1.5.dist-info/entry_points.txt +2 -0
kader/memory/session.py
ADDED
|
@@ -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))
|