idun-agent-engine 0.3.8__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.
Files changed (39) hide show
  1. idun_agent_engine/_version.py +1 -1
  2. idun_agent_engine/agent/langgraph/langgraph.py +1 -1
  3. idun_agent_engine/core/app_factory.py +1 -1
  4. idun_agent_engine/core/config_builder.py +5 -6
  5. idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
  6. idun_agent_engine/mcp/__init__.py +18 -2
  7. idun_agent_engine/mcp/helpers.py +95 -45
  8. idun_agent_engine/mcp/registry.py +7 -1
  9. idun_agent_engine/server/lifespan.py +22 -0
  10. idun_agent_engine/telemetry/__init__.py +19 -0
  11. idun_agent_engine/telemetry/config.py +29 -0
  12. idun_agent_engine/telemetry/telemetry.py +248 -0
  13. {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/METADATA +12 -8
  14. {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/RECORD +39 -14
  15. idun_platform_cli/groups/init.py +23 -0
  16. idun_platform_cli/main.py +3 -0
  17. idun_platform_cli/tui/__init__.py +0 -0
  18. idun_platform_cli/tui/css/__init__.py +0 -0
  19. idun_platform_cli/tui/css/create_agent.py +789 -0
  20. idun_platform_cli/tui/css/main.py +92 -0
  21. idun_platform_cli/tui/main.py +87 -0
  22. idun_platform_cli/tui/schemas/__init__.py +0 -0
  23. idun_platform_cli/tui/schemas/create_agent.py +60 -0
  24. idun_platform_cli/tui/screens/__init__.py +0 -0
  25. idun_platform_cli/tui/screens/create_agent.py +482 -0
  26. idun_platform_cli/tui/utils/__init__.py +0 -0
  27. idun_platform_cli/tui/utils/config.py +161 -0
  28. idun_platform_cli/tui/validators/__init__.py +0 -0
  29. idun_platform_cli/tui/validators/guardrails.py +76 -0
  30. idun_platform_cli/tui/validators/mcps.py +84 -0
  31. idun_platform_cli/tui/validators/observability.py +65 -0
  32. idun_platform_cli/tui/widgets/__init__.py +15 -0
  33. idun_platform_cli/tui/widgets/guardrails_widget.py +348 -0
  34. idun_platform_cli/tui/widgets/identity_widget.py +234 -0
  35. idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
  36. idun_platform_cli/tui/widgets/observability_widget.py +384 -0
  37. idun_platform_cli/tui/widgets/serve_widget.py +78 -0
  38. {idun_agent_engine-0.3.8.dist-info → idun_agent_engine-0.4.0.dist-info}/WHEEL +0 -0
  39. {idun_agent_engine-0.3.8.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