idun-agent-engine 0.3.9__py3-none-any.whl → 0.4.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.
- idun_agent_engine/_version.py +1 -1
- 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 +5 -6
- idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
- idun_agent_engine/mcp/__init__.py +18 -2
- idun_agent_engine/mcp/helpers.py +95 -45
- 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.0.dist-info}/METADATA +11 -7
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/RECORD +39 -14
- idun_platform_cli/groups/init.py +23 -0
- idun_platform_cli/main.py +3 -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 +789 -0
- idun_platform_cli/tui/css/main.py +92 -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 +482 -0
- idun_platform_cli/tui/utils/__init__.py +0 -0
- idun_platform_cli/tui/utils/config.py +161 -0
- idun_platform_cli/tui/validators/__init__.py +0 -0
- idun_platform_cli/tui/validators/guardrails.py +76 -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 +15 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +348 -0
- idun_platform_cli/tui/widgets/identity_widget.py +234 -0
- idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
- idun_platform_cli/tui/widgets/observability_widget.py +384 -0
- idun_platform_cli/tui/widgets/serve_widget.py +78 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
4
|
+
from idun_agent_schema.engine.observability_v2 import ObservabilityConfig
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
8
|
+
from textual.reactive import reactive
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.widgets import Button, Footer, Label, RichLog, Static
|
|
11
|
+
|
|
12
|
+
from idun_platform_cli.tui.css.create_agent import CREATE_AGENT_CSS
|
|
13
|
+
from idun_platform_cli.tui.schemas.create_agent import TUIAgentConfig
|
|
14
|
+
from idun_platform_cli.tui.utils.config import ConfigManager
|
|
15
|
+
from idun_platform_cli.tui.widgets import (
|
|
16
|
+
GuardrailsWidget,
|
|
17
|
+
IdentityWidget,
|
|
18
|
+
MCPsWidget,
|
|
19
|
+
ObservabilityWidget,
|
|
20
|
+
ServeWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreateAgentScreen(Screen):
|
|
25
|
+
CSS = CREATE_AGENT_CSS
|
|
26
|
+
BINDINGS = [
|
|
27
|
+
("tab", "toggle_focus_area", "Switch Area / Next Field"),
|
|
28
|
+
("up", "nav_up", "Navigate Panes"),
|
|
29
|
+
("down", "nav_down", ""),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
active_section = reactive("identity")
|
|
33
|
+
nav_panes = [
|
|
34
|
+
"nav-identity",
|
|
35
|
+
"nav-observability",
|
|
36
|
+
"nav-guardrails",
|
|
37
|
+
"nav-mcps",
|
|
38
|
+
"nav-serve",
|
|
39
|
+
]
|
|
40
|
+
current_nav_index = 0
|
|
41
|
+
focus_on_nav = True # Track if focus is on nav or content
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args, **kwargs):
|
|
44
|
+
super().__init__(*args, **kwargs)
|
|
45
|
+
self.widgets_map = {}
|
|
46
|
+
self.config_manager = ConfigManager()
|
|
47
|
+
self.validated_sections = set()
|
|
48
|
+
self.server_process = None
|
|
49
|
+
self.server_running = False
|
|
50
|
+
|
|
51
|
+
def watch_active_section(self, new_section: str) -> None:
|
|
52
|
+
for section_id, widget in self.widgets_map.items():
|
|
53
|
+
if section_id == new_section:
|
|
54
|
+
widget.display = True
|
|
55
|
+
else:
|
|
56
|
+
widget.display = False
|
|
57
|
+
|
|
58
|
+
if new_section == "serve":
|
|
59
|
+
config = self.config_manager.load_config()
|
|
60
|
+
serve_widget = self.widgets_map.get("serve")
|
|
61
|
+
if serve_widget:
|
|
62
|
+
if config:
|
|
63
|
+
serve_widget.load_config(config)
|
|
64
|
+
else:
|
|
65
|
+
self.notify(
|
|
66
|
+
"Debug: Config is empty, agent_path might not be set",
|
|
67
|
+
severity="warning",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for pane_id in [
|
|
71
|
+
"nav-identity",
|
|
72
|
+
"nav-observability",
|
|
73
|
+
"nav-guardrails",
|
|
74
|
+
"nav-mcps",
|
|
75
|
+
"nav-serve",
|
|
76
|
+
]:
|
|
77
|
+
pane = self.query_one(f"#{pane_id}")
|
|
78
|
+
if pane_id == f"nav-{new_section}":
|
|
79
|
+
pane.add_class("nav-pane-active")
|
|
80
|
+
else:
|
|
81
|
+
pane.remove_class("nav-pane-active")
|
|
82
|
+
|
|
83
|
+
def compose(self) -> ComposeResult:
|
|
84
|
+
app_container = Container(classes="app-container")
|
|
85
|
+
app_container.border_title = "Creating a New Agent"
|
|
86
|
+
|
|
87
|
+
with app_container, Horizontal(classes="main-layout"):
|
|
88
|
+
nav_container = Vertical(classes="nav-container")
|
|
89
|
+
nav_container.border_title = "Sections"
|
|
90
|
+
|
|
91
|
+
with nav_container:
|
|
92
|
+
nav_identity = Vertical(
|
|
93
|
+
Label(
|
|
94
|
+
"Configure agent\nname, framework,\nand port",
|
|
95
|
+
id="nav-identity-label",
|
|
96
|
+
),
|
|
97
|
+
classes="nav-pane nav-pane-active",
|
|
98
|
+
id="nav-identity",
|
|
99
|
+
)
|
|
100
|
+
nav_identity.border_title = "Agent Information"
|
|
101
|
+
nav_identity.can_focus = True
|
|
102
|
+
yield nav_identity
|
|
103
|
+
|
|
104
|
+
nav_observability = Vertical(
|
|
105
|
+
Label(
|
|
106
|
+
"Setup monitoring\nand tracing",
|
|
107
|
+
id="nav-observability-label",
|
|
108
|
+
),
|
|
109
|
+
classes="nav-pane",
|
|
110
|
+
id="nav-observability",
|
|
111
|
+
)
|
|
112
|
+
nav_observability.border_title = "Observability"
|
|
113
|
+
nav_observability.can_focus = True
|
|
114
|
+
yield nav_observability
|
|
115
|
+
|
|
116
|
+
nav_guardrails = Vertical(
|
|
117
|
+
Label("Define rules and\nvalidation"),
|
|
118
|
+
classes="nav-pane",
|
|
119
|
+
id="nav-guardrails",
|
|
120
|
+
)
|
|
121
|
+
nav_guardrails.border_title = "Guardrails"
|
|
122
|
+
nav_guardrails.can_focus = True
|
|
123
|
+
yield nav_guardrails
|
|
124
|
+
|
|
125
|
+
nav_mcps = Vertical(
|
|
126
|
+
Label("Add tools and\nresources"),
|
|
127
|
+
classes="nav-pane",
|
|
128
|
+
id="nav-mcps",
|
|
129
|
+
)
|
|
130
|
+
nav_mcps.border_title = "MCPs"
|
|
131
|
+
nav_mcps.can_focus = True
|
|
132
|
+
yield nav_mcps
|
|
133
|
+
|
|
134
|
+
nav_serve = Vertical(
|
|
135
|
+
Label("Review and start\nagent"),
|
|
136
|
+
classes="nav-pane",
|
|
137
|
+
id="nav-serve",
|
|
138
|
+
)
|
|
139
|
+
nav_serve.border_title = "Validate & Run"
|
|
140
|
+
nav_serve.can_focus = True
|
|
141
|
+
yield nav_serve
|
|
142
|
+
|
|
143
|
+
with Horizontal(classes="action-buttons"):
|
|
144
|
+
yield Button("Back", id="back_button", classes="action-btn")
|
|
145
|
+
yield Button("Next", id="next_button", classes="action-btn")
|
|
146
|
+
|
|
147
|
+
with Vertical(classes="content-area"):
|
|
148
|
+
identity = IdentityWidget(id="widget-identity", classes="section")
|
|
149
|
+
|
|
150
|
+
observability = ObservabilityWidget(
|
|
151
|
+
id="widget-observability", classes="section"
|
|
152
|
+
)
|
|
153
|
+
observability.border_title = "Observability"
|
|
154
|
+
|
|
155
|
+
guardrails = GuardrailsWidget(id="widget-guardrails", classes="section")
|
|
156
|
+
guardrails.border_title = "Guardrails"
|
|
157
|
+
|
|
158
|
+
mcps = MCPsWidget(id="widget-mcps", classes="section")
|
|
159
|
+
mcps.border_title = "MCPs"
|
|
160
|
+
|
|
161
|
+
serve = ServeWidget(id="widget-serve", classes="section")
|
|
162
|
+
serve.border_title = "Validate & Run"
|
|
163
|
+
|
|
164
|
+
self.widgets_map = {
|
|
165
|
+
"identity": identity,
|
|
166
|
+
"observability": observability,
|
|
167
|
+
"guardrails": guardrails,
|
|
168
|
+
"mcps": mcps,
|
|
169
|
+
"serve": serve,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
observability.display = False
|
|
173
|
+
guardrails.display = False
|
|
174
|
+
mcps.display = False
|
|
175
|
+
serve.display = False
|
|
176
|
+
|
|
177
|
+
yield identity
|
|
178
|
+
yield observability
|
|
179
|
+
yield guardrails
|
|
180
|
+
yield mcps
|
|
181
|
+
yield serve
|
|
182
|
+
|
|
183
|
+
footer = Static("💡 Press Next to save section", classes="custom-footer")
|
|
184
|
+
yield footer
|
|
185
|
+
|
|
186
|
+
def on_mount(self) -> None:
|
|
187
|
+
nav_pane = self.query_one("#nav-identity")
|
|
188
|
+
nav_pane.focus()
|
|
189
|
+
|
|
190
|
+
def action_toggle_focus_area(self) -> None:
|
|
191
|
+
focused = self.focused
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
focused
|
|
195
|
+
and hasattr(focused, "id")
|
|
196
|
+
and focused.id
|
|
197
|
+
and focused.id.startswith("nav-")
|
|
198
|
+
):
|
|
199
|
+
self.focus_on_nav = False
|
|
200
|
+
active_widget = self.widgets_map.get(self.active_section)
|
|
201
|
+
if active_widget:
|
|
202
|
+
try:
|
|
203
|
+
focusable = active_widget.query(
|
|
204
|
+
"Input, OptionList, DirectoryTree, Button"
|
|
205
|
+
).first()
|
|
206
|
+
if focusable:
|
|
207
|
+
focusable.focus()
|
|
208
|
+
else:
|
|
209
|
+
active_widget.focus()
|
|
210
|
+
except:
|
|
211
|
+
active_widget.focus()
|
|
212
|
+
else:
|
|
213
|
+
self.focus_next()
|
|
214
|
+
|
|
215
|
+
def on_focus(self, event) -> None:
|
|
216
|
+
focused_widget = event.widget
|
|
217
|
+
nav_container = self.query_one(".nav-container")
|
|
218
|
+
|
|
219
|
+
if focused_widget.id and focused_widget.id.startswith("nav-"):
|
|
220
|
+
self.focus_on_nav = True
|
|
221
|
+
nav_container.add_class("nav-container-active")
|
|
222
|
+
else:
|
|
223
|
+
nav_container.remove_class("nav-container-active")
|
|
224
|
+
if focused_widget.id in [
|
|
225
|
+
"name_input",
|
|
226
|
+
"framework_select",
|
|
227
|
+
"port_input",
|
|
228
|
+
"file_tree",
|
|
229
|
+
"variable_list",
|
|
230
|
+
]:
|
|
231
|
+
self.focus_on_nav = False
|
|
232
|
+
|
|
233
|
+
def action_nav_up(self) -> None:
|
|
234
|
+
focused = self.focused
|
|
235
|
+
if (
|
|
236
|
+
focused
|
|
237
|
+
and hasattr(focused, "id")
|
|
238
|
+
and focused.id
|
|
239
|
+
and focused.id.startswith("nav-")
|
|
240
|
+
):
|
|
241
|
+
self.current_nav_index = (self.current_nav_index - 1) % len(self.nav_panes)
|
|
242
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
243
|
+
nav_pane.focus()
|
|
244
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
245
|
+
self.active_section = section
|
|
246
|
+
|
|
247
|
+
def action_nav_down(self) -> None:
|
|
248
|
+
focused = self.focused
|
|
249
|
+
if (
|
|
250
|
+
focused
|
|
251
|
+
and hasattr(focused, "id")
|
|
252
|
+
and focused.id
|
|
253
|
+
and focused.id.startswith("nav-")
|
|
254
|
+
):
|
|
255
|
+
self.current_nav_index = (self.current_nav_index + 1) % len(self.nav_panes)
|
|
256
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
257
|
+
nav_pane.focus()
|
|
258
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
259
|
+
self.active_section = section
|
|
260
|
+
|
|
261
|
+
def on_click(self, event) -> None:
|
|
262
|
+
target = event.widget
|
|
263
|
+
for node in [target] + list(target.ancestors):
|
|
264
|
+
if node.id and node.id.startswith("nav-"):
|
|
265
|
+
section = node.id.replace("nav-", "")
|
|
266
|
+
self.active_section = section
|
|
267
|
+
self.focus_on_nav = True
|
|
268
|
+
|
|
269
|
+
nav_id = f"nav-{section}"
|
|
270
|
+
if nav_id in self.nav_panes:
|
|
271
|
+
self.current_nav_index = self.nav_panes.index(nav_id)
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
275
|
+
if event.button.id == "back_button":
|
|
276
|
+
current_index = self.current_nav_index
|
|
277
|
+
if current_index > 0:
|
|
278
|
+
self.current_nav_index = current_index - 1
|
|
279
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
280
|
+
nav_pane.focus()
|
|
281
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
282
|
+
self.active_section = section
|
|
283
|
+
elif event.button.id == "next_button":
|
|
284
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
285
|
+
widget = self.widgets_map.get(section)
|
|
286
|
+
|
|
287
|
+
if not widget:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
if section == "identity":
|
|
291
|
+
data = widget.get_data()
|
|
292
|
+
if data is None:
|
|
293
|
+
self.notify("Please complete all required fields", severity="error")
|
|
294
|
+
return
|
|
295
|
+
try:
|
|
296
|
+
_ = TUIAgentConfig.model_validate(data)
|
|
297
|
+
agent_name = data.get("name")
|
|
298
|
+
success, msg = self.config_manager.save_partial(
|
|
299
|
+
"identity", data, agent_name=agent_name
|
|
300
|
+
)
|
|
301
|
+
if not success:
|
|
302
|
+
error_msg = str(msg).replace("[", "").replace("]", "")
|
|
303
|
+
self.notify(
|
|
304
|
+
f"Save failed: {error_msg[:200]}",
|
|
305
|
+
severity="error",
|
|
306
|
+
)
|
|
307
|
+
return
|
|
308
|
+
self.validated_sections.add("identity")
|
|
309
|
+
self._update_nav_checkmark("identity")
|
|
310
|
+
except ValidationError as e:
|
|
311
|
+
self.notify(
|
|
312
|
+
f"Validation error: {str(e)}",
|
|
313
|
+
severity="error",
|
|
314
|
+
)
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
elif section == "observability":
|
|
318
|
+
data = widget.get_data()
|
|
319
|
+
if data is None:
|
|
320
|
+
self.validated_sections.add("observability")
|
|
321
|
+
self._update_nav_checkmark("observability")
|
|
322
|
+
elif isinstance(data, ObservabilityConfig):
|
|
323
|
+
success, _ = self.config_manager.save_partial("observability", data)
|
|
324
|
+
if not success:
|
|
325
|
+
self.notify(
|
|
326
|
+
"Observability configuration is invalid", severity="error"
|
|
327
|
+
)
|
|
328
|
+
return
|
|
329
|
+
self.validated_sections.add("observability")
|
|
330
|
+
self._update_nav_checkmark("observability")
|
|
331
|
+
else:
|
|
332
|
+
self.notify(
|
|
333
|
+
"Observability configuration is invalid", severity="error"
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
elif section == "guardrails":
|
|
338
|
+
data = widget.get_data()
|
|
339
|
+
if data and isinstance(data, GuardrailsV2):
|
|
340
|
+
success, msg = self.config_manager.save_partial("guardrails", data)
|
|
341
|
+
if not success:
|
|
342
|
+
self.notify(
|
|
343
|
+
"Guardrails configuration is invalid", severity="error"
|
|
344
|
+
)
|
|
345
|
+
return
|
|
346
|
+
self.validated_sections.add("guardrails")
|
|
347
|
+
self._update_nav_checkmark("guardrails")
|
|
348
|
+
|
|
349
|
+
elif section == "mcps":
|
|
350
|
+
from idun_platform_cli.tui.validators.mcps import validate_mcp_servers
|
|
351
|
+
|
|
352
|
+
data = widget.get_data()
|
|
353
|
+
if data is None:
|
|
354
|
+
self.notify("Invalid MCP configuration", severity="error")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
validated_servers, msg = validate_mcp_servers(data)
|
|
358
|
+
if validated_servers is None:
|
|
359
|
+
error_msg = str(msg).replace("[", "").replace("]", "")
|
|
360
|
+
self.notify(f"MCP validation failed: {error_msg[:200]}", severity="error")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
success, save_msg = self.config_manager.save_partial(
|
|
364
|
+
"mcp_servers", validated_servers
|
|
365
|
+
)
|
|
366
|
+
if not success:
|
|
367
|
+
self.notify(f"Failed to save: {save_msg}", severity="error")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
self.validated_sections.add("mcps")
|
|
371
|
+
self._update_nav_checkmark("mcps")
|
|
372
|
+
|
|
373
|
+
current_index = self.current_nav_index
|
|
374
|
+
if current_index < len(self.nav_panes) - 1:
|
|
375
|
+
self.current_nav_index = current_index + 1
|
|
376
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
377
|
+
nav_pane.focus()
|
|
378
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
379
|
+
self.active_section = section
|
|
380
|
+
|
|
381
|
+
elif event.button.id == "validate_run_button":
|
|
382
|
+
if self.server_running and self.server_process:
|
|
383
|
+
self.server_process.terminate()
|
|
384
|
+
self.server_process = None
|
|
385
|
+
self.server_running = False
|
|
386
|
+
|
|
387
|
+
button = self.query_one("#validate_run_button", Button)
|
|
388
|
+
button.label = "Validate & Run"
|
|
389
|
+
button.remove_class("kill-mode")
|
|
390
|
+
|
|
391
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
392
|
+
rich_log.write("\n[red]Server stopped[/red]")
|
|
393
|
+
|
|
394
|
+
self.notify("Server stopped", severity="information")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
serve_widget = self.widgets_map.get("serve")
|
|
398
|
+
if not serve_widget:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
agent_name = serve_widget.get_agent_name()
|
|
402
|
+
if not agent_name:
|
|
403
|
+
self.notify("No agent configuration found", severity="error")
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
sanitized_name = self.config_manager._sanitize_agent_name(agent_name)
|
|
407
|
+
config_path = Path.home() / ".idun" / f"{sanitized_name}.yaml"
|
|
408
|
+
|
|
409
|
+
if not config_path.exists():
|
|
410
|
+
self.notify("Configuration file not found", severity="error")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
logs_container = self.query_one("#logs_container")
|
|
414
|
+
logs_container.display = True
|
|
415
|
+
|
|
416
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
417
|
+
rich_log.clear()
|
|
418
|
+
rich_log.write(f"Starting server for agent: {agent_name}")
|
|
419
|
+
rich_log.write(f"Config: {config_path}")
|
|
420
|
+
|
|
421
|
+
import fcntl
|
|
422
|
+
import os
|
|
423
|
+
import subprocess
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
process = subprocess.Popen(
|
|
427
|
+
["idun", "agent", "serve", "--source=file", f"--path={config_path}"],
|
|
428
|
+
stdout=subprocess.PIPE,
|
|
429
|
+
stderr=subprocess.STDOUT,
|
|
430
|
+
text=True,
|
|
431
|
+
bufsize=1,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
fd = process.stdout.fileno()
|
|
435
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
436
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
437
|
+
|
|
438
|
+
self.server_process = process
|
|
439
|
+
self.server_running = True
|
|
440
|
+
|
|
441
|
+
button = self.query_one("#validate_run_button", Button)
|
|
442
|
+
button.label = "Kill Server"
|
|
443
|
+
button.add_class("kill-mode")
|
|
444
|
+
|
|
445
|
+
self.run_worker(self._stream_logs(process), exclusive=True)
|
|
446
|
+
|
|
447
|
+
except Exception as e:
|
|
448
|
+
self.notify(f"Failed to start server: {e}", severity="error")
|
|
449
|
+
|
|
450
|
+
async def _stream_logs(self, process) -> None:
|
|
451
|
+
import asyncio
|
|
452
|
+
|
|
453
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
454
|
+
|
|
455
|
+
while self.server_running:
|
|
456
|
+
try:
|
|
457
|
+
line = process.stdout.readline()
|
|
458
|
+
if line:
|
|
459
|
+
rich_log.write(line.strip())
|
|
460
|
+
except:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
if process.poll() is not None:
|
|
464
|
+
remaining = process.stdout.read()
|
|
465
|
+
if remaining:
|
|
466
|
+
for line in remaining.split("\n"):
|
|
467
|
+
if line.strip():
|
|
468
|
+
rich_log.write(line.strip())
|
|
469
|
+
|
|
470
|
+
self.server_running = False
|
|
471
|
+
self.server_process = None
|
|
472
|
+
button = self.query_one("#validate_run_button", Button)
|
|
473
|
+
button.label = "Validate & Run"
|
|
474
|
+
button.remove_class("kill-mode")
|
|
475
|
+
rich_log.write("\n[yellow]Server exited[/yellow]")
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
await asyncio.sleep(0.1)
|
|
479
|
+
|
|
480
|
+
def _update_nav_checkmark(self, section: str) -> None:
|
|
481
|
+
nav_pane = self.query_one(f"#nav-{section}")
|
|
482
|
+
nav_pane.add_class("nav-pane-validated")
|
|
File without changes
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
6
|
+
from idun_agent_schema.engine.observability_v2 import ObservabilityConfig
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from idun_platform_cli.tui.schemas.create_agent import (
|
|
10
|
+
AGENT_SOURCE_KEY_MAPPING,
|
|
11
|
+
TUIAgentConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigManager:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.idun_dir = Path.home() / ".idun"
|
|
18
|
+
self.agent_path = None
|
|
19
|
+
try:
|
|
20
|
+
self.idun_dir.mkdir(exist_ok=True)
|
|
21
|
+
|
|
22
|
+
except OSError as e:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Error while preparing `.idun` config file: {e}\nNote: This file is used to store config and env data"
|
|
25
|
+
) from e
|
|
26
|
+
|
|
27
|
+
def _sanitize_agent_name(self, agent_name: str) -> str:
|
|
28
|
+
return agent_name.lstrip().replace("-", "_").replace(" ", "_")
|
|
29
|
+
|
|
30
|
+
def _validate_data(
|
|
31
|
+
self, config: dict[str, Any]
|
|
32
|
+
) -> tuple[TUIAgentConfig | None, str]:
|
|
33
|
+
try:
|
|
34
|
+
return TUIAgentConfig.model_validate(config), "valid"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return None, f"Error: cannot validate config: {e}"
|
|
37
|
+
|
|
38
|
+
def save_config(self, config: dict) -> tuple[bool, str]:
|
|
39
|
+
try:
|
|
40
|
+
raw_agent_name: str = config["name"]
|
|
41
|
+
except KeyError as e:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Agent name is not defined! Make sure you specify a name for your agent!"
|
|
44
|
+
) from e
|
|
45
|
+
sanitized_agent_name = self._sanitize_agent_name(raw_agent_name)
|
|
46
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(".yaml")
|
|
47
|
+
|
|
48
|
+
with self.agent_path.open("w") as f:
|
|
49
|
+
serialized, msg = self._validate_data(config)
|
|
50
|
+
if serialized is None:
|
|
51
|
+
return False, msg
|
|
52
|
+
with self.agent_path.open("w") as f:
|
|
53
|
+
yaml.dump(serialized.to_engine_config(), f, default_flow_style=False)
|
|
54
|
+
return True, "Valid"
|
|
55
|
+
|
|
56
|
+
def load_draft(self) -> dict | None:
|
|
57
|
+
if self.agent_path is None or not self.agent_path.exists():
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"No agent config file found. Make sure you have saved agent configs."
|
|
60
|
+
)
|
|
61
|
+
with self.agent_path.open("r") as f:
|
|
62
|
+
return yaml.safe_load(f) or {}
|
|
63
|
+
|
|
64
|
+
def save_partial(
|
|
65
|
+
self, section: str, data: dict | Any, agent_name: str = None
|
|
66
|
+
) -> tuple[bool, str]:
|
|
67
|
+
try:
|
|
68
|
+
from idun_agent_engine.core.engine_config import EngineConfig
|
|
69
|
+
|
|
70
|
+
if agent_name:
|
|
71
|
+
sanitized_agent_name = self._sanitize_agent_name(agent_name)
|
|
72
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(
|
|
73
|
+
".yaml"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if self.agent_path is None:
|
|
77
|
+
return False, "Agent name not set. Save identity section first."
|
|
78
|
+
|
|
79
|
+
existing_config = {}
|
|
80
|
+
if self.agent_path.exists():
|
|
81
|
+
with self.agent_path.open("r") as f:
|
|
82
|
+
existing_config = yaml.safe_load(f) or {}
|
|
83
|
+
|
|
84
|
+
if section == "identity":
|
|
85
|
+
from idun_platform_cli.tui.schemas.create_agent import TUIAgentConfig
|
|
86
|
+
|
|
87
|
+
tui_config = TUIAgentConfig.model_validate(data)
|
|
88
|
+
engine_config_dict = tui_config.to_engine_config()
|
|
89
|
+
existing_config["server"] = engine_config_dict["server"]
|
|
90
|
+
existing_config["agent"] = engine_config_dict["agent"]
|
|
91
|
+
elif section == "observability":
|
|
92
|
+
if isinstance(data, ObservabilityConfig):
|
|
93
|
+
obs_dict = {
|
|
94
|
+
"provider": data.provider.value,
|
|
95
|
+
"enabled": data.enabled,
|
|
96
|
+
"config": data.config.model_dump(by_alias=False),
|
|
97
|
+
}
|
|
98
|
+
existing_config["observability"] = [obs_dict]
|
|
99
|
+
else:
|
|
100
|
+
existing_config["observability"] = [data]
|
|
101
|
+
elif section == "guardrails":
|
|
102
|
+
if isinstance(data, GuardrailsV2):
|
|
103
|
+
guardrails_dict = {
|
|
104
|
+
"input": [
|
|
105
|
+
g.model_dump(by_alias=False, mode="json")
|
|
106
|
+
for g in data.input
|
|
107
|
+
],
|
|
108
|
+
"output": [
|
|
109
|
+
g.model_dump(by_alias=False, mode="json")
|
|
110
|
+
for g in data.output
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
existing_config["guardrails"] = guardrails_dict
|
|
114
|
+
else:
|
|
115
|
+
existing_config["guardrails"] = data
|
|
116
|
+
elif section == "mcp_servers":
|
|
117
|
+
if isinstance(data, list):
|
|
118
|
+
mcp_servers_list = []
|
|
119
|
+
for server in data:
|
|
120
|
+
if hasattr(server, "model_dump"):
|
|
121
|
+
mcp_servers_list.append(
|
|
122
|
+
server.model_dump(
|
|
123
|
+
by_alias=True, mode="json", exclude_none=True
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
mcp_servers_list.append(server)
|
|
128
|
+
existing_config["mcp_servers"] = mcp_servers_list
|
|
129
|
+
else:
|
|
130
|
+
existing_config["mcp_servers"] = data
|
|
131
|
+
else:
|
|
132
|
+
existing_config[section] = data
|
|
133
|
+
|
|
134
|
+
EngineConfig.model_validate(existing_config)
|
|
135
|
+
|
|
136
|
+
with self.agent_path.open("w") as f:
|
|
137
|
+
yaml.dump(existing_config, f, default_flow_style=False)
|
|
138
|
+
|
|
139
|
+
return True, "Saved successfully"
|
|
140
|
+
|
|
141
|
+
except ValidationError as e:
|
|
142
|
+
return False, f"Validation error: {e}"
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return False, f"Error saving config: {e}"
|
|
145
|
+
|
|
146
|
+
def load_config(self, agent_name: str = None) -> dict:
|
|
147
|
+
try:
|
|
148
|
+
if agent_name:
|
|
149
|
+
sanitized_agent_name = self._sanitize_agent_name(agent_name)
|
|
150
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(
|
|
151
|
+
".yaml"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if self.agent_path is None or not self.agent_path.exists():
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
with self.agent_path.open("r") as f:
|
|
158
|
+
return yaml.safe_load(f) or {}
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return {}
|
|
File without changes
|