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,622 @@
|
|
|
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
|
+
ChatWidget,
|
|
17
|
+
GuardrailsWidget,
|
|
18
|
+
IdentityWidget,
|
|
19
|
+
MCPsWidget,
|
|
20
|
+
MemoryWidget,
|
|
21
|
+
ObservabilityWidget,
|
|
22
|
+
ServeWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateAgentScreen(Screen):
|
|
27
|
+
CSS = CREATE_AGENT_CSS
|
|
28
|
+
BINDINGS = [
|
|
29
|
+
("tab", "toggle_focus_area", "Switch Area / Next Field"),
|
|
30
|
+
("up", "nav_up", "Navigate Panes"),
|
|
31
|
+
("down", "nav_down", ""),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
active_section = reactive("identity")
|
|
35
|
+
nav_panes = [
|
|
36
|
+
"nav-identity",
|
|
37
|
+
"nav-memory",
|
|
38
|
+
"nav-observability",
|
|
39
|
+
"nav-guardrails",
|
|
40
|
+
"nav-mcps",
|
|
41
|
+
"nav-serve",
|
|
42
|
+
"nav-chat",
|
|
43
|
+
]
|
|
44
|
+
current_nav_index = 0
|
|
45
|
+
focus_on_nav = True # Track if focus is on nav or content
|
|
46
|
+
|
|
47
|
+
def __init__(self, *args, **kwargs):
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
self.widgets_map = {}
|
|
50
|
+
self.config_manager = ConfigManager()
|
|
51
|
+
self.validated_sections = set()
|
|
52
|
+
self.server_process = None
|
|
53
|
+
self.server_running = False
|
|
54
|
+
|
|
55
|
+
def on_unmount(self) -> None:
|
|
56
|
+
if self.server_process:
|
|
57
|
+
import os
|
|
58
|
+
import signal
|
|
59
|
+
import subprocess
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
pgid = os.getpgid(self.server_process.pid)
|
|
63
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
64
|
+
except (ProcessLookupError, OSError, AttributeError, PermissionError):
|
|
65
|
+
try:
|
|
66
|
+
self.server_process.kill()
|
|
67
|
+
except:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
config = self.config_manager.load_config()
|
|
72
|
+
if config:
|
|
73
|
+
port = config.get("server", {}).get("api", {}).get("port", 8008)
|
|
74
|
+
pids = subprocess.check_output(["lsof", "-ti", f":{port}"], text=True).strip().split("\n")
|
|
75
|
+
for pid in pids:
|
|
76
|
+
if pid:
|
|
77
|
+
try:
|
|
78
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
79
|
+
except:
|
|
80
|
+
pass
|
|
81
|
+
except:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def watch_active_section(self, new_section: str) -> None:
|
|
85
|
+
for section_id, widget in self.widgets_map.items():
|
|
86
|
+
if section_id == new_section:
|
|
87
|
+
widget.display = True
|
|
88
|
+
else:
|
|
89
|
+
widget.display = False
|
|
90
|
+
|
|
91
|
+
if new_section == "serve":
|
|
92
|
+
config = self.config_manager.load_config()
|
|
93
|
+
serve_widget = self.widgets_map.get("serve")
|
|
94
|
+
if serve_widget:
|
|
95
|
+
if config:
|
|
96
|
+
serve_widget.load_config(config)
|
|
97
|
+
else:
|
|
98
|
+
self.notify(
|
|
99
|
+
"Debug: Config is empty, agent_path might not be set",
|
|
100
|
+
severity="warning",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if new_section == "chat":
|
|
104
|
+
config = self.config_manager.load_config()
|
|
105
|
+
chat_widget = self.widgets_map.get("chat")
|
|
106
|
+
if chat_widget and config:
|
|
107
|
+
chat_widget.load_config(config)
|
|
108
|
+
|
|
109
|
+
for pane_id in [
|
|
110
|
+
"nav-identity",
|
|
111
|
+
"nav-memory",
|
|
112
|
+
"nav-observability",
|
|
113
|
+
"nav-guardrails",
|
|
114
|
+
"nav-mcps",
|
|
115
|
+
"nav-serve",
|
|
116
|
+
"nav-chat",
|
|
117
|
+
]:
|
|
118
|
+
pane = self.query_one(f"#{pane_id}")
|
|
119
|
+
if pane_id == f"nav-{new_section}":
|
|
120
|
+
pane.add_class("nav-pane-active")
|
|
121
|
+
else:
|
|
122
|
+
pane.remove_class("nav-pane-active")
|
|
123
|
+
|
|
124
|
+
def compose(self) -> ComposeResult:
|
|
125
|
+
app_container = Container(classes="app-container")
|
|
126
|
+
app_container.border_title = "Creating a New Agent"
|
|
127
|
+
|
|
128
|
+
with app_container, Horizontal(classes="main-layout"):
|
|
129
|
+
nav_container = Vertical(classes="nav-container")
|
|
130
|
+
nav_container.border_title = "Sections"
|
|
131
|
+
|
|
132
|
+
with nav_container:
|
|
133
|
+
nav_identity = Vertical(
|
|
134
|
+
Label(
|
|
135
|
+
"Configure agent\nname, framework,\nand port",
|
|
136
|
+
id="nav-identity-label",
|
|
137
|
+
),
|
|
138
|
+
classes="nav-pane nav-pane-active",
|
|
139
|
+
id="nav-identity",
|
|
140
|
+
)
|
|
141
|
+
nav_identity.border_title = "Agent Information"
|
|
142
|
+
nav_identity.can_focus = True
|
|
143
|
+
yield nav_identity
|
|
144
|
+
|
|
145
|
+
nav_memory = Vertical(
|
|
146
|
+
Label(
|
|
147
|
+
"Configure agent\ncheckpointing",
|
|
148
|
+
id="nav-memory-label",
|
|
149
|
+
),
|
|
150
|
+
classes="nav-pane",
|
|
151
|
+
id="nav-memory",
|
|
152
|
+
)
|
|
153
|
+
nav_memory.border_title = "Memory"
|
|
154
|
+
nav_memory.can_focus = True
|
|
155
|
+
yield nav_memory
|
|
156
|
+
|
|
157
|
+
nav_observability = Vertical(
|
|
158
|
+
Label(
|
|
159
|
+
"Setup monitoring\nand tracing",
|
|
160
|
+
id="nav-observability-label",
|
|
161
|
+
),
|
|
162
|
+
classes="nav-pane",
|
|
163
|
+
id="nav-observability",
|
|
164
|
+
)
|
|
165
|
+
nav_observability.border_title = "Observability"
|
|
166
|
+
nav_observability.can_focus = True
|
|
167
|
+
yield nav_observability
|
|
168
|
+
|
|
169
|
+
nav_guardrails = Vertical(
|
|
170
|
+
Label("Define rules and\nvalidation"),
|
|
171
|
+
classes="nav-pane",
|
|
172
|
+
id="nav-guardrails",
|
|
173
|
+
)
|
|
174
|
+
nav_guardrails.border_title = "Guardrails"
|
|
175
|
+
nav_guardrails.can_focus = True
|
|
176
|
+
yield nav_guardrails
|
|
177
|
+
|
|
178
|
+
nav_mcps = Vertical(
|
|
179
|
+
Label("Add tools and\nresources"),
|
|
180
|
+
classes="nav-pane",
|
|
181
|
+
id="nav-mcps",
|
|
182
|
+
)
|
|
183
|
+
nav_mcps.border_title = "MCPs"
|
|
184
|
+
nav_mcps.can_focus = True
|
|
185
|
+
yield nav_mcps
|
|
186
|
+
|
|
187
|
+
nav_serve = Vertical(
|
|
188
|
+
Label("Review and start\nagent"),
|
|
189
|
+
classes="nav-pane",
|
|
190
|
+
id="nav-serve",
|
|
191
|
+
)
|
|
192
|
+
nav_serve.border_title = "Validate & Run"
|
|
193
|
+
nav_serve.can_focus = True
|
|
194
|
+
yield nav_serve
|
|
195
|
+
|
|
196
|
+
nav_chat = Vertical(
|
|
197
|
+
Label("Chat with your\nrunning agent"),
|
|
198
|
+
classes="nav-pane",
|
|
199
|
+
id="nav-chat",
|
|
200
|
+
)
|
|
201
|
+
nav_chat.border_title = "Chat"
|
|
202
|
+
nav_chat.can_focus = True
|
|
203
|
+
yield nav_chat
|
|
204
|
+
|
|
205
|
+
with Horizontal(classes="action-buttons"):
|
|
206
|
+
yield Button("Back", id="back_button", classes="action-btn")
|
|
207
|
+
yield Button("Next", id="next_button", classes="action-btn")
|
|
208
|
+
|
|
209
|
+
with Vertical(classes="content-area"):
|
|
210
|
+
identity = IdentityWidget(id="widget-identity", classes="section")
|
|
211
|
+
|
|
212
|
+
memory = MemoryWidget(id="widget-memory", classes="section")
|
|
213
|
+
memory.border_title = "Memory & Checkpointing"
|
|
214
|
+
|
|
215
|
+
observability = ObservabilityWidget(
|
|
216
|
+
id="widget-observability", classes="section"
|
|
217
|
+
)
|
|
218
|
+
observability.border_title = "Observability"
|
|
219
|
+
|
|
220
|
+
guardrails = GuardrailsWidget(id="widget-guardrails", classes="section")
|
|
221
|
+
guardrails.border_title = "Guardrails"
|
|
222
|
+
|
|
223
|
+
mcps = MCPsWidget(id="widget-mcps", classes="section")
|
|
224
|
+
mcps.border_title = "MCPs"
|
|
225
|
+
|
|
226
|
+
serve = ServeWidget(id="widget-serve", classes="section")
|
|
227
|
+
serve.border_title = "Validate & Run"
|
|
228
|
+
|
|
229
|
+
chat = ChatWidget(id="widget-chat", classes="section")
|
|
230
|
+
chat.border_title = "Chat"
|
|
231
|
+
|
|
232
|
+
self.widgets_map = {
|
|
233
|
+
"identity": identity,
|
|
234
|
+
"memory": memory,
|
|
235
|
+
"observability": observability,
|
|
236
|
+
"guardrails": guardrails,
|
|
237
|
+
"mcps": mcps,
|
|
238
|
+
"serve": serve,
|
|
239
|
+
"chat": chat,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
memory.display = False
|
|
243
|
+
observability.display = False
|
|
244
|
+
guardrails.display = False
|
|
245
|
+
mcps.display = False
|
|
246
|
+
serve.display = False
|
|
247
|
+
chat.display = False
|
|
248
|
+
|
|
249
|
+
yield identity
|
|
250
|
+
yield memory
|
|
251
|
+
yield observability
|
|
252
|
+
yield guardrails
|
|
253
|
+
yield mcps
|
|
254
|
+
yield serve
|
|
255
|
+
yield chat
|
|
256
|
+
|
|
257
|
+
footer = Static("💡 Press Next to save section | Press Ctrl+Q to exit", classes="custom-footer")
|
|
258
|
+
yield footer
|
|
259
|
+
|
|
260
|
+
def on_mount(self) -> None:
|
|
261
|
+
nav_pane = self.query_one("#nav-identity")
|
|
262
|
+
nav_pane.focus()
|
|
263
|
+
|
|
264
|
+
def action_toggle_focus_area(self) -> None:
|
|
265
|
+
focused = self.focused
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
focused
|
|
269
|
+
and hasattr(focused, "id")
|
|
270
|
+
and focused.id
|
|
271
|
+
and focused.id.startswith("nav-")
|
|
272
|
+
):
|
|
273
|
+
self.focus_on_nav = False
|
|
274
|
+
active_widget = self.widgets_map.get(self.active_section)
|
|
275
|
+
if active_widget:
|
|
276
|
+
try:
|
|
277
|
+
focusable = active_widget.query(
|
|
278
|
+
"Input, OptionList, DirectoryTree, Button, RadioSet"
|
|
279
|
+
).first()
|
|
280
|
+
if focusable:
|
|
281
|
+
focusable.focus()
|
|
282
|
+
else:
|
|
283
|
+
active_widget.focus()
|
|
284
|
+
except:
|
|
285
|
+
active_widget.focus()
|
|
286
|
+
else:
|
|
287
|
+
self.focus_next()
|
|
288
|
+
|
|
289
|
+
def on_focus(self, event) -> None:
|
|
290
|
+
focused_widget = event.widget
|
|
291
|
+
nav_container = self.query_one(".nav-container")
|
|
292
|
+
|
|
293
|
+
if focused_widget.id and focused_widget.id.startswith("nav-"):
|
|
294
|
+
self.focus_on_nav = True
|
|
295
|
+
nav_container.add_class("nav-container-active")
|
|
296
|
+
else:
|
|
297
|
+
nav_container.remove_class("nav-container-active")
|
|
298
|
+
if focused_widget.id in [
|
|
299
|
+
"name_input",
|
|
300
|
+
"framework_select",
|
|
301
|
+
"port_input",
|
|
302
|
+
"file_tree",
|
|
303
|
+
"variable_list",
|
|
304
|
+
]:
|
|
305
|
+
self.focus_on_nav = False
|
|
306
|
+
|
|
307
|
+
def action_nav_up(self) -> None:
|
|
308
|
+
focused = self.focused
|
|
309
|
+
if (
|
|
310
|
+
focused
|
|
311
|
+
and hasattr(focused, "id")
|
|
312
|
+
and focused.id
|
|
313
|
+
and focused.id.startswith("nav-")
|
|
314
|
+
):
|
|
315
|
+
self.current_nav_index = (self.current_nav_index - 1) % len(self.nav_panes)
|
|
316
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
317
|
+
nav_pane.focus()
|
|
318
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
319
|
+
self.active_section = section
|
|
320
|
+
|
|
321
|
+
def action_nav_down(self) -> None:
|
|
322
|
+
focused = self.focused
|
|
323
|
+
if (
|
|
324
|
+
focused
|
|
325
|
+
and hasattr(focused, "id")
|
|
326
|
+
and focused.id
|
|
327
|
+
and focused.id.startswith("nav-")
|
|
328
|
+
):
|
|
329
|
+
self.current_nav_index = (self.current_nav_index + 1) % len(self.nav_panes)
|
|
330
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
331
|
+
nav_pane.focus()
|
|
332
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
333
|
+
self.active_section = section
|
|
334
|
+
|
|
335
|
+
def on_click(self, event) -> None:
|
|
336
|
+
target = event.widget
|
|
337
|
+
for node in [target] + list(target.ancestors):
|
|
338
|
+
if node.id and node.id.startswith("nav-"):
|
|
339
|
+
section = node.id.replace("nav-", "")
|
|
340
|
+
self.active_section = section
|
|
341
|
+
self.focus_on_nav = True
|
|
342
|
+
|
|
343
|
+
nav_id = f"nav-{section}"
|
|
344
|
+
if nav_id in self.nav_panes:
|
|
345
|
+
self.current_nav_index = self.nav_panes.index(nav_id)
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
349
|
+
if event.button.id == "back_button":
|
|
350
|
+
current_index = self.current_nav_index
|
|
351
|
+
if current_index > 0:
|
|
352
|
+
self.current_nav_index = current_index - 1
|
|
353
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
354
|
+
nav_pane.focus()
|
|
355
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
356
|
+
self.active_section = section
|
|
357
|
+
elif event.button.id == "next_button":
|
|
358
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
359
|
+
widget = self.widgets_map.get(section)
|
|
360
|
+
|
|
361
|
+
if not widget:
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
if section == "identity":
|
|
365
|
+
if not widget.validate():
|
|
366
|
+
return
|
|
367
|
+
data = widget.get_data()
|
|
368
|
+
if data is None:
|
|
369
|
+
self.notify("Please complete all required fields", severity="error")
|
|
370
|
+
return
|
|
371
|
+
try:
|
|
372
|
+
_ = TUIAgentConfig.model_validate(data)
|
|
373
|
+
agent_name = data.get("name")
|
|
374
|
+
success, msg = self.config_manager.save_partial(
|
|
375
|
+
"identity", data, agent_name=agent_name
|
|
376
|
+
)
|
|
377
|
+
if not success:
|
|
378
|
+
error_msg = str(msg).replace("[", "").replace("]", "")
|
|
379
|
+
self.notify(
|
|
380
|
+
f"Save failed: {error_msg[:200]}",
|
|
381
|
+
severity="error",
|
|
382
|
+
)
|
|
383
|
+
return
|
|
384
|
+
self.validated_sections.add("identity")
|
|
385
|
+
self._update_nav_checkmark("identity")
|
|
386
|
+
except ValidationError:
|
|
387
|
+
self.notify(
|
|
388
|
+
"Error validating Identity: make sure all fields are correct.",
|
|
389
|
+
severity="error",
|
|
390
|
+
)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
elif section == "memory":
|
|
394
|
+
data = widget.get_data()
|
|
395
|
+
if data is not None:
|
|
396
|
+
success, msg = self.config_manager.save_partial("memory", data)
|
|
397
|
+
if not success:
|
|
398
|
+
self.notify(
|
|
399
|
+
"Memory configuration is invalid", severity="error"
|
|
400
|
+
)
|
|
401
|
+
return
|
|
402
|
+
self.validated_sections.add("memory")
|
|
403
|
+
self._update_nav_checkmark("memory")
|
|
404
|
+
else:
|
|
405
|
+
self.notify("Please configure checkpoint settings", severity="error")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
elif section == "observability":
|
|
409
|
+
data = widget.get_data()
|
|
410
|
+
if data is None:
|
|
411
|
+
self.validated_sections.add("observability")
|
|
412
|
+
self._update_nav_checkmark("observability")
|
|
413
|
+
elif isinstance(data, ObservabilityConfig):
|
|
414
|
+
success, _ = self.config_manager.save_partial("observability", data)
|
|
415
|
+
if not success:
|
|
416
|
+
self.notify(
|
|
417
|
+
"Observability configuration is invalid", severity="error"
|
|
418
|
+
)
|
|
419
|
+
return
|
|
420
|
+
self.validated_sections.add("observability")
|
|
421
|
+
self._update_nav_checkmark("observability")
|
|
422
|
+
else:
|
|
423
|
+
self.notify(
|
|
424
|
+
"Observability configuration is invalid", severity="error"
|
|
425
|
+
)
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
elif section == "guardrails":
|
|
429
|
+
data = widget.get_data()
|
|
430
|
+
if data and isinstance(data, GuardrailsV2):
|
|
431
|
+
success, msg = self.config_manager.save_partial("guardrails", data)
|
|
432
|
+
if not success:
|
|
433
|
+
self.notify(
|
|
434
|
+
"Guardrails configuration is invalid", severity="error"
|
|
435
|
+
)
|
|
436
|
+
return
|
|
437
|
+
self.validated_sections.add("guardrails")
|
|
438
|
+
self._update_nav_checkmark("guardrails")
|
|
439
|
+
|
|
440
|
+
elif section == "mcps":
|
|
441
|
+
from idun_platform_cli.tui.validators.mcps import validate_mcp_servers
|
|
442
|
+
|
|
443
|
+
data = widget.get_data()
|
|
444
|
+
if data is None:
|
|
445
|
+
self.notify("Invalid MCP configuration", severity="error")
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
validated_servers, msg = validate_mcp_servers(data)
|
|
449
|
+
if validated_servers is None:
|
|
450
|
+
self.notify("Error validating MCPs: make sure all fields are correct.", severity="error")
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
success, save_msg = self.config_manager.save_partial(
|
|
454
|
+
"mcp_servers", validated_servers
|
|
455
|
+
)
|
|
456
|
+
if not success:
|
|
457
|
+
self.notify("Error saving MCPs: make sure all fields are correct.", severity="error")
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
self.validated_sections.add("mcps")
|
|
461
|
+
self._update_nav_checkmark("mcps")
|
|
462
|
+
|
|
463
|
+
current_index = self.current_nav_index
|
|
464
|
+
if current_index < len(self.nav_panes) - 1:
|
|
465
|
+
self.current_nav_index = current_index + 1
|
|
466
|
+
nav_pane = self.query_one(f"#{self.nav_panes[self.current_nav_index]}")
|
|
467
|
+
nav_pane.focus()
|
|
468
|
+
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
469
|
+
self.active_section = section
|
|
470
|
+
|
|
471
|
+
elif event.button.id == "save_exit_button":
|
|
472
|
+
if self.server_running and self.server_process:
|
|
473
|
+
self.notify("Kill Server before exiting", severity="warning")
|
|
474
|
+
return
|
|
475
|
+
self.notify("Configuration saved. Exiting...", severity="information")
|
|
476
|
+
self.app.exit()
|
|
477
|
+
|
|
478
|
+
elif event.button.id == "save_run_button":
|
|
479
|
+
if self.server_running and self.server_process:
|
|
480
|
+
import os
|
|
481
|
+
import signal
|
|
482
|
+
import subprocess
|
|
483
|
+
|
|
484
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
485
|
+
rich_log.write("\n[yellow]Stopping server...[/yellow]")
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
pgid = os.getpgid(self.server_process.pid)
|
|
489
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
490
|
+
try:
|
|
491
|
+
self.server_process.wait(timeout=3)
|
|
492
|
+
except subprocess.TimeoutExpired:
|
|
493
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
494
|
+
self.server_process.wait(timeout=1)
|
|
495
|
+
except (ProcessLookupError, OSError, PermissionError):
|
|
496
|
+
try:
|
|
497
|
+
self.server_process.terminate()
|
|
498
|
+
self.server_process.wait(timeout=2)
|
|
499
|
+
except subprocess.TimeoutExpired:
|
|
500
|
+
self.server_process.kill()
|
|
501
|
+
self.server_process.wait(timeout=1)
|
|
502
|
+
except:
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
config = self.config_manager.load_config()
|
|
506
|
+
if config:
|
|
507
|
+
port = config.get("server", {}).get("api", {}).get("port", 8008)
|
|
508
|
+
try:
|
|
509
|
+
subprocess.run(
|
|
510
|
+
["lsof", "-ti", f":{port}"],
|
|
511
|
+
capture_output=True,
|
|
512
|
+
text=True,
|
|
513
|
+
check=True
|
|
514
|
+
)
|
|
515
|
+
pids = subprocess.check_output(["lsof", "-ti", f":{port}"], text=True).strip().split("\n")
|
|
516
|
+
for pid in pids:
|
|
517
|
+
if pid:
|
|
518
|
+
try:
|
|
519
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
520
|
+
except:
|
|
521
|
+
pass
|
|
522
|
+
except:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
self.server_process = None
|
|
526
|
+
self.server_running = False
|
|
527
|
+
|
|
528
|
+
button = self.query_one("#save_run_button", Button)
|
|
529
|
+
button.label = "Save and Run"
|
|
530
|
+
button.remove_class("kill-mode")
|
|
531
|
+
|
|
532
|
+
rich_log.write("\n[red]Server stopped[/red]")
|
|
533
|
+
self.notify("Server stopped", severity="information")
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
serve_widget = self.widgets_map.get("serve")
|
|
537
|
+
if not serve_widget:
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
agent_name = serve_widget.get_agent_name()
|
|
541
|
+
if not agent_name:
|
|
542
|
+
self.notify("No agent configuration found", severity="error")
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
sanitized_name = self.config_manager._sanitize_agent_name(agent_name)
|
|
546
|
+
config_path = Path.home() / ".idun" / f"{sanitized_name}.yaml"
|
|
547
|
+
|
|
548
|
+
if not config_path.exists():
|
|
549
|
+
self.notify("Configuration file not found", severity="error")
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
logs_container = self.query_one("#logs_container")
|
|
553
|
+
logs_container.display = True
|
|
554
|
+
|
|
555
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
556
|
+
rich_log.clear()
|
|
557
|
+
rich_log.write(f"Starting server for agent: {agent_name}")
|
|
558
|
+
rich_log.write(f"Config: {config_path}")
|
|
559
|
+
|
|
560
|
+
import fcntl
|
|
561
|
+
import os
|
|
562
|
+
import subprocess
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
process = subprocess.Popen(
|
|
566
|
+
["idun", "agent", "serve", "--source=file", f"--path={config_path}"],
|
|
567
|
+
stdout=subprocess.PIPE,
|
|
568
|
+
stderr=subprocess.STDOUT,
|
|
569
|
+
text=True,
|
|
570
|
+
bufsize=1,
|
|
571
|
+
start_new_session=True,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
fd = process.stdout.fileno()
|
|
575
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
576
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
577
|
+
|
|
578
|
+
self.server_process = process
|
|
579
|
+
self.server_running = True
|
|
580
|
+
|
|
581
|
+
button = self.query_one("#save_run_button", Button)
|
|
582
|
+
button.label = "Kill Server"
|
|
583
|
+
button.add_class("kill-mode")
|
|
584
|
+
|
|
585
|
+
self.run_worker(self._stream_logs(process), exclusive=True)
|
|
586
|
+
|
|
587
|
+
except Exception:
|
|
588
|
+
self.notify("Failed to start server. Check your configuration.", severity="error")
|
|
589
|
+
|
|
590
|
+
async def _stream_logs(self, process) -> None:
|
|
591
|
+
import asyncio
|
|
592
|
+
|
|
593
|
+
rich_log = self.query_one("#server_logs", RichLog)
|
|
594
|
+
|
|
595
|
+
while self.server_running:
|
|
596
|
+
try:
|
|
597
|
+
line = process.stdout.readline()
|
|
598
|
+
if line:
|
|
599
|
+
rich_log.write(line.strip())
|
|
600
|
+
except:
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
if process.poll() is not None:
|
|
604
|
+
remaining = process.stdout.read()
|
|
605
|
+
if remaining:
|
|
606
|
+
for line in remaining.split("\n"):
|
|
607
|
+
if line.strip():
|
|
608
|
+
rich_log.write(line.strip())
|
|
609
|
+
|
|
610
|
+
self.server_running = False
|
|
611
|
+
self.server_process = None
|
|
612
|
+
button = self.query_one("#save_run_button", Button)
|
|
613
|
+
button.label = "Save and Run"
|
|
614
|
+
button.remove_class("kill-mode")
|
|
615
|
+
rich_log.write("\n[yellow]Server exited[/yellow]")
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
await asyncio.sleep(0.1)
|
|
619
|
+
|
|
620
|
+
def _update_nav_checkmark(self, section: str) -> None:
|
|
621
|
+
nav_pane = self.query_one(f"#nav-{section}")
|
|
622
|
+
nav_pane.add_class("nav-pane-validated")
|
|
File without changes
|