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,309 @@
|
|
|
1
|
+
"""Inline selection widget for tool confirmation and model selection."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.binding import Binding
|
|
5
|
+
from textual.containers import Vertical
|
|
6
|
+
from textual.message import Message as TextualMessage
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InlineSelector(Widget, can_focus=True):
|
|
13
|
+
"""
|
|
14
|
+
Inline selector widget for Yes/No confirmation.
|
|
15
|
+
|
|
16
|
+
Uses arrow keys to navigate, Enter to confirm.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
BINDINGS = [
|
|
20
|
+
Binding("up", "move_up", "Up", show=False),
|
|
21
|
+
Binding("down", "move_down", "Down", show=False),
|
|
22
|
+
Binding("left", "move_up", "Left", show=False),
|
|
23
|
+
Binding("right", "move_down", "Right", show=False),
|
|
24
|
+
Binding("enter", "confirm", "Confirm", show=False),
|
|
25
|
+
Binding("y", "confirm_yes", "Yes", show=False),
|
|
26
|
+
Binding("n", "confirm_no", "No", show=False),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
InlineSelector {
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: auto;
|
|
33
|
+
padding: 1;
|
|
34
|
+
border: solid $primary;
|
|
35
|
+
background: $surface;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
InlineSelector:focus {
|
|
39
|
+
border: double $primary;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
InlineSelector .selector-container {
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: auto;
|
|
45
|
+
align: center middle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
InlineSelector .option {
|
|
49
|
+
padding: 0 3;
|
|
50
|
+
margin: 0 2;
|
|
51
|
+
min-width: 12;
|
|
52
|
+
text-align: center;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
InlineSelector .option.selected {
|
|
56
|
+
background: $primary;
|
|
57
|
+
color: $text;
|
|
58
|
+
text-style: bold reverse;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
InlineSelector .option.not-selected {
|
|
62
|
+
background: $surface-darken-1;
|
|
63
|
+
color: $text-muted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
InlineSelector .prompt-text {
|
|
67
|
+
margin-bottom: 1;
|
|
68
|
+
text-align: center;
|
|
69
|
+
width: 100%;
|
|
70
|
+
color: $text-muted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
InlineSelector .message-text {
|
|
74
|
+
margin-bottom: 1;
|
|
75
|
+
text-align: center;
|
|
76
|
+
width: 100%;
|
|
77
|
+
color: $warning;
|
|
78
|
+
text-style: bold;
|
|
79
|
+
}
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
selected_index: reactive[int] = reactive(0)
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, options: list[str] = None, **kwargs) -> None:
|
|
85
|
+
super().__init__(**kwargs)
|
|
86
|
+
self.message = message
|
|
87
|
+
self.options = options or ["✅ Yes", "❌ No"]
|
|
88
|
+
|
|
89
|
+
def compose(self) -> ComposeResult:
|
|
90
|
+
from textual.containers import Horizontal
|
|
91
|
+
|
|
92
|
+
yield Static(f"🔧 {self.message}", classes="message-text")
|
|
93
|
+
yield Static(
|
|
94
|
+
"↑↓ to select • Enter to confirm • Y/N for quick select",
|
|
95
|
+
classes="prompt-text",
|
|
96
|
+
)
|
|
97
|
+
with Horizontal(classes="selector-container"):
|
|
98
|
+
for i, option in enumerate(self.options):
|
|
99
|
+
cls = (
|
|
100
|
+
"option selected"
|
|
101
|
+
if i == self.selected_index
|
|
102
|
+
else "option not-selected"
|
|
103
|
+
)
|
|
104
|
+
yield Static(option, classes=cls, id=f"option-{i}")
|
|
105
|
+
|
|
106
|
+
def on_mount(self) -> None:
|
|
107
|
+
"""Focus self when mounted."""
|
|
108
|
+
self.focus()
|
|
109
|
+
|
|
110
|
+
def watch_selected_index(self, old_index: int, new_index: int) -> None:
|
|
111
|
+
"""Update visual selection when index changes."""
|
|
112
|
+
try:
|
|
113
|
+
old_option = self.query_one(f"#option-{old_index}", Static)
|
|
114
|
+
old_option.remove_class("selected")
|
|
115
|
+
old_option.add_class("not-selected")
|
|
116
|
+
|
|
117
|
+
new_option = self.query_one(f"#option-{new_index}", Static)
|
|
118
|
+
new_option.remove_class("not-selected")
|
|
119
|
+
new_option.add_class("selected")
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def action_move_up(self) -> None:
|
|
124
|
+
"""Move selection up/left."""
|
|
125
|
+
self.selected_index = (self.selected_index - 1) % len(self.options)
|
|
126
|
+
|
|
127
|
+
def action_move_down(self) -> None:
|
|
128
|
+
"""Move selection down/right."""
|
|
129
|
+
self.selected_index = (self.selected_index + 1) % len(self.options)
|
|
130
|
+
|
|
131
|
+
def action_confirm(self) -> None:
|
|
132
|
+
"""Confirm current selection."""
|
|
133
|
+
self.post_message(self.Confirmed(self.selected_index == 0))
|
|
134
|
+
|
|
135
|
+
def action_confirm_yes(self) -> None:
|
|
136
|
+
"""Quick confirm Yes."""
|
|
137
|
+
self.selected_index = 0
|
|
138
|
+
self.post_message(self.Confirmed(True))
|
|
139
|
+
|
|
140
|
+
def action_confirm_no(self) -> None:
|
|
141
|
+
"""Quick confirm No."""
|
|
142
|
+
self.selected_index = 1
|
|
143
|
+
self.post_message(self.Confirmed(False))
|
|
144
|
+
|
|
145
|
+
class Confirmed(TextualMessage):
|
|
146
|
+
"""Message sent when user confirms selection."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, confirmed: bool) -> None:
|
|
149
|
+
super().__init__()
|
|
150
|
+
self.confirmed = confirmed
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ModelSelector(Widget, can_focus=True):
|
|
154
|
+
"""
|
|
155
|
+
Model selector widget for choosing LLM models.
|
|
156
|
+
|
|
157
|
+
Uses arrow keys to navigate, Enter to confirm, Escape to cancel.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
BINDINGS = [
|
|
161
|
+
Binding("up", "move_up", "Up", show=False),
|
|
162
|
+
Binding("down", "move_down", "Down", show=False),
|
|
163
|
+
Binding("enter", "confirm", "Confirm", show=False),
|
|
164
|
+
Binding("escape", "cancel", "Cancel", show=False),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
DEFAULT_CSS = """
|
|
168
|
+
ModelSelector {
|
|
169
|
+
width: 100%;
|
|
170
|
+
height: auto;
|
|
171
|
+
padding: 1;
|
|
172
|
+
border: solid $primary;
|
|
173
|
+
background: $surface;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
ModelSelector:focus {
|
|
177
|
+
border: double $primary;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ModelSelector .title-text {
|
|
181
|
+
margin-bottom: 1;
|
|
182
|
+
text-align: center;
|
|
183
|
+
width: 100%;
|
|
184
|
+
color: $warning;
|
|
185
|
+
text-style: bold;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
ModelSelector .prompt-text {
|
|
189
|
+
margin-bottom: 1;
|
|
190
|
+
text-align: center;
|
|
191
|
+
width: 100%;
|
|
192
|
+
color: $text-muted;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ModelSelector .model-list {
|
|
196
|
+
width: 100%;
|
|
197
|
+
height: auto;
|
|
198
|
+
max-height: 15;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ModelSelector .model-option {
|
|
202
|
+
padding: 0 2;
|
|
203
|
+
width: 100%;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ModelSelector .model-option.selected {
|
|
207
|
+
background: $primary;
|
|
208
|
+
color: $text;
|
|
209
|
+
text-style: bold;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ModelSelector .model-option.not-selected {
|
|
213
|
+
background: $surface;
|
|
214
|
+
color: $text-muted;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ModelSelector .model-option.current {
|
|
218
|
+
color: $success;
|
|
219
|
+
}
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
selected_index: reactive[int] = reactive(0)
|
|
223
|
+
|
|
224
|
+
def __init__(self, models: list[str], current_model: str = "", **kwargs) -> None:
|
|
225
|
+
super().__init__(**kwargs)
|
|
226
|
+
self.models = models
|
|
227
|
+
self.current_model = current_model
|
|
228
|
+
# Start with current model selected if it exists
|
|
229
|
+
if current_model in models:
|
|
230
|
+
self.selected_index = models.index(current_model)
|
|
231
|
+
|
|
232
|
+
def compose(self) -> ComposeResult:
|
|
233
|
+
yield Static("🤖 Select Model", classes="title-text")
|
|
234
|
+
yield Static(
|
|
235
|
+
"↑↓ to navigate • Enter to select • Esc to cancel", classes="prompt-text"
|
|
236
|
+
)
|
|
237
|
+
with Vertical(classes="model-list"):
|
|
238
|
+
for i, model in enumerate(self.models):
|
|
239
|
+
is_current = model == self.current_model
|
|
240
|
+
is_selected = i == self.selected_index
|
|
241
|
+
classes = "model-option"
|
|
242
|
+
if is_selected:
|
|
243
|
+
classes += " selected"
|
|
244
|
+
else:
|
|
245
|
+
classes += " not-selected"
|
|
246
|
+
if is_current:
|
|
247
|
+
classes += " current"
|
|
248
|
+
label = f" ▶ {model}" if is_selected else f" {model}"
|
|
249
|
+
if is_current:
|
|
250
|
+
label += " (current)"
|
|
251
|
+
yield Static(label, classes=classes, id=f"model-{i}")
|
|
252
|
+
|
|
253
|
+
def on_mount(self) -> None:
|
|
254
|
+
"""Focus self when mounted."""
|
|
255
|
+
self.focus()
|
|
256
|
+
|
|
257
|
+
def watch_selected_index(self, old_index: int, new_index: int) -> None:
|
|
258
|
+
"""Update visual selection when index changes."""
|
|
259
|
+
try:
|
|
260
|
+
# Update old option
|
|
261
|
+
old_option = self.query_one(f"#model-{old_index}", Static)
|
|
262
|
+
old_option.remove_class("selected")
|
|
263
|
+
old_option.add_class("not-selected")
|
|
264
|
+
old_model = self.models[old_index]
|
|
265
|
+
old_label = f" {old_model}"
|
|
266
|
+
if old_model == self.current_model:
|
|
267
|
+
old_label += " (current)"
|
|
268
|
+
old_option.update(old_label)
|
|
269
|
+
|
|
270
|
+
# Update new option
|
|
271
|
+
new_option = self.query_one(f"#model-{new_index}", Static)
|
|
272
|
+
new_option.remove_class("not-selected")
|
|
273
|
+
new_option.add_class("selected")
|
|
274
|
+
new_model = self.models[new_index]
|
|
275
|
+
new_label = f" ▶ {new_model}"
|
|
276
|
+
if new_model == self.current_model:
|
|
277
|
+
new_label += " (current)"
|
|
278
|
+
new_option.update(new_label)
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def action_move_up(self) -> None:
|
|
283
|
+
"""Move selection up."""
|
|
284
|
+
self.selected_index = (self.selected_index - 1) % len(self.models)
|
|
285
|
+
|
|
286
|
+
def action_move_down(self) -> None:
|
|
287
|
+
"""Move selection down."""
|
|
288
|
+
self.selected_index = (self.selected_index + 1) % len(self.models)
|
|
289
|
+
|
|
290
|
+
def action_confirm(self) -> None:
|
|
291
|
+
"""Confirm current selection."""
|
|
292
|
+
selected_model = self.models[self.selected_index]
|
|
293
|
+
self.post_message(self.ModelSelected(selected_model))
|
|
294
|
+
|
|
295
|
+
def action_cancel(self) -> None:
|
|
296
|
+
"""Cancel selection."""
|
|
297
|
+
self.post_message(self.ModelCancelled())
|
|
298
|
+
|
|
299
|
+
class ModelSelected(TextualMessage):
|
|
300
|
+
"""Message sent when user selects a model."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, model: str) -> None:
|
|
303
|
+
super().__init__()
|
|
304
|
+
self.model = model
|
|
305
|
+
|
|
306
|
+
class ModelCancelled(TextualMessage):
|
|
307
|
+
"""Message sent when user cancels selection."""
|
|
308
|
+
|
|
309
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Conversation display widget for Kader CLI."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import VerticalScroll
|
|
5
|
+
from textual.widgets import Markdown, Static
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Message(Static):
|
|
9
|
+
"""A single message in the conversation."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, content: str, role: str = "user") -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.content = content
|
|
14
|
+
self.role = role
|
|
15
|
+
self.add_class(f"message-{role}")
|
|
16
|
+
|
|
17
|
+
def compose(self) -> ComposeResult:
|
|
18
|
+
prefix = "👤 **You:**" if self.role == "user" else "🤖 **Kader:**"
|
|
19
|
+
yield Markdown(f"{prefix}\n\n{self.content}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConversationView(VerticalScroll):
|
|
23
|
+
"""Scrollable conversation history with markdown rendering."""
|
|
24
|
+
|
|
25
|
+
DEFAULT_CSS = """
|
|
26
|
+
ConversationView {
|
|
27
|
+
padding: 1 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ConversationView Message {
|
|
31
|
+
margin-bottom: 1;
|
|
32
|
+
padding: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ConversationView .message-user {
|
|
36
|
+
background: $surface;
|
|
37
|
+
border-left: thick $primary;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ConversationView .message-assistant {
|
|
41
|
+
background: $surface-darken-1;
|
|
42
|
+
border-left: thick $success;
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def add_message(self, content: str, role: str = "user") -> None:
|
|
47
|
+
"""Add a message to the conversation."""
|
|
48
|
+
message = Message(content, role)
|
|
49
|
+
self.mount(message)
|
|
50
|
+
self.scroll_end(animate=True)
|
|
51
|
+
|
|
52
|
+
def clear_messages(self) -> None:
|
|
53
|
+
"""Clear all messages from the conversation."""
|
|
54
|
+
for child in self.query(Message):
|
|
55
|
+
child.remove()
|
cli/widgets/loading.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Loading spinner widget for Kader CLI."""
|
|
2
|
+
|
|
3
|
+
from textual.reactive import reactive
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoadingSpinner(Static):
|
|
8
|
+
"""Animated loading spinner shown during LLM response generation."""
|
|
9
|
+
|
|
10
|
+
DEFAULT_CSS = """
|
|
11
|
+
LoadingSpinner {
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: auto;
|
|
14
|
+
padding: 1 2;
|
|
15
|
+
color: $text-muted;
|
|
16
|
+
text-style: italic;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
LoadingSpinner.hidden {
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
25
|
+
|
|
26
|
+
frame_index: reactive[int] = reactive(0)
|
|
27
|
+
is_spinning: reactive[bool] = reactive(False)
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.add_class("hidden")
|
|
32
|
+
self._timer = None
|
|
33
|
+
|
|
34
|
+
def on_mount(self) -> None:
|
|
35
|
+
"""Start the animation timer when mounted."""
|
|
36
|
+
self._timer = self.set_interval(0.1, self._advance_frame)
|
|
37
|
+
|
|
38
|
+
def _advance_frame(self) -> None:
|
|
39
|
+
"""Advance to the next spinner frame."""
|
|
40
|
+
if self.is_spinning:
|
|
41
|
+
self.frame_index = (self.frame_index + 1) % len(self.SPINNER_FRAMES)
|
|
42
|
+
|
|
43
|
+
def watch_frame_index(self, frame_index: int) -> None:
|
|
44
|
+
"""Update display when frame changes."""
|
|
45
|
+
if self.is_spinning:
|
|
46
|
+
spinner = self.SPINNER_FRAMES[frame_index]
|
|
47
|
+
self.update(f"{spinner} Kader is thinking...")
|
|
48
|
+
|
|
49
|
+
def start(self) -> None:
|
|
50
|
+
"""Start the loading animation."""
|
|
51
|
+
self.is_spinning = True
|
|
52
|
+
self.remove_class("hidden")
|
|
53
|
+
self.frame_index = 0
|
|
54
|
+
|
|
55
|
+
def stop(self) -> None:
|
|
56
|
+
"""Stop the loading animation."""
|
|
57
|
+
self.is_spinning = False
|
|
58
|
+
self.add_class("hidden")
|
|
59
|
+
self.update("")
|
kader/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Kader module initialization.
|
|
3
|
+
|
|
4
|
+
This module sets up the required configuration when imported, including
|
|
5
|
+
creating the .kader directory in the user's home directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .config import ENV_FILE_PATH, KADER_DIR, initialize_kader_config
|
|
9
|
+
from .providers import * # noqa: F401, F403
|
|
10
|
+
from .tools import * # noqa: F401, F403
|
|
11
|
+
|
|
12
|
+
# Initialize the configuration when the module is imported
|
|
13
|
+
initialize_kader_config()
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__author__ = "Kader Project"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"KADER_DIR",
|
|
19
|
+
"ENV_FILE_PATH",
|
|
20
|
+
"initialize_kader_config",
|
|
21
|
+
# Export everything from providers and tools
|
|
22
|
+
]
|
kader/agent/__init__.py
ADDED
kader/agent/agents.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Specific Agent Implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from kader.agent.base import BaseAgent
|
|
8
|
+
from kader.memory import ConversationManager
|
|
9
|
+
from kader.prompts import PlanningAgentPrompt, PromptBase, ReActAgentPrompt
|
|
10
|
+
from kader.providers.base import BaseLLMProvider
|
|
11
|
+
from kader.tools import BaseTool, TodoTool, ToolRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReActAgent(BaseAgent):
|
|
15
|
+
"""
|
|
16
|
+
ReAct (Reasoning and Acting) Agent.
|
|
17
|
+
|
|
18
|
+
Uses a ReAct prompt strategy to reason about tasks and use tools.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str,
|
|
24
|
+
tools: Union[list[BaseTool], ToolRegistry],
|
|
25
|
+
system_prompt: Optional[Union[str, PromptBase]] = None,
|
|
26
|
+
provider: Optional[BaseLLMProvider] = None,
|
|
27
|
+
memory: Optional[ConversationManager] = None,
|
|
28
|
+
retry_attempts: int = 3,
|
|
29
|
+
model_name: str = "qwen3-coder:480b-cloud",
|
|
30
|
+
session_id: Optional[str] = None,
|
|
31
|
+
use_persistence: bool = False,
|
|
32
|
+
interrupt_before_tool: bool = True,
|
|
33
|
+
tool_confirmation_callback: Optional[callable] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
# Resolve tools for prompt context if necessary
|
|
36
|
+
# The base agent handles tool registration, but for the prompt template
|
|
37
|
+
# we might need to pass tool descriptions initially.
|
|
38
|
+
|
|
39
|
+
# Temporary logic to get tool names/descriptions for the prompt
|
|
40
|
+
# In a real scenario, this might need dynamic updates or be handled by the Prompt class itself
|
|
41
|
+
# accessing the agent's registry. Here we do a best-effort pre-fill.
|
|
42
|
+
|
|
43
|
+
_tools_list = []
|
|
44
|
+
if isinstance(tools, list):
|
|
45
|
+
_tools_list = tools
|
|
46
|
+
elif isinstance(tools, ToolRegistry):
|
|
47
|
+
_tools_list = tools.tools
|
|
48
|
+
|
|
49
|
+
tool_names = ", ".join([t.name for t in _tools_list])
|
|
50
|
+
|
|
51
|
+
if system_prompt is None:
|
|
52
|
+
system_prompt = ReActAgentPrompt(
|
|
53
|
+
tools=_tools_list,
|
|
54
|
+
tool_names=tool_names,
|
|
55
|
+
input="", # This acts as a placeholder or initial context
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
super().__init__(
|
|
59
|
+
name=name,
|
|
60
|
+
system_prompt=system_prompt,
|
|
61
|
+
tools=tools,
|
|
62
|
+
provider=provider,
|
|
63
|
+
memory=memory,
|
|
64
|
+
retry_attempts=retry_attempts,
|
|
65
|
+
model_name=model_name,
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
use_persistence=use_persistence,
|
|
68
|
+
interrupt_before_tool=interrupt_before_tool,
|
|
69
|
+
tool_confirmation_callback=tool_confirmation_callback,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PlanningAgent(BaseAgent):
|
|
74
|
+
"""
|
|
75
|
+
Planning Agent.
|
|
76
|
+
|
|
77
|
+
Breaks tasks into plans and executes them.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
name: str,
|
|
83
|
+
tools: Union[list[BaseTool], ToolRegistry],
|
|
84
|
+
system_prompt: Optional[Union[str, PromptBase]] = None,
|
|
85
|
+
provider: Optional[BaseLLMProvider] = None,
|
|
86
|
+
memory: Optional[ConversationManager] = None,
|
|
87
|
+
retry_attempts: int = 3,
|
|
88
|
+
model_name: str = "qwen3-coder:480b-cloud",
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
use_persistence: bool = False,
|
|
91
|
+
interrupt_before_tool: bool = True,
|
|
92
|
+
tool_confirmation_callback: Optional[callable] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
# Ensure TodoTool is available
|
|
95
|
+
_todo_tool = TodoTool()
|
|
96
|
+
if isinstance(tools, ToolRegistry):
|
|
97
|
+
if _todo_tool.name not in tools:
|
|
98
|
+
tools.register(_todo_tool)
|
|
99
|
+
elif isinstance(tools, list):
|
|
100
|
+
if not any(t.name == _todo_tool.name for t in tools):
|
|
101
|
+
tools.append(_todo_tool)
|
|
102
|
+
|
|
103
|
+
_tools_list = []
|
|
104
|
+
if isinstance(tools, list):
|
|
105
|
+
_tools_list = tools
|
|
106
|
+
elif isinstance(tools, ToolRegistry):
|
|
107
|
+
_tools_list = tools.tools
|
|
108
|
+
|
|
109
|
+
if system_prompt is None:
|
|
110
|
+
system_prompt = PlanningAgentPrompt(
|
|
111
|
+
tools=_tools_list, input="", agent_scratchpad=""
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
super().__init__(
|
|
115
|
+
name=name,
|
|
116
|
+
system_prompt=system_prompt,
|
|
117
|
+
tools=tools,
|
|
118
|
+
provider=provider,
|
|
119
|
+
memory=memory,
|
|
120
|
+
retry_attempts=retry_attempts,
|
|
121
|
+
model_name=model_name,
|
|
122
|
+
session_id=session_id,
|
|
123
|
+
use_persistence=use_persistence,
|
|
124
|
+
interrupt_before_tool=interrupt_before_tool,
|
|
125
|
+
tool_confirmation_callback=tool_confirmation_callback,
|
|
126
|
+
)
|