idun-agent-engine 0.3.9__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.
- idun_agent_engine/_version.py +1 -1
- idun_agent_engine/agent/adk/adk.py +5 -2
- idun_agent_engine/agent/langgraph/langgraph.py +1 -1
- idun_agent_engine/core/app_factory.py +1 -1
- idun_agent_engine/core/config_builder.py +11 -5
- idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
- idun_agent_engine/mcp/__init__.py +18 -2
- idun_agent_engine/mcp/helpers.py +135 -43
- idun_agent_engine/mcp/registry.py +7 -1
- idun_agent_engine/server/lifespan.py +22 -0
- idun_agent_engine/telemetry/__init__.py +19 -0
- idun_agent_engine/telemetry/config.py +29 -0
- idun_agent_engine/telemetry/telemetry.py +248 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/METADATA +12 -8
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/RECORD +45 -17
- idun_platform_cli/groups/agent/package.py +3 -0
- idun_platform_cli/groups/agent/serve.py +2 -0
- idun_platform_cli/groups/init.py +25 -0
- idun_platform_cli/main.py +3 -0
- idun_platform_cli/telemetry.py +54 -0
- idun_platform_cli/tui/__init__.py +0 -0
- idun_platform_cli/tui/css/__init__.py +0 -0
- idun_platform_cli/tui/css/create_agent.py +912 -0
- idun_platform_cli/tui/css/main.py +89 -0
- idun_platform_cli/tui/main.py +87 -0
- idun_platform_cli/tui/schemas/__init__.py +0 -0
- idun_platform_cli/tui/schemas/create_agent.py +60 -0
- idun_platform_cli/tui/screens/__init__.py +0 -0
- idun_platform_cli/tui/screens/create_agent.py +622 -0
- idun_platform_cli/tui/utils/__init__.py +0 -0
- idun_platform_cli/tui/utils/config.py +182 -0
- idun_platform_cli/tui/validators/__init__.py +0 -0
- idun_platform_cli/tui/validators/guardrails.py +88 -0
- idun_platform_cli/tui/validators/mcps.py +84 -0
- idun_platform_cli/tui/validators/observability.py +65 -0
- idun_platform_cli/tui/widgets/__init__.py +19 -0
- idun_platform_cli/tui/widgets/chat_widget.py +153 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +356 -0
- idun_platform_cli/tui/widgets/identity_widget.py +252 -0
- idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
- idun_platform_cli/tui/widgets/memory_widget.py +195 -0
- idun_platform_cli/tui/widgets/observability_widget.py +382 -0
- idun_platform_cli/tui/widgets/serve_widget.py +82 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Guardrails configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical, Grid
|
|
5
|
+
from textual.widgets import Static, Input, Switch, RadioSet, RadioButton, TextArea
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
9
|
+
from idun_platform_cli.tui.validators.guardrails import validate_guardrail
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GuardrailsWidget(Widget):
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.guardrails_data = {
|
|
16
|
+
"bias_check": {"enabled": False, "applies_to": "both", "config": {}},
|
|
17
|
+
"toxic_language": {"enabled": False, "applies_to": "both", "config": {}},
|
|
18
|
+
"competition_check": {"enabled": False, "applies_to": "both", "config": {}},
|
|
19
|
+
"ban_list": {"enabled": False, "applies_to": "both", "config": {}},
|
|
20
|
+
"detect_pii": {"enabled": False, "applies_to": "both", "config": {}},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def compose(self) -> ComposeResult:
|
|
24
|
+
api_key_section = Vertical(classes="global-api-key-section")
|
|
25
|
+
api_key_section.border_title = "Global API Key"
|
|
26
|
+
api_key_section.compose_add_child(
|
|
27
|
+
Static(
|
|
28
|
+
"API Key (for guardrails that require it):", classes="gr-label-small"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
api_key_section.compose_add_child(
|
|
32
|
+
Input(
|
|
33
|
+
placeholder="Enter API key",
|
|
34
|
+
password=True,
|
|
35
|
+
id="global_guardrails_api_key",
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
yield api_key_section
|
|
39
|
+
|
|
40
|
+
grid = Grid(classes="guardrails-grid")
|
|
41
|
+
for card in self._create_all_cards():
|
|
42
|
+
grid.compose_add_child(card)
|
|
43
|
+
yield grid
|
|
44
|
+
|
|
45
|
+
def _create_all_cards(self):
|
|
46
|
+
return [
|
|
47
|
+
self._create_bias_check_card(),
|
|
48
|
+
self._create_toxic_language_card(),
|
|
49
|
+
self._create_competition_check_card(),
|
|
50
|
+
self._create_ban_list_card(),
|
|
51
|
+
self._create_detect_pii_card(),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
def _create_bias_check_card(self) -> Vertical:
|
|
55
|
+
card = Vertical(classes="guardrail-card", id="bias_check_card")
|
|
56
|
+
card.border_title = "Bias Check"
|
|
57
|
+
|
|
58
|
+
header = Horizontal(classes="guardrail-header")
|
|
59
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
60
|
+
header.compose_add_child(Switch(value=False, id="bias_check_enabled"))
|
|
61
|
+
card.compose_add_child(header)
|
|
62
|
+
|
|
63
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
64
|
+
applies_section.compose_add_child(
|
|
65
|
+
Static("Applies To:", classes="gr-label-small")
|
|
66
|
+
)
|
|
67
|
+
radio_set = RadioSet(id="bias_check_applies_to")
|
|
68
|
+
radio_set.compose_add_child(RadioButton("Input", id="bias_check_input"))
|
|
69
|
+
radio_set.compose_add_child(RadioButton("Output", id="bias_check_output"))
|
|
70
|
+
radio_set.compose_add_child(
|
|
71
|
+
RadioButton("Both", value=True, id="bias_check_both")
|
|
72
|
+
)
|
|
73
|
+
applies_section.compose_add_child(radio_set)
|
|
74
|
+
card.compose_add_child(applies_section)
|
|
75
|
+
|
|
76
|
+
config_section = Vertical(classes="config-section", id="bias_check_config")
|
|
77
|
+
config_section.display = False
|
|
78
|
+
config_section.compose_add_child(
|
|
79
|
+
Static("Threshold (0.0-1.0):", classes="gr-label-small")
|
|
80
|
+
)
|
|
81
|
+
config_section.compose_add_child(
|
|
82
|
+
Input(placeholder="0.5", id="bias_check_threshold")
|
|
83
|
+
)
|
|
84
|
+
card.compose_add_child(config_section)
|
|
85
|
+
|
|
86
|
+
return card
|
|
87
|
+
|
|
88
|
+
def _create_toxic_language_card(self) -> Vertical:
|
|
89
|
+
card = Vertical(classes="guardrail-card", id="toxic_language_card")
|
|
90
|
+
card.border_title = "Toxic Language"
|
|
91
|
+
|
|
92
|
+
header = Horizontal(classes="guardrail-header")
|
|
93
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
94
|
+
header.compose_add_child(Switch(value=False, id="toxic_language_enabled"))
|
|
95
|
+
card.compose_add_child(header)
|
|
96
|
+
|
|
97
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
98
|
+
applies_section.compose_add_child(
|
|
99
|
+
Static("Applies To:", classes="gr-label-small")
|
|
100
|
+
)
|
|
101
|
+
radio_set = RadioSet(id="toxic_language_applies_to")
|
|
102
|
+
radio_set.compose_add_child(RadioButton("Input", id="toxic_language_input"))
|
|
103
|
+
radio_set.compose_add_child(RadioButton("Output", id="toxic_language_output"))
|
|
104
|
+
radio_set.compose_add_child(
|
|
105
|
+
RadioButton("Both", value=True, id="toxic_language_both")
|
|
106
|
+
)
|
|
107
|
+
applies_section.compose_add_child(radio_set)
|
|
108
|
+
card.compose_add_child(applies_section)
|
|
109
|
+
|
|
110
|
+
config_section = Vertical(classes="config-section", id="toxic_language_config")
|
|
111
|
+
config_section.display = False
|
|
112
|
+
config_section.compose_add_child(
|
|
113
|
+
Static("Threshold (0.0-1.0):", classes="gr-label-small")
|
|
114
|
+
)
|
|
115
|
+
config_section.compose_add_child(
|
|
116
|
+
Input(placeholder="0.5", id="toxic_language_threshold")
|
|
117
|
+
)
|
|
118
|
+
card.compose_add_child(config_section)
|
|
119
|
+
|
|
120
|
+
return card
|
|
121
|
+
|
|
122
|
+
def _create_competition_check_card(self) -> Vertical:
|
|
123
|
+
card = Vertical(classes="guardrail-card", id="competition_check_card")
|
|
124
|
+
card.border_title = "Competition Check"
|
|
125
|
+
|
|
126
|
+
header = Horizontal(classes="guardrail-header")
|
|
127
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
128
|
+
header.compose_add_child(Switch(value=False, id="competition_check_enabled"))
|
|
129
|
+
card.compose_add_child(header)
|
|
130
|
+
|
|
131
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
132
|
+
applies_section.compose_add_child(
|
|
133
|
+
Static("Applies To:", classes="gr-label-small")
|
|
134
|
+
)
|
|
135
|
+
radio_set = RadioSet(id="competition_check_applies_to")
|
|
136
|
+
radio_set.compose_add_child(RadioButton("Input", id="competition_check_input"))
|
|
137
|
+
radio_set.compose_add_child(
|
|
138
|
+
RadioButton("Output", id="competition_check_output")
|
|
139
|
+
)
|
|
140
|
+
radio_set.compose_add_child(
|
|
141
|
+
RadioButton("Both", value=True, id="competition_check_both")
|
|
142
|
+
)
|
|
143
|
+
applies_section.compose_add_child(radio_set)
|
|
144
|
+
card.compose_add_child(applies_section)
|
|
145
|
+
|
|
146
|
+
config_section = Vertical(
|
|
147
|
+
classes="config-section", id="competition_check_config"
|
|
148
|
+
)
|
|
149
|
+
config_section.display = False
|
|
150
|
+
config_section.compose_add_child(
|
|
151
|
+
Static("Competitors (comma-separated):", classes="gr-label-small")
|
|
152
|
+
)
|
|
153
|
+
competitors_textarea = TextArea(
|
|
154
|
+
id="competition_check_competitors", classes="gr-textarea"
|
|
155
|
+
)
|
|
156
|
+
competitors_textarea.placeholder = (
|
|
157
|
+
"e.g., Competitor-1, Competitor-2, Competitor-3..."
|
|
158
|
+
)
|
|
159
|
+
config_section.compose_add_child(competitors_textarea)
|
|
160
|
+
card.compose_add_child(config_section)
|
|
161
|
+
|
|
162
|
+
return card
|
|
163
|
+
|
|
164
|
+
def _create_ban_list_card(self) -> Vertical:
|
|
165
|
+
card = Vertical(classes="guardrail-card", id="ban_list_card")
|
|
166
|
+
card.border_title = "Ban List"
|
|
167
|
+
|
|
168
|
+
header = Horizontal(classes="guardrail-header")
|
|
169
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
170
|
+
header.compose_add_child(Switch(value=False, id="ban_list_enabled"))
|
|
171
|
+
card.compose_add_child(header)
|
|
172
|
+
|
|
173
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
174
|
+
applies_section.compose_add_child(
|
|
175
|
+
Static("Applies To:", classes="gr-label-small")
|
|
176
|
+
)
|
|
177
|
+
radio_set = RadioSet(id="ban_list_applies_to")
|
|
178
|
+
radio_set.compose_add_child(RadioButton("Input", id="ban_list_input"))
|
|
179
|
+
radio_set.compose_add_child(RadioButton("Output", id="ban_list_output"))
|
|
180
|
+
radio_set.compose_add_child(RadioButton("Both", value=True, id="ban_list_both"))
|
|
181
|
+
applies_section.compose_add_child(radio_set)
|
|
182
|
+
card.compose_add_child(applies_section)
|
|
183
|
+
|
|
184
|
+
config_section = Vertical(classes="config-section", id="ban_list_config")
|
|
185
|
+
config_section.display = False
|
|
186
|
+
config_section.compose_add_child(
|
|
187
|
+
Static("Reject Message:", classes="gr-label-small")
|
|
188
|
+
)
|
|
189
|
+
config_section.compose_add_child(
|
|
190
|
+
Input(placeholder="Message to show", id="ban_list_reject_message")
|
|
191
|
+
)
|
|
192
|
+
config_section.compose_add_child(
|
|
193
|
+
Static("Banned Words (comma-separated):", classes="gr-label-small")
|
|
194
|
+
)
|
|
195
|
+
banned_words_textarea = TextArea(
|
|
196
|
+
id="ban_list_banned_words", classes="gr-textarea"
|
|
197
|
+
)
|
|
198
|
+
banned_words_textarea.placeholder = "Words to ban..."
|
|
199
|
+
config_section.compose_add_child(banned_words_textarea)
|
|
200
|
+
card.compose_add_child(config_section)
|
|
201
|
+
|
|
202
|
+
return card
|
|
203
|
+
|
|
204
|
+
def _create_detect_pii_card(self) -> Vertical:
|
|
205
|
+
card = Vertical(classes="guardrail-card", id="detect_pii_card")
|
|
206
|
+
card.border_title = "Detect PII"
|
|
207
|
+
|
|
208
|
+
header = Horizontal(classes="guardrail-header")
|
|
209
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
210
|
+
header.compose_add_child(Switch(value=False, id="detect_pii_enabled"))
|
|
211
|
+
card.compose_add_child(header)
|
|
212
|
+
|
|
213
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
214
|
+
applies_section.compose_add_child(
|
|
215
|
+
Static("Applies To:", classes="gr-label-small")
|
|
216
|
+
)
|
|
217
|
+
radio_set = RadioSet(id="detect_pii_applies_to")
|
|
218
|
+
radio_set.compose_add_child(RadioButton("Input", id="detect_pii_input"))
|
|
219
|
+
radio_set.compose_add_child(RadioButton("Output", id="detect_pii_output"))
|
|
220
|
+
radio_set.compose_add_child(
|
|
221
|
+
RadioButton("Both", value=True, id="detect_pii_both")
|
|
222
|
+
)
|
|
223
|
+
applies_section.compose_add_child(radio_set)
|
|
224
|
+
card.compose_add_child(applies_section)
|
|
225
|
+
|
|
226
|
+
config_section = Vertical(classes="config-section", id="detect_pii_config")
|
|
227
|
+
config_section.display = False
|
|
228
|
+
config_section.compose_add_child(
|
|
229
|
+
Static("Reject Message:", classes="gr-label-small")
|
|
230
|
+
)
|
|
231
|
+
config_section.compose_add_child(
|
|
232
|
+
Input(placeholder="Message to show", id="detect_pii_reject_message")
|
|
233
|
+
)
|
|
234
|
+
config_section.compose_add_child(
|
|
235
|
+
Static("PII Entities (comma-separated):", classes="gr-label-small")
|
|
236
|
+
)
|
|
237
|
+
pii_entities_textarea = TextArea(
|
|
238
|
+
id="detect_pii_pii_entities", classes="gr-textarea"
|
|
239
|
+
)
|
|
240
|
+
pii_entities_textarea.placeholder = "e.g., EMAIL_ADDRESS, PHONE_NUMBER, SSN..."
|
|
241
|
+
config_section.compose_add_child(pii_entities_textarea)
|
|
242
|
+
card.compose_add_child(config_section)
|
|
243
|
+
|
|
244
|
+
return card
|
|
245
|
+
|
|
246
|
+
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
247
|
+
switch_id = event.switch.id
|
|
248
|
+
if not switch_id:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
guardrail_type = switch_id.replace("_enabled", "")
|
|
252
|
+
config_section_id = f"{guardrail_type}_config"
|
|
253
|
+
card_id = f"{guardrail_type}_card"
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
config_section = self.query_one(f"#{config_section_id}")
|
|
257
|
+
card = self.query_one(f"#{card_id}")
|
|
258
|
+
|
|
259
|
+
config_section.display = event.value
|
|
260
|
+
self.guardrails_data[guardrail_type]["enabled"] = event.value
|
|
261
|
+
|
|
262
|
+
if event.value:
|
|
263
|
+
card.add_class("guardrail-card-enabled")
|
|
264
|
+
else:
|
|
265
|
+
card.remove_class("guardrail-card-enabled")
|
|
266
|
+
except:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
def get_data(self) -> GuardrailsV2 | None:
|
|
270
|
+
input_guardrails = []
|
|
271
|
+
output_guardrails = []
|
|
272
|
+
|
|
273
|
+
for guardrail_id in self.guardrails_data.keys():
|
|
274
|
+
enabled_switch = self.query_one(f"#{guardrail_id}_enabled", Switch)
|
|
275
|
+
if not enabled_switch.value:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
applies_to_radioset = self.query_one(
|
|
279
|
+
f"#{guardrail_id}_applies_to", RadioSet
|
|
280
|
+
)
|
|
281
|
+
pressed_button = applies_to_radioset.pressed_button
|
|
282
|
+
if not pressed_button:
|
|
283
|
+
applies_to = "both"
|
|
284
|
+
else:
|
|
285
|
+
button_id = str(pressed_button.id)
|
|
286
|
+
if "input" in button_id:
|
|
287
|
+
applies_to = "input"
|
|
288
|
+
elif "output" in button_id:
|
|
289
|
+
applies_to = "output"
|
|
290
|
+
else:
|
|
291
|
+
applies_to = "both"
|
|
292
|
+
|
|
293
|
+
config_dict = self._extract_config(guardrail_id)
|
|
294
|
+
validated_config, msg = validate_guardrail(guardrail_id, config_dict)
|
|
295
|
+
|
|
296
|
+
if not validated_config:
|
|
297
|
+
self.app.notify(
|
|
298
|
+
f"Validation error for {guardrail_id}: {msg}", severity="error"
|
|
299
|
+
)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
if applies_to in ["input", "both"]:
|
|
303
|
+
input_guardrails.append(validated_config)
|
|
304
|
+
if applies_to in ["output", "both"]:
|
|
305
|
+
output_guardrails.append(validated_config)
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
return GuardrailsV2(input=input_guardrails, output=output_guardrails)
|
|
309
|
+
except Exception:
|
|
310
|
+
self.app.notify(
|
|
311
|
+
"Error validating Guardrails: make sure all fields are correct.",
|
|
312
|
+
severity="error",
|
|
313
|
+
timeout=10
|
|
314
|
+
)
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def _extract_config(self, guardrail_id: str) -> dict:
|
|
318
|
+
config = {}
|
|
319
|
+
global_api_key = self.query_one("#global_guardrails_api_key", Input).value
|
|
320
|
+
|
|
321
|
+
match guardrail_id:
|
|
322
|
+
case "bias_check":
|
|
323
|
+
threshold_input = self.query_one("#bias_check_threshold", Input)
|
|
324
|
+
config["threshold"] = threshold_input.value or "0.5"
|
|
325
|
+
|
|
326
|
+
case "toxic_language":
|
|
327
|
+
threshold_input = self.query_one("#toxic_language_threshold", Input)
|
|
328
|
+
config["threshold"] = threshold_input.value or "0.5"
|
|
329
|
+
|
|
330
|
+
case "competition_check":
|
|
331
|
+
competitors_textarea = self.query_one(
|
|
332
|
+
"#competition_check_competitors", TextArea
|
|
333
|
+
)
|
|
334
|
+
config["competitors"] = competitors_textarea.text
|
|
335
|
+
|
|
336
|
+
case "ban_list":
|
|
337
|
+
config["api_key"] = global_api_key
|
|
338
|
+
config["reject_message"] = self.query_one(
|
|
339
|
+
"#ban_list_reject_message", Input
|
|
340
|
+
).value
|
|
341
|
+
banned_words_textarea = self.query_one(
|
|
342
|
+
"#ban_list_banned_words", TextArea
|
|
343
|
+
)
|
|
344
|
+
config["banned_words"] = banned_words_textarea.text
|
|
345
|
+
|
|
346
|
+
case "detect_pii":
|
|
347
|
+
config["api_key"] = global_api_key
|
|
348
|
+
config["reject_message"] = self.query_one(
|
|
349
|
+
"#detect_pii_reject_message", Input
|
|
350
|
+
).value
|
|
351
|
+
pii_entities_textarea = self.query_one(
|
|
352
|
+
"#detect_pii_pii_entities", TextArea
|
|
353
|
+
)
|
|
354
|
+
config["pii_entities"] = pii_entities_textarea.text
|
|
355
|
+
|
|
356
|
+
return config
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Identity configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.reactive import reactive
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
from textual.widgets import DirectoryTree, Input, Markdown, OptionList, Static
|
|
8
|
+
from textual.widgets.option_list import Option
|
|
9
|
+
|
|
10
|
+
HELP_TEXT = """
|
|
11
|
+
**Quick Guide**
|
|
12
|
+
|
|
13
|
+
- **Name** : Name of your agent
|
|
14
|
+
- **Framework** : LangGraph/ADK/Haystack
|
|
15
|
+
- **Port** : Network port
|
|
16
|
+
- **Graph** : Select .py file + variable
|
|
17
|
+
|
|
18
|
+
[📚 Docs](https://idun-group.github.io/idun-agent-platform) | [💬 Help](https://discord.gg/KCZ6nW2jQe)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IdentityWidget(Widget):
|
|
23
|
+
full_definition = reactive("")
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
self.selected_file_path = ""
|
|
28
|
+
self.selected_variable = ""
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
agent_info_section = Horizontal(
|
|
32
|
+
classes="section section-split agent-info-section"
|
|
33
|
+
)
|
|
34
|
+
agent_info_section.border_title = "Agent Information"
|
|
35
|
+
|
|
36
|
+
with agent_info_section:
|
|
37
|
+
identity_container = Vertical(classes="form-fields-container")
|
|
38
|
+
identity_container.border_title = "Identity"
|
|
39
|
+
|
|
40
|
+
with identity_container:
|
|
41
|
+
with Horizontal(classes="field-row"):
|
|
42
|
+
yield Static("Name:", classes="field-label")
|
|
43
|
+
yield Input(
|
|
44
|
+
value="my-agent", classes="field-input", id="name_input"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
with Horizontal(classes="field-row framework-row"):
|
|
48
|
+
yield Static("Framework:", classes="field-label")
|
|
49
|
+
yield OptionList(
|
|
50
|
+
Option("LANGGRAPH", id="LANGGRAPH"),
|
|
51
|
+
Option("ADK", id="ADK"),
|
|
52
|
+
Option("HAYSTACK", id="HAYSTACK"),
|
|
53
|
+
classes="field-input",
|
|
54
|
+
id="framework_select",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with Horizontal(classes="field-row"):
|
|
58
|
+
yield Static("Port:", classes="field-label")
|
|
59
|
+
yield Input(value="8008", classes="field-input", id="port_input")
|
|
60
|
+
|
|
61
|
+
yield Static("", classes="error-message", id="identity_error")
|
|
62
|
+
|
|
63
|
+
with Vertical(classes="info-panel"):
|
|
64
|
+
yield Markdown(HELP_TEXT, classes="help-markdown")
|
|
65
|
+
|
|
66
|
+
graph_section = Vertical(classes="graph-definition-section")
|
|
67
|
+
graph_section.border_title = "Graph Definition"
|
|
68
|
+
|
|
69
|
+
with graph_section:
|
|
70
|
+
with Horizontal(classes="graph-def-row"):
|
|
71
|
+
tree_container = Vertical(classes="tree-container")
|
|
72
|
+
tree_container.border_title = "Select Python File"
|
|
73
|
+
with tree_container:
|
|
74
|
+
yield DirectoryTree(".", id="file_tree")
|
|
75
|
+
|
|
76
|
+
var_container = Vertical(classes="var-container")
|
|
77
|
+
var_container.border_title = "Select Variable"
|
|
78
|
+
with var_container:
|
|
79
|
+
yield OptionList(
|
|
80
|
+
classes="var-list",
|
|
81
|
+
id="variable_list",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
path_container = Vertical(classes="path-display-container")
|
|
85
|
+
path_container.border_title = "Agent Path"
|
|
86
|
+
with path_container:
|
|
87
|
+
yield Static(
|
|
88
|
+
"Select file and variable",
|
|
89
|
+
classes="full-definition-display",
|
|
90
|
+
id="full_definition",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def on_mount(self) -> None:
|
|
94
|
+
option_list = self.query_one("#framework_select", OptionList)
|
|
95
|
+
option_list.highlighted = 0
|
|
96
|
+
self._update_section_labels()
|
|
97
|
+
|
|
98
|
+
def watch_full_definition(self, value: str) -> None:
|
|
99
|
+
self.query_one("#full_definition", Static).update(
|
|
100
|
+
value if value else "Select file and variable"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def on_directory_tree_file_selected(
|
|
104
|
+
self, event: DirectoryTree.FileSelected
|
|
105
|
+
) -> None:
|
|
106
|
+
selected_path = str(event.path)
|
|
107
|
+
|
|
108
|
+
if selected_path.endswith(".py"):
|
|
109
|
+
self.selected_file_path = selected_path
|
|
110
|
+
self._parse_python_file(selected_path)
|
|
111
|
+
self._update_full_definition()
|
|
112
|
+
else:
|
|
113
|
+
self.app.notify("Please select a Python file", severity="warning")
|
|
114
|
+
|
|
115
|
+
def _parse_python_file(self, file_path: str) -> None:
|
|
116
|
+
import ast
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with open(file_path) as f:
|
|
120
|
+
tree = ast.parse(f.read())
|
|
121
|
+
|
|
122
|
+
variables = []
|
|
123
|
+
for node in ast.walk(tree):
|
|
124
|
+
if isinstance(node, ast.Assign):
|
|
125
|
+
for target in node.targets:
|
|
126
|
+
if isinstance(target, ast.Name):
|
|
127
|
+
variables.append(target.id)
|
|
128
|
+
|
|
129
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
130
|
+
var_list.clear_options()
|
|
131
|
+
self.selected_variable = ""
|
|
132
|
+
|
|
133
|
+
if variables:
|
|
134
|
+
for var in variables:
|
|
135
|
+
var_list.add_option(Option(var, id=var))
|
|
136
|
+
else:
|
|
137
|
+
var_list.add_option(Option("No variables found", id="none"))
|
|
138
|
+
|
|
139
|
+
except Exception:
|
|
140
|
+
self.app.notify(
|
|
141
|
+
"Error parsing file. Make sure it's a valid Python file.",
|
|
142
|
+
severity="error",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def on_option_list_option_highlighted(
|
|
146
|
+
self, event: OptionList.OptionHighlighted
|
|
147
|
+
) -> None:
|
|
148
|
+
if event.option_list.id == "variable_list":
|
|
149
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
150
|
+
if var_list.highlighted is not None:
|
|
151
|
+
variable_option = var_list.get_option_at_index(var_list.highlighted)
|
|
152
|
+
self.selected_variable = str(variable_option.id)
|
|
153
|
+
self._update_full_definition()
|
|
154
|
+
elif event.option_list.id == "framework_select":
|
|
155
|
+
self._update_section_labels()
|
|
156
|
+
|
|
157
|
+
def _update_section_labels(self) -> None:
|
|
158
|
+
framework_select = self.query_one("#framework_select", OptionList)
|
|
159
|
+
if framework_select.highlighted is not None:
|
|
160
|
+
framework_option = framework_select.get_option_at_index(
|
|
161
|
+
framework_select.highlighted
|
|
162
|
+
)
|
|
163
|
+
framework = str(framework_option.id)
|
|
164
|
+
|
|
165
|
+
graph_section = self.query_one(".graph-definition-section", Vertical)
|
|
166
|
+
|
|
167
|
+
if framework == "LANGGRAPH":
|
|
168
|
+
graph_section.border_title = "Graph Definition"
|
|
169
|
+
elif framework == "ADK":
|
|
170
|
+
graph_section.border_title = "Agent Definition"
|
|
171
|
+
elif framework == "HAYSTACK":
|
|
172
|
+
graph_section.border_title = "Pipeline Definition"
|
|
173
|
+
|
|
174
|
+
def _update_full_definition(self) -> None:
|
|
175
|
+
if not self.selected_file_path:
|
|
176
|
+
self.full_definition = ""
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
180
|
+
var_index = var_list.highlighted
|
|
181
|
+
|
|
182
|
+
if var_index is not None and var_list.option_count > 0:
|
|
183
|
+
try:
|
|
184
|
+
variable_option = var_list.get_option_at_index(var_index)
|
|
185
|
+
variable_name = str(variable_option.id)
|
|
186
|
+
self.full_definition = f"{self.selected_file_path}:{variable_name}"
|
|
187
|
+
except:
|
|
188
|
+
self.full_definition = self.selected_file_path
|
|
189
|
+
else:
|
|
190
|
+
self.full_definition = self.selected_file_path
|
|
191
|
+
|
|
192
|
+
def validate(self) -> bool:
|
|
193
|
+
self.query_one("#identity_error", Static).update("")
|
|
194
|
+
|
|
195
|
+
port = self.query_one("#port_input", Input).value
|
|
196
|
+
name = self.query_one("#name_input", Input).value
|
|
197
|
+
|
|
198
|
+
if not port or not name:
|
|
199
|
+
self.query_one("#identity_error", Static).update(
|
|
200
|
+
"Agent name or port is empty!"
|
|
201
|
+
)
|
|
202
|
+
self.app.notify("Agent name and port are required!", severity="error")
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
if not self.selected_file_path:
|
|
206
|
+
self.query_one("#identity_error", Static).update(
|
|
207
|
+
"Please select a Python file"
|
|
208
|
+
)
|
|
209
|
+
self.app.notify(
|
|
210
|
+
"Graph definition incomplete! Select a file.", severity="error"
|
|
211
|
+
)
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
if not self.selected_variable or self.selected_variable == "none":
|
|
215
|
+
self.query_one("#identity_error", Static).update(
|
|
216
|
+
"Please select a variable from the list"
|
|
217
|
+
)
|
|
218
|
+
self.app.notify(
|
|
219
|
+
"Graph definition incomplete! Select a variable.", severity="error"
|
|
220
|
+
)
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
def get_data(self) -> dict:
|
|
226
|
+
var_list = self.query_one("#variable_list", OptionList)
|
|
227
|
+
var_index = var_list.highlighted
|
|
228
|
+
|
|
229
|
+
variable_name = ""
|
|
230
|
+
if var_index is not None and var_list.option_count > 0:
|
|
231
|
+
variable_option = var_list.get_option_at_index(var_index)
|
|
232
|
+
variable_name = str(variable_option.id)
|
|
233
|
+
|
|
234
|
+
graph_definition = (
|
|
235
|
+
f"{self.selected_file_path}:{variable_name}"
|
|
236
|
+
if self.selected_file_path
|
|
237
|
+
else ""
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
option_list = self.query_one("#framework_select", OptionList)
|
|
241
|
+
index = option_list.highlighted
|
|
242
|
+
framework = "LANGGRAPH"
|
|
243
|
+
if index is not None:
|
|
244
|
+
selected_option = option_list.get_option_at_index(index)
|
|
245
|
+
framework = str(selected_option.id)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"name": self.query_one("#name_input", Input).value,
|
|
249
|
+
"framework": framework,
|
|
250
|
+
"port": self.query_one("#port_input", Input).value,
|
|
251
|
+
"graph_definition": graph_definition,
|
|
252
|
+
}
|