idun-agent-engine 0.4.0__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/core/config_builder.py +7 -0
- idun_agent_engine/mcp/helpers.py +44 -2
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.1.dist-info}/METADATA +2 -2
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.1.dist-info}/RECORD +26 -23
- idun_platform_cli/groups/agent/package.py +3 -0
- idun_platform_cli/groups/agent/serve.py +2 -0
- idun_platform_cli/groups/init.py +2 -0
- idun_platform_cli/telemetry.py +54 -0
- idun_platform_cli/tui/css/create_agent.py +137 -14
- idun_platform_cli/tui/css/main.py +7 -10
- idun_platform_cli/tui/main.py +3 -3
- idun_platform_cli/tui/screens/create_agent.py +158 -18
- idun_platform_cli/tui/utils/config.py +22 -1
- idun_platform_cli/tui/validators/guardrails.py +15 -3
- idun_platform_cli/tui/widgets/__init__.py +8 -4
- idun_platform_cli/tui/widgets/chat_widget.py +153 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +9 -1
- idun_platform_cli/tui/widgets/identity_widget.py +28 -10
- idun_platform_cli/tui/widgets/mcps_widget.py +2 -2
- idun_platform_cli/tui/widgets/memory_widget.py +195 -0
- idun_platform_cli/tui/widgets/observability_widget.py +11 -13
- idun_platform_cli/tui/widgets/serve_widget.py +51 -47
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.1.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.4.0.dist-info → idun_agent_engine-0.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -13,9 +13,11 @@ from idun_platform_cli.tui.css.create_agent import CREATE_AGENT_CSS
|
|
|
13
13
|
from idun_platform_cli.tui.schemas.create_agent import TUIAgentConfig
|
|
14
14
|
from idun_platform_cli.tui.utils.config import ConfigManager
|
|
15
15
|
from idun_platform_cli.tui.widgets import (
|
|
16
|
+
ChatWidget,
|
|
16
17
|
GuardrailsWidget,
|
|
17
18
|
IdentityWidget,
|
|
18
19
|
MCPsWidget,
|
|
20
|
+
MemoryWidget,
|
|
19
21
|
ObservabilityWidget,
|
|
20
22
|
ServeWidget,
|
|
21
23
|
)
|
|
@@ -32,10 +34,12 @@ class CreateAgentScreen(Screen):
|
|
|
32
34
|
active_section = reactive("identity")
|
|
33
35
|
nav_panes = [
|
|
34
36
|
"nav-identity",
|
|
37
|
+
"nav-memory",
|
|
35
38
|
"nav-observability",
|
|
36
39
|
"nav-guardrails",
|
|
37
40
|
"nav-mcps",
|
|
38
41
|
"nav-serve",
|
|
42
|
+
"nav-chat",
|
|
39
43
|
]
|
|
40
44
|
current_nav_index = 0
|
|
41
45
|
focus_on_nav = True # Track if focus is on nav or content
|
|
@@ -48,6 +52,35 @@ class CreateAgentScreen(Screen):
|
|
|
48
52
|
self.server_process = None
|
|
49
53
|
self.server_running = False
|
|
50
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
|
+
|
|
51
84
|
def watch_active_section(self, new_section: str) -> None:
|
|
52
85
|
for section_id, widget in self.widgets_map.items():
|
|
53
86
|
if section_id == new_section:
|
|
@@ -67,12 +100,20 @@ class CreateAgentScreen(Screen):
|
|
|
67
100
|
severity="warning",
|
|
68
101
|
)
|
|
69
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
|
+
|
|
70
109
|
for pane_id in [
|
|
71
110
|
"nav-identity",
|
|
111
|
+
"nav-memory",
|
|
72
112
|
"nav-observability",
|
|
73
113
|
"nav-guardrails",
|
|
74
114
|
"nav-mcps",
|
|
75
115
|
"nav-serve",
|
|
116
|
+
"nav-chat",
|
|
76
117
|
]:
|
|
77
118
|
pane = self.query_one(f"#{pane_id}")
|
|
78
119
|
if pane_id == f"nav-{new_section}":
|
|
@@ -101,6 +142,18 @@ class CreateAgentScreen(Screen):
|
|
|
101
142
|
nav_identity.can_focus = True
|
|
102
143
|
yield nav_identity
|
|
103
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
|
+
|
|
104
157
|
nav_observability = Vertical(
|
|
105
158
|
Label(
|
|
106
159
|
"Setup monitoring\nand tracing",
|
|
@@ -140,6 +193,15 @@ class CreateAgentScreen(Screen):
|
|
|
140
193
|
nav_serve.can_focus = True
|
|
141
194
|
yield nav_serve
|
|
142
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
|
+
|
|
143
205
|
with Horizontal(classes="action-buttons"):
|
|
144
206
|
yield Button("Back", id="back_button", classes="action-btn")
|
|
145
207
|
yield Button("Next", id="next_button", classes="action-btn")
|
|
@@ -147,6 +209,9 @@ class CreateAgentScreen(Screen):
|
|
|
147
209
|
with Vertical(classes="content-area"):
|
|
148
210
|
identity = IdentityWidget(id="widget-identity", classes="section")
|
|
149
211
|
|
|
212
|
+
memory = MemoryWidget(id="widget-memory", classes="section")
|
|
213
|
+
memory.border_title = "Memory & Checkpointing"
|
|
214
|
+
|
|
150
215
|
observability = ObservabilityWidget(
|
|
151
216
|
id="widget-observability", classes="section"
|
|
152
217
|
)
|
|
@@ -161,26 +226,35 @@ class CreateAgentScreen(Screen):
|
|
|
161
226
|
serve = ServeWidget(id="widget-serve", classes="section")
|
|
162
227
|
serve.border_title = "Validate & Run"
|
|
163
228
|
|
|
229
|
+
chat = ChatWidget(id="widget-chat", classes="section")
|
|
230
|
+
chat.border_title = "Chat"
|
|
231
|
+
|
|
164
232
|
self.widgets_map = {
|
|
165
233
|
"identity": identity,
|
|
234
|
+
"memory": memory,
|
|
166
235
|
"observability": observability,
|
|
167
236
|
"guardrails": guardrails,
|
|
168
237
|
"mcps": mcps,
|
|
169
238
|
"serve": serve,
|
|
239
|
+
"chat": chat,
|
|
170
240
|
}
|
|
171
241
|
|
|
242
|
+
memory.display = False
|
|
172
243
|
observability.display = False
|
|
173
244
|
guardrails.display = False
|
|
174
245
|
mcps.display = False
|
|
175
246
|
serve.display = False
|
|
247
|
+
chat.display = False
|
|
176
248
|
|
|
177
249
|
yield identity
|
|
250
|
+
yield memory
|
|
178
251
|
yield observability
|
|
179
252
|
yield guardrails
|
|
180
253
|
yield mcps
|
|
181
254
|
yield serve
|
|
255
|
+
yield chat
|
|
182
256
|
|
|
183
|
-
footer = Static("💡 Press Next to save section", classes="custom-footer")
|
|
257
|
+
footer = Static("💡 Press Next to save section | Press Ctrl+Q to exit", classes="custom-footer")
|
|
184
258
|
yield footer
|
|
185
259
|
|
|
186
260
|
def on_mount(self) -> None:
|
|
@@ -201,7 +275,7 @@ class CreateAgentScreen(Screen):
|
|
|
201
275
|
if active_widget:
|
|
202
276
|
try:
|
|
203
277
|
focusable = active_widget.query(
|
|
204
|
-
"Input, OptionList, DirectoryTree, Button"
|
|
278
|
+
"Input, OptionList, DirectoryTree, Button, RadioSet"
|
|
205
279
|
).first()
|
|
206
280
|
if focusable:
|
|
207
281
|
focusable.focus()
|
|
@@ -288,6 +362,8 @@ class CreateAgentScreen(Screen):
|
|
|
288
362
|
return
|
|
289
363
|
|
|
290
364
|
if section == "identity":
|
|
365
|
+
if not widget.validate():
|
|
366
|
+
return
|
|
291
367
|
data = widget.get_data()
|
|
292
368
|
if data is None:
|
|
293
369
|
self.notify("Please complete all required fields", severity="error")
|
|
@@ -307,13 +383,28 @@ class CreateAgentScreen(Screen):
|
|
|
307
383
|
return
|
|
308
384
|
self.validated_sections.add("identity")
|
|
309
385
|
self._update_nav_checkmark("identity")
|
|
310
|
-
except ValidationError
|
|
386
|
+
except ValidationError:
|
|
311
387
|
self.notify(
|
|
312
|
-
|
|
388
|
+
"Error validating Identity: make sure all fields are correct.",
|
|
313
389
|
severity="error",
|
|
314
390
|
)
|
|
315
391
|
return
|
|
316
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
|
+
|
|
317
408
|
elif section == "observability":
|
|
318
409
|
data = widget.get_data()
|
|
319
410
|
if data is None:
|
|
@@ -356,15 +447,14 @@ class CreateAgentScreen(Screen):
|
|
|
356
447
|
|
|
357
448
|
validated_servers, msg = validate_mcp_servers(data)
|
|
358
449
|
if validated_servers is None:
|
|
359
|
-
|
|
360
|
-
self.notify(f"MCP validation failed: {error_msg[:200]}", severity="error")
|
|
450
|
+
self.notify("Error validating MCPs: make sure all fields are correct.", severity="error")
|
|
361
451
|
return
|
|
362
452
|
|
|
363
453
|
success, save_msg = self.config_manager.save_partial(
|
|
364
454
|
"mcp_servers", validated_servers
|
|
365
455
|
)
|
|
366
456
|
if not success:
|
|
367
|
-
self.notify(
|
|
457
|
+
self.notify("Error saving MCPs: make sure all fields are correct.", severity="error")
|
|
368
458
|
return
|
|
369
459
|
|
|
370
460
|
self.validated_sections.add("mcps")
|
|
@@ -378,19 +468,68 @@ class CreateAgentScreen(Screen):
|
|
|
378
468
|
section = self.nav_panes[self.current_nav_index].replace("nav-", "")
|
|
379
469
|
self.active_section = section
|
|
380
470
|
|
|
381
|
-
elif event.button.id == "
|
|
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":
|
|
382
479
|
if self.server_running and self.server_process:
|
|
383
|
-
|
|
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
|
+
|
|
384
525
|
self.server_process = None
|
|
385
526
|
self.server_running = False
|
|
386
527
|
|
|
387
|
-
button = self.query_one("#
|
|
388
|
-
button.label = "
|
|
528
|
+
button = self.query_one("#save_run_button", Button)
|
|
529
|
+
button.label = "Save and Run"
|
|
389
530
|
button.remove_class("kill-mode")
|
|
390
531
|
|
|
391
|
-
rich_log = self.query_one("#server_logs", RichLog)
|
|
392
532
|
rich_log.write("\n[red]Server stopped[/red]")
|
|
393
|
-
|
|
394
533
|
self.notify("Server stopped", severity="information")
|
|
395
534
|
return
|
|
396
535
|
|
|
@@ -429,6 +568,7 @@ class CreateAgentScreen(Screen):
|
|
|
429
568
|
stderr=subprocess.STDOUT,
|
|
430
569
|
text=True,
|
|
431
570
|
bufsize=1,
|
|
571
|
+
start_new_session=True,
|
|
432
572
|
)
|
|
433
573
|
|
|
434
574
|
fd = process.stdout.fileno()
|
|
@@ -438,14 +578,14 @@ class CreateAgentScreen(Screen):
|
|
|
438
578
|
self.server_process = process
|
|
439
579
|
self.server_running = True
|
|
440
580
|
|
|
441
|
-
button = self.query_one("#
|
|
581
|
+
button = self.query_one("#save_run_button", Button)
|
|
442
582
|
button.label = "Kill Server"
|
|
443
583
|
button.add_class("kill-mode")
|
|
444
584
|
|
|
445
585
|
self.run_worker(self._stream_logs(process), exclusive=True)
|
|
446
586
|
|
|
447
|
-
except Exception
|
|
448
|
-
self.notify(
|
|
587
|
+
except Exception:
|
|
588
|
+
self.notify("Failed to start server. Check your configuration.", severity="error")
|
|
449
589
|
|
|
450
590
|
async def _stream_logs(self, process) -> None:
|
|
451
591
|
import asyncio
|
|
@@ -469,8 +609,8 @@ class CreateAgentScreen(Screen):
|
|
|
469
609
|
|
|
470
610
|
self.server_running = False
|
|
471
611
|
self.server_process = None
|
|
472
|
-
button = self.query_one("#
|
|
473
|
-
button.label = "
|
|
612
|
+
button = self.query_one("#save_run_button", Button)
|
|
613
|
+
button.label = "Save and Run"
|
|
474
614
|
button.remove_class("kill-mode")
|
|
475
615
|
rich_log.write("\n[yellow]Server exited[/yellow]")
|
|
476
616
|
break
|
|
@@ -7,7 +7,6 @@ from idun_agent_schema.engine.observability_v2 import ObservabilityConfig
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
|
|
9
9
|
from idun_platform_cli.tui.schemas.create_agent import (
|
|
10
|
-
AGENT_SOURCE_KEY_MAPPING,
|
|
11
10
|
TUIAgentConfig,
|
|
12
11
|
)
|
|
13
12
|
|
|
@@ -128,6 +127,28 @@ class ConfigManager:
|
|
|
128
127
|
existing_config["mcp_servers"] = mcp_servers_list
|
|
129
128
|
else:
|
|
130
129
|
existing_config["mcp_servers"] = data
|
|
130
|
+
elif section == "memory":
|
|
131
|
+
from idun_agent_schema.engine.langgraph import CheckpointConfig
|
|
132
|
+
|
|
133
|
+
if "agent" not in existing_config:
|
|
134
|
+
return False, "Agent configuration not found. Save identity first."
|
|
135
|
+
|
|
136
|
+
agent_type = existing_config.get("agent", {}).get("type")
|
|
137
|
+
if agent_type != "LANGGRAPH":
|
|
138
|
+
return (
|
|
139
|
+
True,
|
|
140
|
+
"Checkpoint configuration skipped for non-LANGGRAPH agents",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if isinstance(data, CheckpointConfig):
|
|
144
|
+
checkpoint_dict = data.model_dump(by_alias=False, mode="json")
|
|
145
|
+
|
|
146
|
+
if "config" not in existing_config["agent"]:
|
|
147
|
+
existing_config["agent"]["config"] = {}
|
|
148
|
+
|
|
149
|
+
existing_config["agent"]["config"]["checkpointer"] = checkpoint_dict
|
|
150
|
+
else:
|
|
151
|
+
return False, "Invalid checkpoint configuration type"
|
|
131
152
|
else:
|
|
132
153
|
existing_config[section] = data
|
|
133
154
|
|
|
@@ -16,14 +16,20 @@ def validate_guardrail(guardrail_id: str, config: dict) -> tuple[any, str]:
|
|
|
16
16
|
case "bias_check":
|
|
17
17
|
threshold = float(config.get("threshold", 0.5))
|
|
18
18
|
validated = BiasCheckConfig(
|
|
19
|
-
config_id=GuardrailConfigId.BIAS_CHECK,
|
|
19
|
+
config_id=GuardrailConfigId.BIAS_CHECK,
|
|
20
|
+
api_key=config.get("api_key", ""),
|
|
21
|
+
reject_message=config.get("reject_message", "Bias detected"),
|
|
22
|
+
threshold=threshold
|
|
20
23
|
)
|
|
21
24
|
return validated, "ok"
|
|
22
25
|
|
|
23
26
|
case "toxic_language":
|
|
24
27
|
threshold = float(config.get("threshold", 0.5))
|
|
25
28
|
validated = ToxicLanguageConfig(
|
|
26
|
-
config_id=GuardrailConfigId.TOXIC_LANGUAGE,
|
|
29
|
+
config_id=GuardrailConfigId.TOXIC_LANGUAGE,
|
|
30
|
+
api_key=config.get("api_key", ""),
|
|
31
|
+
reject_message=config.get("reject_message", "Toxic language detected"),
|
|
32
|
+
threshold=threshold
|
|
27
33
|
)
|
|
28
34
|
return validated, "ok"
|
|
29
35
|
|
|
@@ -35,6 +41,8 @@ def validate_guardrail(guardrail_id: str, config: dict) -> tuple[any, str]:
|
|
|
35
41
|
]
|
|
36
42
|
validated = CompetitionCheckConfig(
|
|
37
43
|
config_id=GuardrailConfigId.COMPETITION_CHECK,
|
|
44
|
+
api_key=config.get("api_key", ""),
|
|
45
|
+
reject_message=config.get("reject_message", "Competitor mentioned"),
|
|
38
46
|
competitors=competitors,
|
|
39
47
|
)
|
|
40
48
|
return validated, "ok"
|
|
@@ -73,4 +81,8 @@ def validate_guardrail(guardrail_id: str, config: dict) -> tuple[any, str]:
|
|
|
73
81
|
return None, f"Unknown guardrail type: {guardrail_id}"
|
|
74
82
|
|
|
75
83
|
except Exception as e:
|
|
76
|
-
|
|
84
|
+
error_msg = str(e)
|
|
85
|
+
if len(error_msg) > 100:
|
|
86
|
+
error_msg = error_msg[:100] + "..."
|
|
87
|
+
error_msg = error_msg.replace("<", "").replace(">", "")
|
|
88
|
+
return None, f"Validation error for {guardrail_id}: {error_msg}"
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"""Widget components for the agent configuration screens."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
from .observability_widget import ObservabilityWidget
|
|
3
|
+
from .chat_widget import ChatWidget
|
|
5
4
|
from .guardrails_widget import GuardrailsWidget
|
|
5
|
+
from .identity_widget import IdentityWidget
|
|
6
6
|
from .mcps_widget import MCPsWidget
|
|
7
|
+
from .memory_widget import MemoryWidget
|
|
8
|
+
from .observability_widget import ObservabilityWidget
|
|
7
9
|
from .serve_widget import ServeWidget
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
|
-
"
|
|
11
|
-
"ObservabilityWidget",
|
|
12
|
+
"ChatWidget",
|
|
12
13
|
"GuardrailsWidget",
|
|
14
|
+
"IdentityWidget",
|
|
13
15
|
"MCPsWidget",
|
|
16
|
+
"MemoryWidget",
|
|
17
|
+
"ObservabilityWidget",
|
|
14
18
|
"ServeWidget",
|
|
15
19
|
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Chat widget for interacting with running agent."""
|
|
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 Button, Input, Label, LoadingIndicator, RichLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChatWidget(Widget):
|
|
11
|
+
server_running = reactive(False)
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.config_data = {}
|
|
16
|
+
self.server_port = None
|
|
17
|
+
self.agent_name = ""
|
|
18
|
+
|
|
19
|
+
def compose(self) -> ComposeResult:
|
|
20
|
+
chat_container = Vertical(classes="chat-history-container")
|
|
21
|
+
chat_container.border_title = "Conversation"
|
|
22
|
+
with chat_container:
|
|
23
|
+
yield RichLog(id="chat_history", highlight=True, markup=True, wrap=True)
|
|
24
|
+
|
|
25
|
+
thinking_container = Horizontal(classes="chat-thinking-container", id="chat_thinking")
|
|
26
|
+
thinking_container.display = False
|
|
27
|
+
with thinking_container:
|
|
28
|
+
yield LoadingIndicator(id="chat_spinner")
|
|
29
|
+
yield Label("Thinking...", id="thinking_label")
|
|
30
|
+
|
|
31
|
+
input_container = Horizontal(classes="chat-input-container")
|
|
32
|
+
with input_container:
|
|
33
|
+
yield Input(
|
|
34
|
+
placeholder="Type your message...",
|
|
35
|
+
id="chat_input",
|
|
36
|
+
classes="chat-input",
|
|
37
|
+
)
|
|
38
|
+
yield Button("Send", id="send_button", classes="send-btn")
|
|
39
|
+
|
|
40
|
+
def load_config(self, config: dict) -> None:
|
|
41
|
+
self.config_data = config
|
|
42
|
+
server_config = config.get("server", {})
|
|
43
|
+
api_config = server_config.get("api", {})
|
|
44
|
+
self.server_port = api_config.get("port", 8008)
|
|
45
|
+
|
|
46
|
+
agent_config = config.get("agent", {}).get("config", {})
|
|
47
|
+
self.agent_name = agent_config.get("name", "Agent")
|
|
48
|
+
|
|
49
|
+
self.run_worker(self._check_server_status())
|
|
50
|
+
|
|
51
|
+
def on_mount(self) -> None:
|
|
52
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
53
|
+
chat_log.write("[dim]Start chatting with your agent...[/dim]")
|
|
54
|
+
chat_log.write(
|
|
55
|
+
"[dim]Make sure the agent server is running from the Serve page.[/dim]"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
59
|
+
if event.button.id == "send_button":
|
|
60
|
+
self._handle_send()
|
|
61
|
+
|
|
62
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
63
|
+
if event.input.id == "chat_input":
|
|
64
|
+
self._handle_send()
|
|
65
|
+
|
|
66
|
+
def _handle_send(self) -> None:
|
|
67
|
+
input_widget = self.query_one("#chat_input", Input)
|
|
68
|
+
message = input_widget.value.strip()
|
|
69
|
+
|
|
70
|
+
if not message:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if not self.server_port:
|
|
74
|
+
self.app.notify("Server not configured", severity="error")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
input_widget.value = ""
|
|
78
|
+
|
|
79
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
80
|
+
chat_log.write(f"[cyan]You:[/cyan] {message}")
|
|
81
|
+
|
|
82
|
+
thinking_container = self.query_one("#chat_thinking")
|
|
83
|
+
thinking_container.display = True
|
|
84
|
+
|
|
85
|
+
self.run_worker(self._send_message(message))
|
|
86
|
+
|
|
87
|
+
async def _send_message(self, message: str) -> None:
|
|
88
|
+
import httpx
|
|
89
|
+
|
|
90
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
91
|
+
thinking_container = self.query_one("#chat_thinking")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
url = f"http://localhost:{self.server_port}/agent/invoke"
|
|
95
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
96
|
+
response = await client.post(
|
|
97
|
+
url, json={"session_id": "123", "query": message}
|
|
98
|
+
)
|
|
99
|
+
result = response.json()
|
|
100
|
+
|
|
101
|
+
agent_response = result.get(
|
|
102
|
+
"output", result.get("response", "No response")
|
|
103
|
+
)
|
|
104
|
+
thinking_container.display = False
|
|
105
|
+
chat_log.write(f"[green]{self.agent_name}:[/green] {agent_response}")
|
|
106
|
+
|
|
107
|
+
except httpx.ConnectError:
|
|
108
|
+
thinking_container.display = False
|
|
109
|
+
chat_log.write("[red]Error:[/red] Cannot connect to server. Is it running?")
|
|
110
|
+
self.app.notify(
|
|
111
|
+
"Server not reachable. Start it from the Serve page.", severity="error"
|
|
112
|
+
)
|
|
113
|
+
except httpx.TimeoutException:
|
|
114
|
+
thinking_container.display = False
|
|
115
|
+
chat_log.write("[red]Error:[/red] Request timed out")
|
|
116
|
+
self.app.notify("Request timed out", severity="error")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
thinking_container.display = False
|
|
119
|
+
chat_log.write(f"[red]Error:[/red] Failed to send message: {e}")
|
|
120
|
+
self.app.notify(
|
|
121
|
+
"Failed to send message. Check server connection.", severity="error"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def _check_server_status(self) -> None:
|
|
125
|
+
import httpx
|
|
126
|
+
|
|
127
|
+
if not self.server_port:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
url = f"http://localhost:{self.server_port}/health"
|
|
132
|
+
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
133
|
+
response = await client.get(url)
|
|
134
|
+
self.server_running = response.status_code == 200
|
|
135
|
+
|
|
136
|
+
if self.server_running:
|
|
137
|
+
chat_log = self.query_one("#chat_history", RichLog)
|
|
138
|
+
chat_log.write(
|
|
139
|
+
f"[green]✓ Connected to server on port {self.server_port}[/green]"
|
|
140
|
+
)
|
|
141
|
+
except Exception:
|
|
142
|
+
self.server_running = False
|
|
143
|
+
|
|
144
|
+
def watch_server_running(self, is_running: bool) -> None:
|
|
145
|
+
input_widget = self.query_one("#chat_input", Input)
|
|
146
|
+
send_button = self.query_one("#send_button", Button)
|
|
147
|
+
|
|
148
|
+
if is_running:
|
|
149
|
+
input_widget.disabled = False
|
|
150
|
+
send_button.disabled = False
|
|
151
|
+
else:
|
|
152
|
+
input_widget.disabled = True
|
|
153
|
+
send_button.disabled = True
|
|
@@ -304,7 +304,15 @@ class GuardrailsWidget(Widget):
|
|
|
304
304
|
if applies_to in ["output", "both"]:
|
|
305
305
|
output_guardrails.append(validated_config)
|
|
306
306
|
|
|
307
|
-
|
|
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
|
|
308
316
|
|
|
309
317
|
def _extract_config(self, guardrail_id: str) -> dict:
|
|
310
318
|
config = {}
|