kader 0.1.0__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 +547 -0
- cli/app.tcss +648 -0
- cli/utils.py +62 -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 +920 -0
- kader/agent/logger.py +188 -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.0.dist-info/METADATA +319 -0
- kader-0.1.0.dist-info/RECORD +45 -0
- kader-0.1.0.dist-info/WHEEL +4 -0
- kader-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation management for agents.
|
|
3
|
+
|
|
4
|
+
Provides conversation history management and context windowing
|
|
5
|
+
to handle token limits and maintain coherent conversations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .types import get_timestamp
|
|
13
|
+
|
|
14
|
+
# Import Message from providers for compatibility
|
|
15
|
+
# This allows the conversation module to work with the existing Message type
|
|
16
|
+
try:
|
|
17
|
+
from kader.providers.base import Message
|
|
18
|
+
except ImportError:
|
|
19
|
+
# Fallback for standalone usage
|
|
20
|
+
Message = None # type: ignore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ConversationMessage:
|
|
25
|
+
"""Wrapper around a Message with metadata.
|
|
26
|
+
|
|
27
|
+
Provides additional metadata for conversation management
|
|
28
|
+
while maintaining compatibility with the provider's Message type.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
message: Dictionary representation of the underlying message
|
|
32
|
+
message_id: Index/position in the conversation
|
|
33
|
+
created_at: ISO timestamp when message was added
|
|
34
|
+
updated_at: ISO timestamp when message was last updated
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
message: dict[str, Any]
|
|
38
|
+
message_id: int
|
|
39
|
+
created_at: str = field(default_factory=get_timestamp)
|
|
40
|
+
updated_at: str = field(default_factory=get_timestamp)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def role(self) -> str:
|
|
44
|
+
"""Get the message role."""
|
|
45
|
+
return self.message.get("role", "")
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def content(self) -> str:
|
|
49
|
+
"""Get the message content."""
|
|
50
|
+
return self.message.get("content", "")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def tool_calls(self) -> list[dict[str, Any]] | None:
|
|
54
|
+
"""Get tool calls if present."""
|
|
55
|
+
return self.message.get("tool_calls")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def tool_call_id(self) -> str | None:
|
|
59
|
+
"""Get tool call ID if this is a tool response."""
|
|
60
|
+
return self.message.get("tool_call_id")
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
"""Convert to dictionary for serialization.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary representation
|
|
67
|
+
"""
|
|
68
|
+
return {
|
|
69
|
+
"message": self.message,
|
|
70
|
+
"message_id": self.message_id,
|
|
71
|
+
"created_at": self.created_at,
|
|
72
|
+
"updated_at": self.updated_at,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict[str, Any]) -> "ConversationMessage":
|
|
77
|
+
"""Create from dictionary.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data: Dictionary representation
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
New ConversationMessage instance
|
|
84
|
+
"""
|
|
85
|
+
return cls(
|
|
86
|
+
message=data.get("message", {}),
|
|
87
|
+
message_id=data.get("message_id", 0),
|
|
88
|
+
created_at=data.get("created_at", get_timestamp()),
|
|
89
|
+
updated_at=data.get("updated_at", get_timestamp()),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_message(cls, message: Any, index: int) -> "ConversationMessage":
|
|
94
|
+
"""Create from a provider Message object.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
message: Message object (from kader.providers.base)
|
|
98
|
+
index: Position in conversation
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
New ConversationMessage instance
|
|
102
|
+
"""
|
|
103
|
+
# Handle both Message objects and dicts
|
|
104
|
+
if hasattr(message, "to_dict"):
|
|
105
|
+
msg_dict = message.to_dict()
|
|
106
|
+
elif isinstance(message, dict):
|
|
107
|
+
msg_dict = message
|
|
108
|
+
else:
|
|
109
|
+
msg_dict = {"role": "user", "content": str(message)}
|
|
110
|
+
|
|
111
|
+
return cls(
|
|
112
|
+
message=msg_dict,
|
|
113
|
+
message_id=index,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def to_message(self) -> Any:
|
|
117
|
+
"""Convert back to a provider Message object.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Message object if kader.providers.base is available, else dict
|
|
121
|
+
"""
|
|
122
|
+
if Message is not None:
|
|
123
|
+
return Message(
|
|
124
|
+
role=self.message.get("role", "user"),
|
|
125
|
+
content=self.message.get("content", ""),
|
|
126
|
+
name=self.message.get("name"),
|
|
127
|
+
tool_call_id=self.message.get("tool_call_id"),
|
|
128
|
+
tool_calls=self.message.get("tool_calls"),
|
|
129
|
+
)
|
|
130
|
+
return self.message
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ConversationManager(ABC):
|
|
134
|
+
"""Abstract base class for conversation management.
|
|
135
|
+
|
|
136
|
+
Provides interface for managing conversation history and
|
|
137
|
+
applying context management strategies.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def add_message(self, message: Any) -> ConversationMessage:
|
|
142
|
+
"""Add a message to the conversation.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
message: Message to add (Message object or dict)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The wrapped ConversationMessage
|
|
149
|
+
"""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def add_messages(self, messages: list[Any]) -> list[ConversationMessage]:
|
|
154
|
+
"""Add multiple messages to the conversation.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
messages: List of messages to add
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of wrapped ConversationMessages
|
|
161
|
+
"""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def get_messages(self) -> list[ConversationMessage]:
|
|
166
|
+
"""Get all conversation messages.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of ConversationMessages
|
|
170
|
+
"""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def get_state(self) -> dict[str, Any]:
|
|
175
|
+
"""Get manager state for persistence.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
State dictionary
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def set_state(self, state: dict[str, Any]) -> None:
|
|
184
|
+
"""Restore manager state.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
state: State dictionary
|
|
188
|
+
"""
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def apply_window(self) -> list[dict[str, Any]]:
|
|
193
|
+
"""Apply context management strategy and return messages.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of message dictionaries ready for LLM consumption
|
|
197
|
+
"""
|
|
198
|
+
...
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
def clear(self) -> None:
|
|
202
|
+
"""Clear all messages from the conversation."""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class SlidingWindowConversationManager(ConversationManager):
|
|
207
|
+
"""Sliding window conversation manager.
|
|
208
|
+
|
|
209
|
+
Maintains a fixed number of recent message pairs to prevent
|
|
210
|
+
exceeding model context limits. Preserves tool call/result pairs.
|
|
211
|
+
|
|
212
|
+
Attributes:
|
|
213
|
+
window_size: Maximum number of message pairs to keep
|
|
214
|
+
messages: List of conversation messages
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, window_size: int = 20) -> None:
|
|
218
|
+
"""Initialize the sliding window manager.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
window_size: Maximum message pairs to keep (default: 20)
|
|
222
|
+
"""
|
|
223
|
+
self.window_size = window_size
|
|
224
|
+
self._messages: list[ConversationMessage] = []
|
|
225
|
+
self._next_id = 0
|
|
226
|
+
|
|
227
|
+
def add_message(self, message: Any) -> ConversationMessage:
|
|
228
|
+
"""Add a message to the conversation.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
message: Message to add (Message object or dict)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The wrapped ConversationMessage
|
|
235
|
+
"""
|
|
236
|
+
conv_msg = ConversationMessage.from_message(message, self._next_id)
|
|
237
|
+
self._messages.append(conv_msg)
|
|
238
|
+
self._next_id += 1
|
|
239
|
+
return conv_msg
|
|
240
|
+
|
|
241
|
+
def add_messages(self, messages: list[Any]) -> list[ConversationMessage]:
|
|
242
|
+
"""Add multiple messages to the conversation.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
messages: List of messages to add
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of wrapped ConversationMessages
|
|
249
|
+
"""
|
|
250
|
+
return [self.add_message(msg) for msg in messages]
|
|
251
|
+
|
|
252
|
+
def get_messages(self) -> list[ConversationMessage]:
|
|
253
|
+
"""Get all conversation messages.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of ConversationMessages
|
|
257
|
+
"""
|
|
258
|
+
return list(self._messages)
|
|
259
|
+
|
|
260
|
+
def get_state(self) -> dict[str, Any]:
|
|
261
|
+
"""Get manager state for persistence.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
State dictionary
|
|
265
|
+
"""
|
|
266
|
+
return {
|
|
267
|
+
"window_size": self.window_size,
|
|
268
|
+
"next_id": self._next_id,
|
|
269
|
+
"messages": [msg.to_dict() for msg in self._messages],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def set_state(self, state: dict[str, Any]) -> None:
|
|
273
|
+
"""Restore manager state.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
state: State dictionary
|
|
277
|
+
"""
|
|
278
|
+
self.window_size = state.get("window_size", self.window_size)
|
|
279
|
+
self._next_id = state.get("next_id", 0)
|
|
280
|
+
self._messages = [
|
|
281
|
+
ConversationMessage.from_dict(msg_data)
|
|
282
|
+
for msg_data in state.get("messages", [])
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
def apply_window(self) -> list[dict[str, Any]]:
|
|
286
|
+
"""Apply sliding window and return messages for LLM.
|
|
287
|
+
|
|
288
|
+
Keeps the most recent messages within the window size,
|
|
289
|
+
while preserving complete tool call/result pairs.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List of message dictionaries
|
|
293
|
+
"""
|
|
294
|
+
if not self._messages:
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
# Calculate how many messages to keep (pairs = user + assistant)
|
|
298
|
+
max_messages = self.window_size * 2
|
|
299
|
+
|
|
300
|
+
if len(self._messages) <= max_messages:
|
|
301
|
+
return [msg.message for msg in self._messages]
|
|
302
|
+
|
|
303
|
+
# Start from the end and work backwards
|
|
304
|
+
# Ensure we don't break tool call/result pairs
|
|
305
|
+
messages_to_keep = self._messages[-max_messages:]
|
|
306
|
+
|
|
307
|
+
# Check if first message is a tool result without its call
|
|
308
|
+
# If so, find and include the tool call
|
|
309
|
+
result = []
|
|
310
|
+
tool_call_ids_needed: set[str] = set()
|
|
311
|
+
|
|
312
|
+
for msg in reversed(messages_to_keep):
|
|
313
|
+
if msg.role == "tool" and msg.tool_call_id:
|
|
314
|
+
tool_call_ids_needed.add(msg.tool_call_id)
|
|
315
|
+
|
|
316
|
+
# Include any assistant messages with tool calls that are needed
|
|
317
|
+
for msg in reversed(self._messages):
|
|
318
|
+
if msg in messages_to_keep:
|
|
319
|
+
result.insert(0, msg.message)
|
|
320
|
+
elif msg.role == "assistant" and msg.tool_calls:
|
|
321
|
+
# Check if any of the tool calls are needed
|
|
322
|
+
for tc in msg.tool_calls:
|
|
323
|
+
tc_id = tc.get("id", "")
|
|
324
|
+
if tc_id in tool_call_ids_needed:
|
|
325
|
+
result.insert(0, msg.message)
|
|
326
|
+
tool_call_ids_needed.discard(tc_id)
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
if not tool_call_ids_needed:
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
def clear(self) -> None:
|
|
335
|
+
"""Clear all messages from the conversation."""
|
|
336
|
+
self._messages.clear()
|
|
337
|
+
self._next_id = 0
|
|
338
|
+
|
|
339
|
+
def __len__(self) -> int:
|
|
340
|
+
"""Return number of messages."""
|
|
341
|
+
return len(self._messages)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class NullConversationManager(ConversationManager):
|
|
345
|
+
"""No-op conversation manager.
|
|
346
|
+
|
|
347
|
+
For short interactions or when managing context manually.
|
|
348
|
+
Does not store any messages.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def add_message(self, message: Any) -> ConversationMessage:
|
|
352
|
+
"""Add a message (no-op, returns wrapper).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
message: Message to add
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
The wrapped ConversationMessage (not stored)
|
|
359
|
+
"""
|
|
360
|
+
return ConversationMessage.from_message(message, 0)
|
|
361
|
+
|
|
362
|
+
def add_messages(self, messages: list[Any]) -> list[ConversationMessage]:
|
|
363
|
+
"""Add multiple messages (no-op).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
messages: List of messages to add
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
List of wrapped ConversationMessages (not stored)
|
|
370
|
+
"""
|
|
371
|
+
return [
|
|
372
|
+
ConversationMessage.from_message(msg, i) for i, msg in enumerate(messages)
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
def get_messages(self) -> list[ConversationMessage]:
|
|
376
|
+
"""Get all messages (always empty).
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Empty list
|
|
380
|
+
"""
|
|
381
|
+
return []
|
|
382
|
+
|
|
383
|
+
def get_state(self) -> dict[str, Any]:
|
|
384
|
+
"""Get manager state (empty).
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Empty dict
|
|
388
|
+
"""
|
|
389
|
+
return {}
|
|
390
|
+
|
|
391
|
+
def set_state(self, state: dict[str, Any]) -> None:
|
|
392
|
+
"""Restore manager state (no-op).
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
state: State dictionary (ignored)
|
|
396
|
+
"""
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
def apply_window(self) -> list[dict[str, Any]]:
|
|
400
|
+
"""Apply context management (returns empty).
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Empty list
|
|
404
|
+
"""
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
def clear(self) -> None:
|
|
408
|
+
"""Clear messages (no-op)."""
|
|
409
|
+
pass
|