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,182 @@
|
|
|
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
|
+
TUIAgentConfig,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigManager:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.idun_dir = Path.home() / ".idun"
|
|
17
|
+
self.agent_path = None
|
|
18
|
+
try:
|
|
19
|
+
self.idun_dir.mkdir(exist_ok=True)
|
|
20
|
+
|
|
21
|
+
except OSError as e:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Error while preparing `.idun` config file: {e}\nNote: This file is used to store config and env data"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
def _sanitize_agent_name(self, agent_name: str) -> str:
|
|
27
|
+
return agent_name.lstrip().replace("-", "_").replace(" ", "_")
|
|
28
|
+
|
|
29
|
+
def _validate_data(
|
|
30
|
+
self, config: dict[str, Any]
|
|
31
|
+
) -> tuple[TUIAgentConfig | None, str]:
|
|
32
|
+
try:
|
|
33
|
+
return TUIAgentConfig.model_validate(config), "valid"
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return None, f"Error: cannot validate config: {e}"
|
|
36
|
+
|
|
37
|
+
def save_config(self, config: dict) -> tuple[bool, str]:
|
|
38
|
+
try:
|
|
39
|
+
raw_agent_name: str = config["name"]
|
|
40
|
+
except KeyError as e:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"Agent name is not defined! Make sure you specify a name for your agent!"
|
|
43
|
+
) from e
|
|
44
|
+
sanitized_agent_name = self._sanitize_agent_name(raw_agent_name)
|
|
45
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(".yaml")
|
|
46
|
+
|
|
47
|
+
with self.agent_path.open("w") as f:
|
|
48
|
+
serialized, msg = self._validate_data(config)
|
|
49
|
+
if serialized is None:
|
|
50
|
+
return False, msg
|
|
51
|
+
with self.agent_path.open("w") as f:
|
|
52
|
+
yaml.dump(serialized.to_engine_config(), f, default_flow_style=False)
|
|
53
|
+
return True, "Valid"
|
|
54
|
+
|
|
55
|
+
def load_draft(self) -> dict | None:
|
|
56
|
+
if self.agent_path is None or not self.agent_path.exists():
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"No agent config file found. Make sure you have saved agent configs."
|
|
59
|
+
)
|
|
60
|
+
with self.agent_path.open("r") as f:
|
|
61
|
+
return yaml.safe_load(f) or {}
|
|
62
|
+
|
|
63
|
+
def save_partial(
|
|
64
|
+
self, section: str, data: dict | Any, agent_name: str = None
|
|
65
|
+
) -> tuple[bool, str]:
|
|
66
|
+
try:
|
|
67
|
+
from idun_agent_engine.core.engine_config import EngineConfig
|
|
68
|
+
|
|
69
|
+
if agent_name:
|
|
70
|
+
sanitized_agent_name = self._sanitize_agent_name(agent_name)
|
|
71
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(
|
|
72
|
+
".yaml"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if self.agent_path is None:
|
|
76
|
+
return False, "Agent name not set. Save identity section first."
|
|
77
|
+
|
|
78
|
+
existing_config = {}
|
|
79
|
+
if self.agent_path.exists():
|
|
80
|
+
with self.agent_path.open("r") as f:
|
|
81
|
+
existing_config = yaml.safe_load(f) or {}
|
|
82
|
+
|
|
83
|
+
if section == "identity":
|
|
84
|
+
from idun_platform_cli.tui.schemas.create_agent import TUIAgentConfig
|
|
85
|
+
|
|
86
|
+
tui_config = TUIAgentConfig.model_validate(data)
|
|
87
|
+
engine_config_dict = tui_config.to_engine_config()
|
|
88
|
+
existing_config["server"] = engine_config_dict["server"]
|
|
89
|
+
existing_config["agent"] = engine_config_dict["agent"]
|
|
90
|
+
elif section == "observability":
|
|
91
|
+
if isinstance(data, ObservabilityConfig):
|
|
92
|
+
obs_dict = {
|
|
93
|
+
"provider": data.provider.value,
|
|
94
|
+
"enabled": data.enabled,
|
|
95
|
+
"config": data.config.model_dump(by_alias=False),
|
|
96
|
+
}
|
|
97
|
+
existing_config["observability"] = [obs_dict]
|
|
98
|
+
else:
|
|
99
|
+
existing_config["observability"] = [data]
|
|
100
|
+
elif section == "guardrails":
|
|
101
|
+
if isinstance(data, GuardrailsV2):
|
|
102
|
+
guardrails_dict = {
|
|
103
|
+
"input": [
|
|
104
|
+
g.model_dump(by_alias=False, mode="json")
|
|
105
|
+
for g in data.input
|
|
106
|
+
],
|
|
107
|
+
"output": [
|
|
108
|
+
g.model_dump(by_alias=False, mode="json")
|
|
109
|
+
for g in data.output
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
existing_config["guardrails"] = guardrails_dict
|
|
113
|
+
else:
|
|
114
|
+
existing_config["guardrails"] = data
|
|
115
|
+
elif section == "mcp_servers":
|
|
116
|
+
if isinstance(data, list):
|
|
117
|
+
mcp_servers_list = []
|
|
118
|
+
for server in data:
|
|
119
|
+
if hasattr(server, "model_dump"):
|
|
120
|
+
mcp_servers_list.append(
|
|
121
|
+
server.model_dump(
|
|
122
|
+
by_alias=True, mode="json", exclude_none=True
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
mcp_servers_list.append(server)
|
|
127
|
+
existing_config["mcp_servers"] = mcp_servers_list
|
|
128
|
+
else:
|
|
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"
|
|
152
|
+
else:
|
|
153
|
+
existing_config[section] = data
|
|
154
|
+
|
|
155
|
+
EngineConfig.model_validate(existing_config)
|
|
156
|
+
|
|
157
|
+
with self.agent_path.open("w") as f:
|
|
158
|
+
yaml.dump(existing_config, f, default_flow_style=False)
|
|
159
|
+
|
|
160
|
+
return True, "Saved successfully"
|
|
161
|
+
|
|
162
|
+
except ValidationError as e:
|
|
163
|
+
return False, f"Validation error: {e}"
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return False, f"Error saving config: {e}"
|
|
166
|
+
|
|
167
|
+
def load_config(self, agent_name: str = None) -> dict:
|
|
168
|
+
try:
|
|
169
|
+
if agent_name:
|
|
170
|
+
sanitized_agent_name = self._sanitize_agent_name(agent_name)
|
|
171
|
+
self.agent_path = (self.idun_dir / sanitized_agent_name).with_suffix(
|
|
172
|
+
".yaml"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if self.agent_path is None or not self.agent_path.exists():
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
with self.agent_path.open("r") as f:
|
|
179
|
+
return yaml.safe_load(f) or {}
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return {}
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Guardrails validation logic."""
|
|
2
|
+
|
|
3
|
+
from idun_agent_schema.engine.guardrails_v2 import (
|
|
4
|
+
GuardrailConfigId,
|
|
5
|
+
BiasCheckConfig,
|
|
6
|
+
ToxicLanguageConfig,
|
|
7
|
+
CompetitionCheckConfig,
|
|
8
|
+
BanListConfig,
|
|
9
|
+
DetectPIIConfig,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_guardrail(guardrail_id: str, config: dict) -> tuple[any, str]:
|
|
14
|
+
try:
|
|
15
|
+
match guardrail_id:
|
|
16
|
+
case "bias_check":
|
|
17
|
+
threshold = float(config.get("threshold", 0.5))
|
|
18
|
+
validated = BiasCheckConfig(
|
|
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
|
|
23
|
+
)
|
|
24
|
+
return validated, "ok"
|
|
25
|
+
|
|
26
|
+
case "toxic_language":
|
|
27
|
+
threshold = float(config.get("threshold", 0.5))
|
|
28
|
+
validated = ToxicLanguageConfig(
|
|
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
|
|
33
|
+
)
|
|
34
|
+
return validated, "ok"
|
|
35
|
+
|
|
36
|
+
case "competition_check":
|
|
37
|
+
competitors = config.get("competitors", [])
|
|
38
|
+
if isinstance(competitors, str):
|
|
39
|
+
competitors = [
|
|
40
|
+
c.strip() for c in competitors.split(",") if c.strip()
|
|
41
|
+
]
|
|
42
|
+
validated = CompetitionCheckConfig(
|
|
43
|
+
config_id=GuardrailConfigId.COMPETITION_CHECK,
|
|
44
|
+
api_key=config.get("api_key", ""),
|
|
45
|
+
reject_message=config.get("reject_message", "Competitor mentioned"),
|
|
46
|
+
competitors=competitors,
|
|
47
|
+
)
|
|
48
|
+
return validated, "ok"
|
|
49
|
+
|
|
50
|
+
case "ban_list":
|
|
51
|
+
banned_words = config.get("banned_words", [])
|
|
52
|
+
if isinstance(banned_words, str):
|
|
53
|
+
banned_words = [
|
|
54
|
+
w.strip() for w in banned_words.split(",") if w.strip()
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
validated = BanListConfig(
|
|
58
|
+
config_id=GuardrailConfigId.BAN_LIST,
|
|
59
|
+
api_key=config.get("api_key", ""),
|
|
60
|
+
reject_message=config.get("reject_message", ""),
|
|
61
|
+
guard_params={"banned_words": banned_words},
|
|
62
|
+
)
|
|
63
|
+
return validated, "ok"
|
|
64
|
+
|
|
65
|
+
case "detect_pii":
|
|
66
|
+
pii_entities = config.get("pii_entities", [])
|
|
67
|
+
if isinstance(pii_entities, str):
|
|
68
|
+
pii_entities = [
|
|
69
|
+
e.strip() for e in pii_entities.split(",") if e.strip()
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
validated = DetectPIIConfig(
|
|
73
|
+
config_id=GuardrailConfigId.DETECT_PII,
|
|
74
|
+
api_key=config.get("api_key", ""),
|
|
75
|
+
reject_message=config.get("reject_message", ""),
|
|
76
|
+
guard_params={"pii_entities": pii_entities, "on_fail": "exception"},
|
|
77
|
+
)
|
|
78
|
+
return validated, "ok"
|
|
79
|
+
|
|
80
|
+
case _:
|
|
81
|
+
return None, f"Unknown guardrail type: {guardrail_id}"
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
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}"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""MCPs validation logic."""
|
|
2
|
+
|
|
3
|
+
from idun_agent_schema.engine.mcp_server import MCPServer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_mcp_servers(
|
|
7
|
+
mcp_servers_data: list[dict],
|
|
8
|
+
) -> tuple[list[MCPServer] | None, str]:
|
|
9
|
+
if not mcp_servers_data:
|
|
10
|
+
return [], "ok"
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
validated_servers = []
|
|
14
|
+
seen_names = set()
|
|
15
|
+
|
|
16
|
+
for idx, server_data in enumerate(mcp_servers_data):
|
|
17
|
+
name = server_data.get("name", "")
|
|
18
|
+
if not name:
|
|
19
|
+
return None, f"Server {idx + 1}: Name is required"
|
|
20
|
+
|
|
21
|
+
if name in seen_names:
|
|
22
|
+
return None, f"Duplicate server name: {name}"
|
|
23
|
+
seen_names.add(name)
|
|
24
|
+
|
|
25
|
+
transport = server_data.get("transport", "streamable_http")
|
|
26
|
+
|
|
27
|
+
if transport == "stdio":
|
|
28
|
+
if not server_data.get("command"):
|
|
29
|
+
return (
|
|
30
|
+
None,
|
|
31
|
+
f"Server '{name}': command is required for stdio transport",
|
|
32
|
+
)
|
|
33
|
+
elif transport in ["sse", "streamable_http", "websocket"]:
|
|
34
|
+
if not server_data.get("url"):
|
|
35
|
+
return (
|
|
36
|
+
None,
|
|
37
|
+
f"Server '{name}': url is required for {transport} transport",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
args = server_data.get("args", [])
|
|
41
|
+
if isinstance(args, str):
|
|
42
|
+
args = [a.strip() for a in args.split("\n") if a.strip()]
|
|
43
|
+
|
|
44
|
+
headers = server_data.get("headers", {})
|
|
45
|
+
if isinstance(headers, str):
|
|
46
|
+
import json
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
headers = json.loads(headers) if headers.strip() else {}
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return None, f"Server '{name}': Invalid JSON for headers"
|
|
52
|
+
|
|
53
|
+
env = server_data.get("env", {})
|
|
54
|
+
if isinstance(env, str):
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
env = json.loads(env) if env.strip() else {}
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
return None, f"Server '{name}': Invalid JSON for env"
|
|
61
|
+
|
|
62
|
+
server_config = {
|
|
63
|
+
"name": name,
|
|
64
|
+
"transport": transport,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if server_data.get("url"):
|
|
68
|
+
server_config["url"] = server_data["url"]
|
|
69
|
+
if headers:
|
|
70
|
+
server_config["headers"] = headers
|
|
71
|
+
if server_data.get("command"):
|
|
72
|
+
server_config["command"] = server_data["command"]
|
|
73
|
+
if args:
|
|
74
|
+
server_config["args"] = args
|
|
75
|
+
if env:
|
|
76
|
+
server_config["env"] = env
|
|
77
|
+
|
|
78
|
+
validated_server = MCPServer.model_validate(server_config)
|
|
79
|
+
validated_servers.append(validated_server)
|
|
80
|
+
|
|
81
|
+
return validated_servers, "ok"
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return None, f"Validation error: {str(e)}"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from idun_agent_schema.engine.observability_v2 import (
|
|
2
|
+
ObservabilityConfig,
|
|
3
|
+
ObservabilityProvider,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_observability(
|
|
8
|
+
provider: ObservabilityProvider, config
|
|
9
|
+
) -> tuple[ObservabilityConfig | None, str]:
|
|
10
|
+
match provider:
|
|
11
|
+
case ObservabilityProvider.LANGFUSE:
|
|
12
|
+
from idun_agent_schema.engine.observability_v2 import LangfuseConfig
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
config = LangfuseConfig(**config)
|
|
16
|
+
return ObservabilityConfig(
|
|
17
|
+
provider=provider, config=config, enabled=True
|
|
18
|
+
), "ok"
|
|
19
|
+
except Exception as e:
|
|
20
|
+
return None, f"Error validating Langfuse config: {e}"
|
|
21
|
+
|
|
22
|
+
case ObservabilityProvider.PHOENIX:
|
|
23
|
+
from idun_agent_schema.engine.observability_v2 import PhoenixConfig
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
config = PhoenixConfig(**config)
|
|
27
|
+
return ObservabilityConfig(
|
|
28
|
+
provider=provider, config=config, enabled=True
|
|
29
|
+
), "ok"
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return None, f"Error validating Phoenix config: {e}"
|
|
32
|
+
|
|
33
|
+
case ObservabilityProvider.GCP_LOGGING:
|
|
34
|
+
from idun_agent_schema.engine.observability_v2 import GCPLoggingConfig
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
config = GCPLoggingConfig(**config)
|
|
38
|
+
return ObservabilityConfig(
|
|
39
|
+
provider=provider, config=config, enabled=True
|
|
40
|
+
), "ok"
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return None, f"Error validating GCP logging config: {e}"
|
|
43
|
+
|
|
44
|
+
case ObservabilityProvider.GCP_TRACE:
|
|
45
|
+
from idun_agent_schema.engine.observability_v2 import GCPTraceConfig
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
config = GCPTraceConfig(**config)
|
|
49
|
+
return ObservabilityConfig(
|
|
50
|
+
provider=provider, config=config, enabled=True
|
|
51
|
+
), "ok"
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return None, f"Error validating GCP trace config: {e}"
|
|
54
|
+
|
|
55
|
+
case ObservabilityProvider.LANGSMITH:
|
|
56
|
+
from idun_agent_schema.engine.observability_v2 import LangsmithConfig
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
config = LangsmithConfig(**config)
|
|
60
|
+
|
|
61
|
+
return ObservabilityConfig(
|
|
62
|
+
provider=provider, config=config, enabled=True
|
|
63
|
+
), "ok"
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return None, f"Error validating Langsmith config: {e}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Widget components for the agent configuration screens."""
|
|
2
|
+
|
|
3
|
+
from .chat_widget import ChatWidget
|
|
4
|
+
from .guardrails_widget import GuardrailsWidget
|
|
5
|
+
from .identity_widget import IdentityWidget
|
|
6
|
+
from .mcps_widget import MCPsWidget
|
|
7
|
+
from .memory_widget import MemoryWidget
|
|
8
|
+
from .observability_widget import ObservabilityWidget
|
|
9
|
+
from .serve_widget import ServeWidget
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ChatWidget",
|
|
13
|
+
"GuardrailsWidget",
|
|
14
|
+
"IdentityWidget",
|
|
15
|
+
"MCPsWidget",
|
|
16
|
+
"MemoryWidget",
|
|
17
|
+
"ObservabilityWidget",
|
|
18
|
+
"ServeWidget",
|
|
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
|