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.
Files changed (45) hide show
  1. idun_agent_engine/_version.py +1 -1
  2. idun_agent_engine/agent/adk/adk.py +5 -2
  3. idun_agent_engine/agent/langgraph/langgraph.py +1 -1
  4. idun_agent_engine/core/app_factory.py +1 -1
  5. idun_agent_engine/core/config_builder.py +11 -5
  6. idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
  7. idun_agent_engine/mcp/__init__.py +18 -2
  8. idun_agent_engine/mcp/helpers.py +135 -43
  9. idun_agent_engine/mcp/registry.py +7 -1
  10. idun_agent_engine/server/lifespan.py +22 -0
  11. idun_agent_engine/telemetry/__init__.py +19 -0
  12. idun_agent_engine/telemetry/config.py +29 -0
  13. idun_agent_engine/telemetry/telemetry.py +248 -0
  14. {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/METADATA +12 -8
  15. {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/RECORD +45 -17
  16. idun_platform_cli/groups/agent/package.py +3 -0
  17. idun_platform_cli/groups/agent/serve.py +2 -0
  18. idun_platform_cli/groups/init.py +25 -0
  19. idun_platform_cli/main.py +3 -0
  20. idun_platform_cli/telemetry.py +54 -0
  21. idun_platform_cli/tui/__init__.py +0 -0
  22. idun_platform_cli/tui/css/__init__.py +0 -0
  23. idun_platform_cli/tui/css/create_agent.py +912 -0
  24. idun_platform_cli/tui/css/main.py +89 -0
  25. idun_platform_cli/tui/main.py +87 -0
  26. idun_platform_cli/tui/schemas/__init__.py +0 -0
  27. idun_platform_cli/tui/schemas/create_agent.py +60 -0
  28. idun_platform_cli/tui/screens/__init__.py +0 -0
  29. idun_platform_cli/tui/screens/create_agent.py +622 -0
  30. idun_platform_cli/tui/utils/__init__.py +0 -0
  31. idun_platform_cli/tui/utils/config.py +182 -0
  32. idun_platform_cli/tui/validators/__init__.py +0 -0
  33. idun_platform_cli/tui/validators/guardrails.py +88 -0
  34. idun_platform_cli/tui/validators/mcps.py +84 -0
  35. idun_platform_cli/tui/validators/observability.py +65 -0
  36. idun_platform_cli/tui/widgets/__init__.py +19 -0
  37. idun_platform_cli/tui/widgets/chat_widget.py +153 -0
  38. idun_platform_cli/tui/widgets/guardrails_widget.py +356 -0
  39. idun_platform_cli/tui/widgets/identity_widget.py +252 -0
  40. idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
  41. idun_platform_cli/tui/widgets/memory_widget.py +195 -0
  42. idun_platform_cli/tui/widgets/observability_widget.py +382 -0
  43. idun_platform_cli/tui/widgets/serve_widget.py +82 -0
  44. {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/WHEEL +0 -0
  45. {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,230 @@
1
+ """MCPs configuration widget."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal, Vertical
5
+ from textual.widgets import Static, Input, Button, RadioSet, RadioButton, TextArea, OptionList
6
+ from textual.widget import Widget
7
+ from textual.widgets.option_list import Option
8
+
9
+
10
+ MCP_TEMPLATES = {
11
+ "time": {
12
+ "name": "time-reference",
13
+ "transport": "stdio",
14
+ "command": "docker",
15
+ "args": ["run", "-i", "--rm", "mcp/time"],
16
+ },
17
+ }
18
+
19
+
20
+ class MCPsWidget(Widget):
21
+ def __init__(self, *args, **kwargs):
22
+ super().__init__(*args, **kwargs)
23
+ self.mcp_servers = []
24
+ self.next_server_id = 0
25
+
26
+ def on_mount(self) -> None:
27
+ template_selector = self.query_one("#template_selector", OptionList)
28
+ if template_selector.option_count > 0:
29
+ template_selector.highlighted = 0
30
+
31
+ def compose(self) -> ComposeResult:
32
+ templates_section = Vertical(classes="mcps-templates-section")
33
+ templates_section.border_title = "MCP Templates"
34
+ templates_row = Horizontal(classes="templates-row")
35
+ templates_row.compose_add_child(Static("Select template:", classes="mcp-label"))
36
+ option_list = OptionList(id="template_selector", classes="template-selector")
37
+ for template_name in MCP_TEMPLATES.keys():
38
+ option_list.add_option(Option(template_name.title(), id=template_name))
39
+ templates_row.compose_add_child(option_list)
40
+ templates_row.compose_add_child(Button("Add from Template", id="add_from_template_button", classes="add-template-btn"))
41
+ templates_section.compose_add_child(templates_row)
42
+ yield templates_section
43
+
44
+ yield Button("+ Add Custom MCP Server", id="add_custom_mcp_button", classes="add-custom-btn")
45
+
46
+ yield Vertical(id="mcps_container", classes="mcps-container")
47
+
48
+ def on_button_pressed(self, event: Button.Pressed) -> None:
49
+ button_id = event.button.id
50
+
51
+ if button_id == "add_custom_mcp_button":
52
+ self._add_mcp_server()
53
+ elif button_id == "add_from_template_button":
54
+ self._add_from_template()
55
+ elif button_id and str(button_id).startswith("remove_mcp_"):
56
+ index = int(str(button_id).replace("remove_mcp_", ""))
57
+ self._remove_mcp_server(index)
58
+
59
+ def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
60
+ radio_id = event.radio_set.id
61
+ if radio_id and str(radio_id).startswith("mcp_transport_"):
62
+ index = int(str(radio_id).replace("mcp_transport_", ""))
63
+ if event.pressed:
64
+ transport = str(event.pressed.id)
65
+ self._update_transport_fields(index, transport)
66
+
67
+ def _add_from_template(self) -> None:
68
+ template_selector = self.query_one("#template_selector", OptionList)
69
+ if template_selector.highlighted is not None:
70
+ option_id = template_selector.get_option_at_index(template_selector.highlighted).id
71
+ if option_id and str(option_id) in MCP_TEMPLATES:
72
+ template_data = MCP_TEMPLATES[str(option_id)].copy()
73
+ self._add_mcp_server(template_data)
74
+ else:
75
+ self.app.notify("Please select a template first", severity="warning")
76
+
77
+ def _add_mcp_server(self, template_data: dict = None) -> None:
78
+ server_id = self.next_server_id
79
+ self.next_server_id += 1
80
+ self.mcp_servers.append(server_id)
81
+
82
+ card = self._create_mcp_card(server_id, template_data)
83
+ container = self.query_one("#mcps_container", Vertical)
84
+ container.mount(card)
85
+
86
+ def _remove_mcp_server(self, index: int) -> None:
87
+ if index in self.mcp_servers:
88
+ self.mcp_servers.remove(index)
89
+ card = self.query_one(f"#mcp_card_{index}")
90
+ card.remove()
91
+
92
+ def _create_mcp_card(self, index: int, template_data: dict = None) -> Vertical:
93
+ card = Vertical(id=f"mcp_card_{index}", classes="mcp-card")
94
+ card.border_title = f"MCP Server {index + 1}"
95
+
96
+ header = Horizontal(classes="mcp-header")
97
+ name_value = template_data.get("name", "") if template_data else ""
98
+ header.compose_add_child(Static(name_value or f"Server {index + 1}", id=f"mcp_name_display_{index}", classes="mcp-name-display"))
99
+ header.compose_add_child(Button("Remove", id=f"remove_mcp_{index}", classes="remove-mcp-btn"))
100
+ card.compose_add_child(header)
101
+
102
+ name_row = Horizontal(classes="mcp-field-row")
103
+ name_row.compose_add_child(Static("Name:", classes="mcp-label"))
104
+ name_row.compose_add_child(Input(value=name_value, placeholder="server-name", id=f"mcp_name_{index}", classes="mcp-input"))
105
+ card.compose_add_child(name_row)
106
+
107
+ transport_row = Horizontal(classes="mcp-field-row")
108
+ transport_row.compose_add_child(Static("Transport:", classes="mcp-label"))
109
+ radio_set = RadioSet(id=f"mcp_transport_{index}")
110
+
111
+ transport_value = template_data.get("transport", "streamable_http") if template_data else "streamable_http"
112
+ radio_set.compose_add_child(RadioButton("stdio", id="stdio", value=(transport_value == "stdio")))
113
+ radio_set.compose_add_child(RadioButton("sse", id="sse", value=(transport_value == "sse")))
114
+ radio_set.compose_add_child(RadioButton("streamable_http", id="streamable_http", value=(transport_value == "streamable_http")))
115
+ radio_set.compose_add_child(RadioButton("websocket", id="websocket", value=(transport_value == "websocket")))
116
+ transport_row.compose_add_child(radio_set)
117
+ card.compose_add_child(transport_row)
118
+
119
+ http_fields = Vertical(id=f"mcp_http_fields_{index}", classes="http-fields-container")
120
+ http_fields.border_title = "HTTP Configuration"
121
+
122
+ url_row = Horizontal(classes="mcp-field-row")
123
+ url_row.compose_add_child(Static("URL:", classes="mcp-label"))
124
+ url_value = template_data.get("url", "") if template_data else ""
125
+ url_row.compose_add_child(Input(value=url_value, placeholder="https://api.example.com/mcp", id=f"mcp_url_{index}", classes="mcp-input"))
126
+ http_fields.compose_add_child(url_row)
127
+
128
+ headers_row = Horizontal(classes="mcp-field-row")
129
+ headers_row.compose_add_child(Static("Headers (JSON):", classes="mcp-label"))
130
+ headers_value = template_data.get("headers", "") if template_data else ""
131
+ headers_row.compose_add_child(TextArea(text=str(headers_value) if headers_value else "", id=f"mcp_headers_{index}", classes="mcp-textarea"))
132
+ http_fields.compose_add_child(headers_row)
133
+
134
+ http_fields.display = transport_value in ["sse", "streamable_http", "websocket"]
135
+ card.compose_add_child(http_fields)
136
+
137
+ stdio_fields = Vertical(id=f"mcp_stdio_fields_{index}", classes="stdio-fields-container")
138
+ stdio_fields.border_title = "Stdio Configuration"
139
+
140
+ command_row = Horizontal(classes="mcp-field-row")
141
+ command_row.compose_add_child(Static("Command:", classes="mcp-label"))
142
+ command_value = template_data.get("command", "") if template_data else ""
143
+ command_row.compose_add_child(Input(value=command_value, placeholder="npx", id=f"mcp_command_{index}", classes="mcp-input"))
144
+ stdio_fields.compose_add_child(command_row)
145
+
146
+ args_row = Horizontal(classes="mcp-field-row")
147
+ args_row.compose_add_child(Static("Args (one per line):", classes="mcp-label"))
148
+ args_value = ""
149
+ if template_data and "args" in template_data:
150
+ args_list = template_data["args"]
151
+ if isinstance(args_list, list):
152
+ args_value = "\n".join(args_list)
153
+ args_textarea = TextArea(text=args_value, id=f"mcp_args_{index}", classes="mcp-textarea")
154
+ args_textarea.placeholder = "run\n-i\n--rm"
155
+ args_row.compose_add_child(args_textarea)
156
+ stdio_fields.compose_add_child(args_row)
157
+
158
+ env_row = Horizontal(classes="mcp-field-row")
159
+ env_row.compose_add_child(Static("Env Vars (JSON):", classes="mcp-label"))
160
+ env_value = ""
161
+ if template_data and "env" in template_data:
162
+ import json
163
+ env_value = json.dumps(template_data["env"], indent=2)
164
+ env_row.compose_add_child(TextArea(text=env_value, id=f"mcp_env_{index}", classes="mcp-textarea"))
165
+ stdio_fields.compose_add_child(env_row)
166
+
167
+ stdio_fields.display = transport_value == "stdio"
168
+ card.compose_add_child(stdio_fields)
169
+
170
+ return card
171
+
172
+ def _update_transport_fields(self, index: int, transport: str) -> None:
173
+ http_fields = self.query_one(f"#mcp_http_fields_{index}")
174
+ stdio_fields = self.query_one(f"#mcp_stdio_fields_{index}")
175
+
176
+ if transport in ["sse", "streamable_http", "websocket"]:
177
+ http_fields.display = True
178
+ stdio_fields.display = False
179
+ elif transport == "stdio":
180
+ http_fields.display = False
181
+ stdio_fields.display = True
182
+
183
+ def get_data(self) -> list[dict] | None:
184
+ servers_data = []
185
+
186
+ for server_id in self.mcp_servers:
187
+ try:
188
+ name_input = self.query_one(f"#mcp_name_{server_id}", Input)
189
+ name = name_input.value
190
+
191
+ if not name:
192
+ self.app.notify(f"Server {server_id + 1}: Name is required", severity="error")
193
+ return None
194
+
195
+ radio_set = self.query_one(f"#mcp_transport_{server_id}", RadioSet)
196
+ transport = "streamable_http"
197
+ if radio_set.pressed_button:
198
+ transport = str(radio_set.pressed_button.id)
199
+
200
+ server_config = {
201
+ "name": name,
202
+ "transport": transport,
203
+ }
204
+
205
+ if transport in ["sse", "streamable_http", "websocket"]:
206
+ url_input = self.query_one(f"#mcp_url_{server_id}", Input)
207
+ server_config["url"] = url_input.value
208
+
209
+ headers_textarea = self.query_one(f"#mcp_headers_{server_id}", TextArea)
210
+ if headers_textarea.text.strip():
211
+ server_config["headers"] = headers_textarea.text.strip()
212
+
213
+ elif transport == "stdio":
214
+ command_input = self.query_one(f"#mcp_command_{server_id}", Input)
215
+ server_config["command"] = command_input.value
216
+
217
+ args_textarea = self.query_one(f"#mcp_args_{server_id}", TextArea)
218
+ server_config["args"] = args_textarea.text
219
+
220
+ env_textarea = self.query_one(f"#mcp_env_{server_id}", TextArea)
221
+ if env_textarea.text.strip():
222
+ server_config["env"] = env_textarea.text.strip()
223
+
224
+ servers_data.append(server_config)
225
+
226
+ except Exception:
227
+ self.app.notify(f"Error reading MCP server {server_id + 1}: check your configuration.", severity="error")
228
+ return None
229
+
230
+ return servers_data
@@ -0,0 +1,195 @@
1
+ """Memory and checkpoint configuration widget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pydantic import ValidationError
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Horizontal, Vertical
10
+ from textual.css.query import NoMatches
11
+ from textual.reactive import reactive
12
+ from textual.widget import Widget
13
+ from textual.widgets import Input, RadioButton, RadioSet, Static
14
+
15
+ if TYPE_CHECKING:
16
+ from idun_agent_schema.engine.langgraph import CheckpointConfig
17
+
18
+
19
+ class MemoryWidget(Widget):
20
+ selected_type = reactive("memory")
21
+
22
+ def compose(self) -> ComposeResult:
23
+ main_section = Vertical(classes="memory-main")
24
+ main_section.border_title = "Checkpoint Configuration"
25
+
26
+ with main_section, Horizontal(classes="field-row framework-row"):
27
+ yield Static("Type:", classes="field-label")
28
+ with RadioSet(id="checkpoint_type_select"):
29
+ yield RadioButton("In-Memory", id="memory", value=True)
30
+ yield RadioButton("SQLite", id="sqlite")
31
+ yield RadioButton("PostgreSQL", id="postgres")
32
+
33
+ config_container = Vertical(
34
+ classes="checkpoint-config-container",
35
+ id="checkpoint_config",
36
+ )
37
+ yield config_container
38
+
39
+ def on_mount(self) -> None:
40
+ self._update_checkpoint_config()
41
+
42
+ def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
43
+ if event.radio_set.id == "checkpoint_type_select":
44
+ self.selected_type = str(event.pressed.id)
45
+ self._update_checkpoint_config()
46
+
47
+ def _update_checkpoint_config(self) -> None:
48
+ try:
49
+ config_container = self.query_one("#checkpoint_config", Vertical)
50
+ config_container.remove_children()
51
+
52
+ if self.selected_type == "memory":
53
+ pass
54
+ elif self.selected_type == "sqlite":
55
+ self._render_sqlite_config(config_container)
56
+ elif self.selected_type == "postgres":
57
+ self._render_postgres_config(config_container)
58
+ except Exception:
59
+ pass
60
+
61
+ def _render_sqlite_config(self, container: Vertical) -> None:
62
+ config_section = Vertical(
63
+ Horizontal(
64
+ Static("DB URL:", classes="field-label"),
65
+ Input(
66
+ placeholder="sqlite:///./checkpoints.db",
67
+ id="sqlite_db_url",
68
+ classes="field-input",
69
+ ),
70
+ classes="field-row",
71
+ ),
72
+ classes="checkpoint-fields-section",
73
+ )
74
+ config_section.border_title = "SQLite Configuration"
75
+ container.mount(config_section)
76
+
77
+ def _render_postgres_config(self, container: Vertical) -> None:
78
+ config_section = Vertical(
79
+ Horizontal(
80
+ Static("DB URL:", classes="field-label"),
81
+ Input(
82
+ placeholder="postgresql://user:pass@localhost:5432/db",
83
+ id="postgres_db_url",
84
+ classes="field-input",
85
+ ),
86
+ classes="field-row",
87
+ ),
88
+ classes="checkpoint-fields-section",
89
+ )
90
+ config_section.border_title = "PostgreSQL Configuration"
91
+ container.mount(config_section)
92
+
93
+ def get_data(self) -> CheckpointConfig | None:
94
+ from idun_agent_schema.engine.langgraph import (
95
+ CheckpointConfig,
96
+ InMemoryCheckpointConfig,
97
+ PostgresCheckpointConfig,
98
+ SqliteCheckpointConfig,
99
+ )
100
+
101
+ try:
102
+ radio_set = self.query_one("#checkpoint_type_select", RadioSet)
103
+
104
+ checkpoint_type = "memory"
105
+ if radio_set.pressed_button:
106
+ checkpoint_type = str(radio_set.pressed_button.id)
107
+
108
+ if checkpoint_type == "memory":
109
+ return InMemoryCheckpointConfig(type="memory")
110
+
111
+ elif checkpoint_type == "sqlite":
112
+ try:
113
+ db_url_input = self.query_one("#sqlite_db_url", Input)
114
+ db_url = db_url_input.value.strip()
115
+ except NoMatches:
116
+ self.app.notify(
117
+ "Configuration error. Please reselect checkpoint type.",
118
+ severity="error",
119
+ )
120
+ return None
121
+
122
+ if not db_url:
123
+ self.app.notify(
124
+ "SQLite DB URL is required",
125
+ severity="error",
126
+ )
127
+ return None
128
+
129
+ if not db_url.startswith("sqlite:///"):
130
+ self.app.notify(
131
+ "SQLite URL must start with 'sqlite:///'",
132
+ severity="error",
133
+ )
134
+ return None
135
+
136
+ try:
137
+ return SqliteCheckpointConfig(type="sqlite", db_url=db_url)
138
+ except ValidationError:
139
+ self.app.notify(
140
+ "Invalid SQLite configuration. Check your URL format.",
141
+ severity="error",
142
+ )
143
+ return None
144
+
145
+ elif checkpoint_type == "postgres":
146
+ try:
147
+ db_url_input = self.query_one("#postgres_db_url", Input)
148
+ db_url = db_url_input.value.strip()
149
+ except NoMatches:
150
+ self.app.notify(
151
+ "Configuration error. Please reselect checkpoint type.",
152
+ severity="error",
153
+ )
154
+ return None
155
+
156
+ if not db_url:
157
+ self.app.notify(
158
+ "PostgreSQL DB URL is required",
159
+ severity="error",
160
+ )
161
+ return None
162
+
163
+ if not (
164
+ db_url.startswith("postgresql://")
165
+ or db_url.startswith("postgres://")
166
+ ):
167
+ self.app.notify(
168
+ "PostgreSQL URL must start with 'postgresql://' or 'postgres://'",
169
+ severity="error",
170
+ )
171
+ return None
172
+
173
+ try:
174
+ return PostgresCheckpointConfig(type="postgres", db_url=db_url)
175
+ except ValidationError:
176
+ self.app.notify(
177
+ "Invalid PostgreSQL configuration. Check your URL format.",
178
+ severity="error",
179
+ )
180
+ return None
181
+
182
+ except NoMatches:
183
+ self.app.notify(
184
+ "Error reading checkpoint configuration",
185
+ severity="error",
186
+ )
187
+ return None
188
+ except Exception:
189
+ self.app.notify(
190
+ "Error validating checkpoint configuration",
191
+ severity="error",
192
+ )
193
+ return None
194
+
195
+ return None