idun-agent-engine 0.3.9__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idun_agent_engine/_version.py +1 -1
- 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 +5 -6
- idun_agent_engine/guardrails/guardrails_hub/__init__.py +2 -2
- idun_agent_engine/mcp/__init__.py +18 -2
- idun_agent_engine/mcp/helpers.py +95 -45
- 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.0.dist-info}/METADATA +11 -7
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/RECORD +39 -14
- idun_platform_cli/groups/init.py +23 -0
- idun_platform_cli/main.py +3 -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 +789 -0
- idun_platform_cli/tui/css/main.py +92 -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 +482 -0
- idun_platform_cli/tui/utils/__init__.py +0 -0
- idun_platform_cli/tui/utils/config.py +161 -0
- idun_platform_cli/tui/validators/__init__.py +0 -0
- idun_platform_cli/tui/validators/guardrails.py +76 -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 +15 -0
- idun_platform_cli/tui/widgets/guardrails_widget.py +348 -0
- idun_platform_cli/tui/widgets/identity_widget.py +234 -0
- idun_platform_cli/tui/widgets/mcps_widget.py +230 -0
- idun_platform_cli/tui/widgets/observability_widget.py +384 -0
- idun_platform_cli/tui/widgets/serve_widget.py +78 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/WHEEL +0 -0
- {idun_agent_engine-0.3.9.dist-info → idun_agent_engine-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
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, threshold=threshold
|
|
20
|
+
)
|
|
21
|
+
return validated, "ok"
|
|
22
|
+
|
|
23
|
+
case "toxic_language":
|
|
24
|
+
threshold = float(config.get("threshold", 0.5))
|
|
25
|
+
validated = ToxicLanguageConfig(
|
|
26
|
+
config_id=GuardrailConfigId.TOXIC_LANGUAGE, threshold=threshold
|
|
27
|
+
)
|
|
28
|
+
return validated, "ok"
|
|
29
|
+
|
|
30
|
+
case "competition_check":
|
|
31
|
+
competitors = config.get("competitors", [])
|
|
32
|
+
if isinstance(competitors, str):
|
|
33
|
+
competitors = [
|
|
34
|
+
c.strip() for c in competitors.split(",") if c.strip()
|
|
35
|
+
]
|
|
36
|
+
validated = CompetitionCheckConfig(
|
|
37
|
+
config_id=GuardrailConfigId.COMPETITION_CHECK,
|
|
38
|
+
competitors=competitors,
|
|
39
|
+
)
|
|
40
|
+
return validated, "ok"
|
|
41
|
+
|
|
42
|
+
case "ban_list":
|
|
43
|
+
banned_words = config.get("banned_words", [])
|
|
44
|
+
if isinstance(banned_words, str):
|
|
45
|
+
banned_words = [
|
|
46
|
+
w.strip() for w in banned_words.split(",") if w.strip()
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
validated = BanListConfig(
|
|
50
|
+
config_id=GuardrailConfigId.BAN_LIST,
|
|
51
|
+
api_key=config.get("api_key", ""),
|
|
52
|
+
reject_message=config.get("reject_message", ""),
|
|
53
|
+
guard_params={"banned_words": banned_words},
|
|
54
|
+
)
|
|
55
|
+
return validated, "ok"
|
|
56
|
+
|
|
57
|
+
case "detect_pii":
|
|
58
|
+
pii_entities = config.get("pii_entities", [])
|
|
59
|
+
if isinstance(pii_entities, str):
|
|
60
|
+
pii_entities = [
|
|
61
|
+
e.strip() for e in pii_entities.split(",") if e.strip()
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
validated = DetectPIIConfig(
|
|
65
|
+
config_id=GuardrailConfigId.DETECT_PII,
|
|
66
|
+
api_key=config.get("api_key", ""),
|
|
67
|
+
reject_message=config.get("reject_message", ""),
|
|
68
|
+
guard_params={"pii_entities": pii_entities, "on_fail": "exception"},
|
|
69
|
+
)
|
|
70
|
+
return validated, "ok"
|
|
71
|
+
|
|
72
|
+
case _:
|
|
73
|
+
return None, f"Unknown guardrail type: {guardrail_id}"
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return None, f"Validation error for {guardrail_id}: {str(e)}"
|
|
@@ -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,15 @@
|
|
|
1
|
+
"""Widget components for the agent configuration screens."""
|
|
2
|
+
|
|
3
|
+
from .identity_widget import IdentityWidget
|
|
4
|
+
from .observability_widget import ObservabilityWidget
|
|
5
|
+
from .guardrails_widget import GuardrailsWidget
|
|
6
|
+
from .mcps_widget import MCPsWidget
|
|
7
|
+
from .serve_widget import ServeWidget
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"IdentityWidget",
|
|
11
|
+
"ObservabilityWidget",
|
|
12
|
+
"GuardrailsWidget",
|
|
13
|
+
"MCPsWidget",
|
|
14
|
+
"ServeWidget",
|
|
15
|
+
]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Guardrails configuration widget."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical, Grid
|
|
5
|
+
from textual.widgets import Static, Input, Switch, RadioSet, RadioButton, TextArea
|
|
6
|
+
from textual.widget import Widget
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.guardrails_v2 import GuardrailsV2
|
|
9
|
+
from idun_platform_cli.tui.validators.guardrails import validate_guardrail
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GuardrailsWidget(Widget):
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.guardrails_data = {
|
|
16
|
+
"bias_check": {"enabled": False, "applies_to": "both", "config": {}},
|
|
17
|
+
"toxic_language": {"enabled": False, "applies_to": "both", "config": {}},
|
|
18
|
+
"competition_check": {"enabled": False, "applies_to": "both", "config": {}},
|
|
19
|
+
"ban_list": {"enabled": False, "applies_to": "both", "config": {}},
|
|
20
|
+
"detect_pii": {"enabled": False, "applies_to": "both", "config": {}},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def compose(self) -> ComposeResult:
|
|
24
|
+
api_key_section = Vertical(classes="global-api-key-section")
|
|
25
|
+
api_key_section.border_title = "Global API Key"
|
|
26
|
+
api_key_section.compose_add_child(
|
|
27
|
+
Static(
|
|
28
|
+
"API Key (for guardrails that require it):", classes="gr-label-small"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
api_key_section.compose_add_child(
|
|
32
|
+
Input(
|
|
33
|
+
placeholder="Enter API key",
|
|
34
|
+
password=True,
|
|
35
|
+
id="global_guardrails_api_key",
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
yield api_key_section
|
|
39
|
+
|
|
40
|
+
grid = Grid(classes="guardrails-grid")
|
|
41
|
+
for card in self._create_all_cards():
|
|
42
|
+
grid.compose_add_child(card)
|
|
43
|
+
yield grid
|
|
44
|
+
|
|
45
|
+
def _create_all_cards(self):
|
|
46
|
+
return [
|
|
47
|
+
self._create_bias_check_card(),
|
|
48
|
+
self._create_toxic_language_card(),
|
|
49
|
+
self._create_competition_check_card(),
|
|
50
|
+
self._create_ban_list_card(),
|
|
51
|
+
self._create_detect_pii_card(),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
def _create_bias_check_card(self) -> Vertical:
|
|
55
|
+
card = Vertical(classes="guardrail-card", id="bias_check_card")
|
|
56
|
+
card.border_title = "Bias Check"
|
|
57
|
+
|
|
58
|
+
header = Horizontal(classes="guardrail-header")
|
|
59
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
60
|
+
header.compose_add_child(Switch(value=False, id="bias_check_enabled"))
|
|
61
|
+
card.compose_add_child(header)
|
|
62
|
+
|
|
63
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
64
|
+
applies_section.compose_add_child(
|
|
65
|
+
Static("Applies To:", classes="gr-label-small")
|
|
66
|
+
)
|
|
67
|
+
radio_set = RadioSet(id="bias_check_applies_to")
|
|
68
|
+
radio_set.compose_add_child(RadioButton("Input", id="bias_check_input"))
|
|
69
|
+
radio_set.compose_add_child(RadioButton("Output", id="bias_check_output"))
|
|
70
|
+
radio_set.compose_add_child(
|
|
71
|
+
RadioButton("Both", value=True, id="bias_check_both")
|
|
72
|
+
)
|
|
73
|
+
applies_section.compose_add_child(radio_set)
|
|
74
|
+
card.compose_add_child(applies_section)
|
|
75
|
+
|
|
76
|
+
config_section = Vertical(classes="config-section", id="bias_check_config")
|
|
77
|
+
config_section.display = False
|
|
78
|
+
config_section.compose_add_child(
|
|
79
|
+
Static("Threshold (0.0-1.0):", classes="gr-label-small")
|
|
80
|
+
)
|
|
81
|
+
config_section.compose_add_child(
|
|
82
|
+
Input(placeholder="0.5", id="bias_check_threshold")
|
|
83
|
+
)
|
|
84
|
+
card.compose_add_child(config_section)
|
|
85
|
+
|
|
86
|
+
return card
|
|
87
|
+
|
|
88
|
+
def _create_toxic_language_card(self) -> Vertical:
|
|
89
|
+
card = Vertical(classes="guardrail-card", id="toxic_language_card")
|
|
90
|
+
card.border_title = "Toxic Language"
|
|
91
|
+
|
|
92
|
+
header = Horizontal(classes="guardrail-header")
|
|
93
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
94
|
+
header.compose_add_child(Switch(value=False, id="toxic_language_enabled"))
|
|
95
|
+
card.compose_add_child(header)
|
|
96
|
+
|
|
97
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
98
|
+
applies_section.compose_add_child(
|
|
99
|
+
Static("Applies To:", classes="gr-label-small")
|
|
100
|
+
)
|
|
101
|
+
radio_set = RadioSet(id="toxic_language_applies_to")
|
|
102
|
+
radio_set.compose_add_child(RadioButton("Input", id="toxic_language_input"))
|
|
103
|
+
radio_set.compose_add_child(RadioButton("Output", id="toxic_language_output"))
|
|
104
|
+
radio_set.compose_add_child(
|
|
105
|
+
RadioButton("Both", value=True, id="toxic_language_both")
|
|
106
|
+
)
|
|
107
|
+
applies_section.compose_add_child(radio_set)
|
|
108
|
+
card.compose_add_child(applies_section)
|
|
109
|
+
|
|
110
|
+
config_section = Vertical(classes="config-section", id="toxic_language_config")
|
|
111
|
+
config_section.display = False
|
|
112
|
+
config_section.compose_add_child(
|
|
113
|
+
Static("Threshold (0.0-1.0):", classes="gr-label-small")
|
|
114
|
+
)
|
|
115
|
+
config_section.compose_add_child(
|
|
116
|
+
Input(placeholder="0.5", id="toxic_language_threshold")
|
|
117
|
+
)
|
|
118
|
+
card.compose_add_child(config_section)
|
|
119
|
+
|
|
120
|
+
return card
|
|
121
|
+
|
|
122
|
+
def _create_competition_check_card(self) -> Vertical:
|
|
123
|
+
card = Vertical(classes="guardrail-card", id="competition_check_card")
|
|
124
|
+
card.border_title = "Competition Check"
|
|
125
|
+
|
|
126
|
+
header = Horizontal(classes="guardrail-header")
|
|
127
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
128
|
+
header.compose_add_child(Switch(value=False, id="competition_check_enabled"))
|
|
129
|
+
card.compose_add_child(header)
|
|
130
|
+
|
|
131
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
132
|
+
applies_section.compose_add_child(
|
|
133
|
+
Static("Applies To:", classes="gr-label-small")
|
|
134
|
+
)
|
|
135
|
+
radio_set = RadioSet(id="competition_check_applies_to")
|
|
136
|
+
radio_set.compose_add_child(RadioButton("Input", id="competition_check_input"))
|
|
137
|
+
radio_set.compose_add_child(
|
|
138
|
+
RadioButton("Output", id="competition_check_output")
|
|
139
|
+
)
|
|
140
|
+
radio_set.compose_add_child(
|
|
141
|
+
RadioButton("Both", value=True, id="competition_check_both")
|
|
142
|
+
)
|
|
143
|
+
applies_section.compose_add_child(radio_set)
|
|
144
|
+
card.compose_add_child(applies_section)
|
|
145
|
+
|
|
146
|
+
config_section = Vertical(
|
|
147
|
+
classes="config-section", id="competition_check_config"
|
|
148
|
+
)
|
|
149
|
+
config_section.display = False
|
|
150
|
+
config_section.compose_add_child(
|
|
151
|
+
Static("Competitors (comma-separated):", classes="gr-label-small")
|
|
152
|
+
)
|
|
153
|
+
competitors_textarea = TextArea(
|
|
154
|
+
id="competition_check_competitors", classes="gr-textarea"
|
|
155
|
+
)
|
|
156
|
+
competitors_textarea.placeholder = (
|
|
157
|
+
"e.g., Competitor-1, Competitor-2, Competitor-3..."
|
|
158
|
+
)
|
|
159
|
+
config_section.compose_add_child(competitors_textarea)
|
|
160
|
+
card.compose_add_child(config_section)
|
|
161
|
+
|
|
162
|
+
return card
|
|
163
|
+
|
|
164
|
+
def _create_ban_list_card(self) -> Vertical:
|
|
165
|
+
card = Vertical(classes="guardrail-card", id="ban_list_card")
|
|
166
|
+
card.border_title = "Ban List"
|
|
167
|
+
|
|
168
|
+
header = Horizontal(classes="guardrail-header")
|
|
169
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
170
|
+
header.compose_add_child(Switch(value=False, id="ban_list_enabled"))
|
|
171
|
+
card.compose_add_child(header)
|
|
172
|
+
|
|
173
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
174
|
+
applies_section.compose_add_child(
|
|
175
|
+
Static("Applies To:", classes="gr-label-small")
|
|
176
|
+
)
|
|
177
|
+
radio_set = RadioSet(id="ban_list_applies_to")
|
|
178
|
+
radio_set.compose_add_child(RadioButton("Input", id="ban_list_input"))
|
|
179
|
+
radio_set.compose_add_child(RadioButton("Output", id="ban_list_output"))
|
|
180
|
+
radio_set.compose_add_child(RadioButton("Both", value=True, id="ban_list_both"))
|
|
181
|
+
applies_section.compose_add_child(radio_set)
|
|
182
|
+
card.compose_add_child(applies_section)
|
|
183
|
+
|
|
184
|
+
config_section = Vertical(classes="config-section", id="ban_list_config")
|
|
185
|
+
config_section.display = False
|
|
186
|
+
config_section.compose_add_child(
|
|
187
|
+
Static("Reject Message:", classes="gr-label-small")
|
|
188
|
+
)
|
|
189
|
+
config_section.compose_add_child(
|
|
190
|
+
Input(placeholder="Message to show", id="ban_list_reject_message")
|
|
191
|
+
)
|
|
192
|
+
config_section.compose_add_child(
|
|
193
|
+
Static("Banned Words (comma-separated):", classes="gr-label-small")
|
|
194
|
+
)
|
|
195
|
+
banned_words_textarea = TextArea(
|
|
196
|
+
id="ban_list_banned_words", classes="gr-textarea"
|
|
197
|
+
)
|
|
198
|
+
banned_words_textarea.placeholder = "Words to ban..."
|
|
199
|
+
config_section.compose_add_child(banned_words_textarea)
|
|
200
|
+
card.compose_add_child(config_section)
|
|
201
|
+
|
|
202
|
+
return card
|
|
203
|
+
|
|
204
|
+
def _create_detect_pii_card(self) -> Vertical:
|
|
205
|
+
card = Vertical(classes="guardrail-card", id="detect_pii_card")
|
|
206
|
+
card.border_title = "Detect PII"
|
|
207
|
+
|
|
208
|
+
header = Horizontal(classes="guardrail-header")
|
|
209
|
+
header.compose_add_child(Static("Enabled:", classes="gr-label"))
|
|
210
|
+
header.compose_add_child(Switch(value=False, id="detect_pii_enabled"))
|
|
211
|
+
card.compose_add_child(header)
|
|
212
|
+
|
|
213
|
+
applies_section = Vertical(classes="applies-to-section")
|
|
214
|
+
applies_section.compose_add_child(
|
|
215
|
+
Static("Applies To:", classes="gr-label-small")
|
|
216
|
+
)
|
|
217
|
+
radio_set = RadioSet(id="detect_pii_applies_to")
|
|
218
|
+
radio_set.compose_add_child(RadioButton("Input", id="detect_pii_input"))
|
|
219
|
+
radio_set.compose_add_child(RadioButton("Output", id="detect_pii_output"))
|
|
220
|
+
radio_set.compose_add_child(
|
|
221
|
+
RadioButton("Both", value=True, id="detect_pii_both")
|
|
222
|
+
)
|
|
223
|
+
applies_section.compose_add_child(radio_set)
|
|
224
|
+
card.compose_add_child(applies_section)
|
|
225
|
+
|
|
226
|
+
config_section = Vertical(classes="config-section", id="detect_pii_config")
|
|
227
|
+
config_section.display = False
|
|
228
|
+
config_section.compose_add_child(
|
|
229
|
+
Static("Reject Message:", classes="gr-label-small")
|
|
230
|
+
)
|
|
231
|
+
config_section.compose_add_child(
|
|
232
|
+
Input(placeholder="Message to show", id="detect_pii_reject_message")
|
|
233
|
+
)
|
|
234
|
+
config_section.compose_add_child(
|
|
235
|
+
Static("PII Entities (comma-separated):", classes="gr-label-small")
|
|
236
|
+
)
|
|
237
|
+
pii_entities_textarea = TextArea(
|
|
238
|
+
id="detect_pii_pii_entities", classes="gr-textarea"
|
|
239
|
+
)
|
|
240
|
+
pii_entities_textarea.placeholder = "e.g., EMAIL_ADDRESS, PHONE_NUMBER, SSN..."
|
|
241
|
+
config_section.compose_add_child(pii_entities_textarea)
|
|
242
|
+
card.compose_add_child(config_section)
|
|
243
|
+
|
|
244
|
+
return card
|
|
245
|
+
|
|
246
|
+
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
247
|
+
switch_id = event.switch.id
|
|
248
|
+
if not switch_id:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
guardrail_type = switch_id.replace("_enabled", "")
|
|
252
|
+
config_section_id = f"{guardrail_type}_config"
|
|
253
|
+
card_id = f"{guardrail_type}_card"
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
config_section = self.query_one(f"#{config_section_id}")
|
|
257
|
+
card = self.query_one(f"#{card_id}")
|
|
258
|
+
|
|
259
|
+
config_section.display = event.value
|
|
260
|
+
self.guardrails_data[guardrail_type]["enabled"] = event.value
|
|
261
|
+
|
|
262
|
+
if event.value:
|
|
263
|
+
card.add_class("guardrail-card-enabled")
|
|
264
|
+
else:
|
|
265
|
+
card.remove_class("guardrail-card-enabled")
|
|
266
|
+
except:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
def get_data(self) -> GuardrailsV2 | None:
|
|
270
|
+
input_guardrails = []
|
|
271
|
+
output_guardrails = []
|
|
272
|
+
|
|
273
|
+
for guardrail_id in self.guardrails_data.keys():
|
|
274
|
+
enabled_switch = self.query_one(f"#{guardrail_id}_enabled", Switch)
|
|
275
|
+
if not enabled_switch.value:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
applies_to_radioset = self.query_one(
|
|
279
|
+
f"#{guardrail_id}_applies_to", RadioSet
|
|
280
|
+
)
|
|
281
|
+
pressed_button = applies_to_radioset.pressed_button
|
|
282
|
+
if not pressed_button:
|
|
283
|
+
applies_to = "both"
|
|
284
|
+
else:
|
|
285
|
+
button_id = str(pressed_button.id)
|
|
286
|
+
if "input" in button_id:
|
|
287
|
+
applies_to = "input"
|
|
288
|
+
elif "output" in button_id:
|
|
289
|
+
applies_to = "output"
|
|
290
|
+
else:
|
|
291
|
+
applies_to = "both"
|
|
292
|
+
|
|
293
|
+
config_dict = self._extract_config(guardrail_id)
|
|
294
|
+
validated_config, msg = validate_guardrail(guardrail_id, config_dict)
|
|
295
|
+
|
|
296
|
+
if not validated_config:
|
|
297
|
+
self.app.notify(
|
|
298
|
+
f"Validation error for {guardrail_id}: {msg}", severity="error"
|
|
299
|
+
)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
if applies_to in ["input", "both"]:
|
|
303
|
+
input_guardrails.append(validated_config)
|
|
304
|
+
if applies_to in ["output", "both"]:
|
|
305
|
+
output_guardrails.append(validated_config)
|
|
306
|
+
|
|
307
|
+
return GuardrailsV2(input=input_guardrails, output=output_guardrails)
|
|
308
|
+
|
|
309
|
+
def _extract_config(self, guardrail_id: str) -> dict:
|
|
310
|
+
config = {}
|
|
311
|
+
global_api_key = self.query_one("#global_guardrails_api_key", Input).value
|
|
312
|
+
|
|
313
|
+
match guardrail_id:
|
|
314
|
+
case "bias_check":
|
|
315
|
+
threshold_input = self.query_one("#bias_check_threshold", Input)
|
|
316
|
+
config["threshold"] = threshold_input.value or "0.5"
|
|
317
|
+
|
|
318
|
+
case "toxic_language":
|
|
319
|
+
threshold_input = self.query_one("#toxic_language_threshold", Input)
|
|
320
|
+
config["threshold"] = threshold_input.value or "0.5"
|
|
321
|
+
|
|
322
|
+
case "competition_check":
|
|
323
|
+
competitors_textarea = self.query_one(
|
|
324
|
+
"#competition_check_competitors", TextArea
|
|
325
|
+
)
|
|
326
|
+
config["competitors"] = competitors_textarea.text
|
|
327
|
+
|
|
328
|
+
case "ban_list":
|
|
329
|
+
config["api_key"] = global_api_key
|
|
330
|
+
config["reject_message"] = self.query_one(
|
|
331
|
+
"#ban_list_reject_message", Input
|
|
332
|
+
).value
|
|
333
|
+
banned_words_textarea = self.query_one(
|
|
334
|
+
"#ban_list_banned_words", TextArea
|
|
335
|
+
)
|
|
336
|
+
config["banned_words"] = banned_words_textarea.text
|
|
337
|
+
|
|
338
|
+
case "detect_pii":
|
|
339
|
+
config["api_key"] = global_api_key
|
|
340
|
+
config["reject_message"] = self.query_one(
|
|
341
|
+
"#detect_pii_reject_message", Input
|
|
342
|
+
).value
|
|
343
|
+
pii_entities_textarea = self.query_one(
|
|
344
|
+
"#detect_pii_pii_entities", TextArea
|
|
345
|
+
)
|
|
346
|
+
config["pii_entities"] = pii_entities_textarea.text
|
|
347
|
+
|
|
348
|
+
return config
|