ag2 0.9.9__py3-none-any.whl → 0.9.10__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.
Potentially problematic release.
This version of ag2 might be problematic. Click here for more details.
- {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/METADATA +232 -210
- {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/RECORD +88 -80
- autogen/_website/generate_mkdocs.py +3 -3
- autogen/_website/notebook_processor.py +1 -1
- autogen/_website/utils.py +1 -1
- autogen/agentchat/assistant_agent.py +15 -15
- autogen/agentchat/chat.py +52 -40
- autogen/agentchat/contrib/agent_eval/criterion.py +1 -1
- autogen/agentchat/contrib/capabilities/text_compressors.py +5 -5
- autogen/agentchat/contrib/capabilities/tools_capability.py +1 -1
- autogen/agentchat/contrib/capabilities/transforms.py +1 -1
- autogen/agentchat/contrib/captainagent/agent_builder.py +1 -1
- autogen/agentchat/contrib/captainagent/captainagent.py +20 -19
- autogen/agentchat/contrib/graph_rag/falkor_graph_query_engine.py +2 -5
- autogen/agentchat/contrib/graph_rag/graph_rag_capability.py +5 -5
- autogen/agentchat/contrib/graph_rag/neo4j_graph_query_engine.py +18 -17
- autogen/agentchat/contrib/rag/mongodb_query_engine.py +2 -2
- autogen/agentchat/contrib/rag/query_engine.py +11 -11
- autogen/agentchat/contrib/retrieve_assistant_agent.py +3 -0
- autogen/agentchat/contrib/swarm_agent.py +3 -2
- autogen/agentchat/contrib/vectordb/couchbase.py +1 -1
- autogen/agentchat/contrib/vectordb/mongodb.py +1 -1
- autogen/agentchat/contrib/web_surfer.py +1 -1
- autogen/agentchat/conversable_agent.py +184 -80
- autogen/agentchat/group/context_expression.py +21 -21
- autogen/agentchat/group/handoffs.py +11 -11
- autogen/agentchat/group/multi_agent_chat.py +3 -2
- autogen/agentchat/group/on_condition.py +11 -11
- autogen/agentchat/group/safeguards/__init__.py +21 -0
- autogen/agentchat/group/safeguards/api.py +224 -0
- autogen/agentchat/group/safeguards/enforcer.py +1064 -0
- autogen/agentchat/group/safeguards/events.py +119 -0
- autogen/agentchat/group/safeguards/validator.py +435 -0
- autogen/agentchat/groupchat.py +58 -17
- autogen/agentchat/realtime/experimental/clients/realtime_client.py +2 -2
- autogen/agentchat/realtime/experimental/function_observer.py +2 -3
- autogen/agentchat/realtime/experimental/realtime_agent.py +2 -3
- autogen/agentchat/realtime/experimental/realtime_swarm.py +21 -10
- autogen/agentchat/user_proxy_agent.py +55 -53
- autogen/agents/experimental/document_agent/document_agent.py +1 -10
- autogen/agents/experimental/document_agent/parser_utils.py +5 -1
- autogen/browser_utils.py +4 -4
- autogen/cache/abstract_cache_base.py +2 -6
- autogen/cache/disk_cache.py +1 -6
- autogen/cache/in_memory_cache.py +2 -6
- autogen/cache/redis_cache.py +1 -5
- autogen/coding/__init__.py +10 -2
- autogen/coding/base.py +2 -1
- autogen/coding/docker_commandline_code_executor.py +1 -6
- autogen/coding/factory.py +9 -0
- autogen/coding/jupyter/docker_jupyter_server.py +1 -7
- autogen/coding/jupyter/jupyter_client.py +2 -9
- autogen/coding/jupyter/jupyter_code_executor.py +2 -7
- autogen/coding/jupyter/local_jupyter_server.py +2 -6
- autogen/coding/local_commandline_code_executor.py +0 -65
- autogen/coding/yepcode_code_executor.py +197 -0
- autogen/environments/docker_python_environment.py +3 -3
- autogen/environments/system_python_environment.py +5 -5
- autogen/environments/venv_python_environment.py +5 -5
- autogen/events/agent_events.py +1 -1
- autogen/events/client_events.py +1 -1
- autogen/fast_depends/utils.py +10 -0
- autogen/graph_utils.py +5 -7
- autogen/import_utils.py +3 -1
- autogen/interop/pydantic_ai/pydantic_ai.py +8 -5
- autogen/io/processors/console_event_processor.py +8 -3
- autogen/llm_config/config.py +168 -91
- autogen/llm_config/entry.py +38 -26
- autogen/llm_config/types.py +35 -0
- autogen/llm_config/utils.py +223 -0
- autogen/mcp/mcp_proxy/operation_grouping.py +48 -39
- autogen/messages/agent_messages.py +1 -1
- autogen/messages/client_messages.py +1 -1
- autogen/oai/__init__.py +8 -1
- autogen/oai/client.py +10 -3
- autogen/oai/client_utils.py +1 -1
- autogen/oai/cohere.py +4 -4
- autogen/oai/gemini.py +4 -6
- autogen/oai/gemini_types.py +1 -0
- autogen/oai/openai_utils.py +44 -115
- autogen/tools/dependency_injection.py +4 -8
- autogen/tools/experimental/reliable/reliable.py +3 -2
- autogen/tools/experimental/web_search_preview/web_search_preview.py +1 -1
- autogen/tools/function_utils.py +2 -1
- autogen/version.py +1 -1
- {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/WHEEL +0 -0
- {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/licenses/LICENSE +0 -0
- {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from termcolor import colored
|
|
12
|
+
|
|
13
|
+
from ....events.base_event import BaseEvent, wrap_event
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@wrap_event
|
|
17
|
+
class SafeguardEvent(BaseEvent):
|
|
18
|
+
"""Event for safeguard actions"""
|
|
19
|
+
|
|
20
|
+
event_type: str # e.g., "load", "check", "violation", "action"
|
|
21
|
+
message: str
|
|
22
|
+
source_agent: str | None = None
|
|
23
|
+
target_agent: str | None = None
|
|
24
|
+
guardrail_type: str | None = None
|
|
25
|
+
action: str | None = None
|
|
26
|
+
content_preview: str | None = None
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
uuid: UUID | None = None,
|
|
32
|
+
event_type: str,
|
|
33
|
+
message: str,
|
|
34
|
+
source_agent: str | None = None,
|
|
35
|
+
target_agent: str | None = None,
|
|
36
|
+
guardrail_type: str | None = None,
|
|
37
|
+
action: str | None = None,
|
|
38
|
+
content_preview: str | None = None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(
|
|
41
|
+
uuid=uuid,
|
|
42
|
+
event_type=event_type,
|
|
43
|
+
message=message,
|
|
44
|
+
source_agent=source_agent,
|
|
45
|
+
target_agent=target_agent,
|
|
46
|
+
guardrail_type=guardrail_type,
|
|
47
|
+
action=action,
|
|
48
|
+
content_preview=content_preview,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def print(self, f: Callable[..., Any] | None = None) -> None:
|
|
52
|
+
f = f or print
|
|
53
|
+
|
|
54
|
+
# Choose color based on event type
|
|
55
|
+
color = "green"
|
|
56
|
+
if self.event_type == "load":
|
|
57
|
+
color = "green"
|
|
58
|
+
elif self.event_type == "check":
|
|
59
|
+
color = "cyan"
|
|
60
|
+
elif self.event_type == "violation":
|
|
61
|
+
color = "red"
|
|
62
|
+
elif self.event_type == "action":
|
|
63
|
+
color = "yellow"
|
|
64
|
+
|
|
65
|
+
# Choose emoji based on event type
|
|
66
|
+
emoji = ""
|
|
67
|
+
if self.event_type == "load":
|
|
68
|
+
emoji = "✅"
|
|
69
|
+
elif self.event_type == "check":
|
|
70
|
+
emoji = "🔍"
|
|
71
|
+
elif self.event_type == "violation":
|
|
72
|
+
emoji = "🛡️"
|
|
73
|
+
elif self.event_type == "action":
|
|
74
|
+
if self.action == "block":
|
|
75
|
+
emoji = "🚨"
|
|
76
|
+
elif self.action == "mask":
|
|
77
|
+
emoji = "🎭"
|
|
78
|
+
elif self.action == "warning":
|
|
79
|
+
emoji = "⚠️"
|
|
80
|
+
else:
|
|
81
|
+
emoji = "⚙️"
|
|
82
|
+
|
|
83
|
+
# Create header based on event type (skip for load events)
|
|
84
|
+
if self.event_type == "check":
|
|
85
|
+
header = f"***** Safeguard Check: {self.message} *****"
|
|
86
|
+
f(colored(header, color), flush=True)
|
|
87
|
+
elif self.event_type == "violation":
|
|
88
|
+
header = "***** Safeguard Violation: DETECTED *****"
|
|
89
|
+
f(colored(header, color), flush=True)
|
|
90
|
+
elif self.event_type == "action":
|
|
91
|
+
header = f"***** Safeguard Enforcement Action: {self.action.upper() if self.action else 'APPLIED'} *****"
|
|
92
|
+
f(colored(header, color), flush=True)
|
|
93
|
+
|
|
94
|
+
# Format the output
|
|
95
|
+
output_parts = [f"{emoji} {self.message}" if emoji else self.message]
|
|
96
|
+
|
|
97
|
+
if self.source_agent and self.target_agent:
|
|
98
|
+
output_parts.append(f" • From: {self.source_agent}")
|
|
99
|
+
output_parts.append(f" • To: {self.target_agent}")
|
|
100
|
+
elif self.source_agent:
|
|
101
|
+
output_parts.append(f" • Agent: {self.source_agent}")
|
|
102
|
+
|
|
103
|
+
if self.guardrail_type:
|
|
104
|
+
output_parts.append(f" • Guardrail: {self.guardrail_type}")
|
|
105
|
+
|
|
106
|
+
if self.action:
|
|
107
|
+
output_parts.append(f" • Action: {self.action}")
|
|
108
|
+
|
|
109
|
+
if self.content_preview:
|
|
110
|
+
# Replace actual newlines with \n for display
|
|
111
|
+
content_display = self.content_preview.replace("\n", "\\n").replace("\r", "\\r")
|
|
112
|
+
output_parts.append(f" • Content: {content_display}")
|
|
113
|
+
|
|
114
|
+
f(colored("\n".join(output_parts), color), flush=True)
|
|
115
|
+
|
|
116
|
+
# Print footer with matching length (skip for load events)
|
|
117
|
+
if self.event_type in ["check", "violation", "action"]:
|
|
118
|
+
footer = "*" * len(header)
|
|
119
|
+
f(colored(footer, color), flush=True)
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SafeguardValidator:
|
|
12
|
+
"""Validator for safeguard policy format and content."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, policy: dict[str, Any]):
|
|
15
|
+
"""Initialize validator with policy.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
policy: The safeguard policy to validate
|
|
19
|
+
"""
|
|
20
|
+
self.policy = policy
|
|
21
|
+
|
|
22
|
+
def validate_policy_structure(self) -> None:
|
|
23
|
+
"""Validate policy format and syntax only."""
|
|
24
|
+
if not isinstance(self.policy, dict):
|
|
25
|
+
raise ValueError("Policy must be a dictionary")
|
|
26
|
+
|
|
27
|
+
# Validate inter-agent safeguards
|
|
28
|
+
if "inter_agent_safeguards" in self.policy:
|
|
29
|
+
self._validate_inter_agent_safeguards()
|
|
30
|
+
|
|
31
|
+
# Validate environment safeguards
|
|
32
|
+
if "agent_environment_safeguards" in self.policy:
|
|
33
|
+
self._validate_environment_safeguards()
|
|
34
|
+
|
|
35
|
+
def validate_policy_complete(self, agent_names: list[str], agent_tool_mapping: dict[str, list[str]]) -> None:
|
|
36
|
+
"""Validate agent and tool names (assumes policy structure already validated).
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
agent_names: List of available agent names for validation
|
|
40
|
+
agent_tool_mapping: Mapping of agent names to their tool names
|
|
41
|
+
"""
|
|
42
|
+
# Validate agent names
|
|
43
|
+
self.validate_agent_names(agent_names)
|
|
44
|
+
|
|
45
|
+
# Validate tool names if any tools exist
|
|
46
|
+
if any(tools for tools in agent_tool_mapping.values()):
|
|
47
|
+
self.validate_tool_names(agent_tool_mapping, agent_names)
|
|
48
|
+
|
|
49
|
+
def _validate_inter_agent_safeguards(self) -> None:
|
|
50
|
+
"""Validate inter-agent safeguards section."""
|
|
51
|
+
inter_agent = self.policy["inter_agent_safeguards"]
|
|
52
|
+
if not isinstance(inter_agent, dict):
|
|
53
|
+
raise ValueError("inter_agent_safeguards must be a dictionary")
|
|
54
|
+
|
|
55
|
+
# Validate agent_transitions
|
|
56
|
+
if "agent_transitions" in inter_agent:
|
|
57
|
+
if not isinstance(inter_agent["agent_transitions"], list):
|
|
58
|
+
raise ValueError("agent_transitions must be a list")
|
|
59
|
+
|
|
60
|
+
for i, rule in enumerate(inter_agent["agent_transitions"]):
|
|
61
|
+
if not isinstance(rule, dict):
|
|
62
|
+
raise ValueError(f"agent_transitions[{i}] must be a dictionary")
|
|
63
|
+
|
|
64
|
+
# Required fields
|
|
65
|
+
required_fields = ["message_source", "message_destination"]
|
|
66
|
+
for field in required_fields:
|
|
67
|
+
if field not in rule:
|
|
68
|
+
raise ValueError(f"agent_transitions[{i}] missing required field: {field}")
|
|
69
|
+
|
|
70
|
+
# Check method validation - no default, must be explicit
|
|
71
|
+
if "check_method" not in rule:
|
|
72
|
+
raise ValueError(f"agent_transitions[{i}] missing required field: check_method")
|
|
73
|
+
check_method = rule["check_method"]
|
|
74
|
+
if check_method not in ["llm", "regex"]:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"agent_transitions[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# LLM-specific validation
|
|
80
|
+
if check_method == "llm":
|
|
81
|
+
if "custom_prompt" not in rule and "disallow_item" not in rule:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"agent_transitions[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
|
|
84
|
+
)
|
|
85
|
+
if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
|
|
86
|
+
raise ValueError(f"agent_transitions[{i}] disallow_item must be a list")
|
|
87
|
+
|
|
88
|
+
# Regex-specific validation
|
|
89
|
+
if check_method == "regex":
|
|
90
|
+
if "pattern" not in rule:
|
|
91
|
+
raise ValueError(f"agent_transitions[{i}] with check_method 'regex' must have 'pattern'")
|
|
92
|
+
if not isinstance(rule["pattern"], str):
|
|
93
|
+
raise ValueError(f"agent_transitions[{i}] pattern must be a string")
|
|
94
|
+
|
|
95
|
+
# Test regex pattern validity
|
|
96
|
+
try:
|
|
97
|
+
re.compile(rule["pattern"])
|
|
98
|
+
except re.error as e:
|
|
99
|
+
raise ValueError(f"agent_transitions[{i}] invalid regex pattern '{rule['pattern']}': {e}")
|
|
100
|
+
|
|
101
|
+
# Validate action - no default, must be explicit
|
|
102
|
+
if "violation_response" not in rule and "action" not in rule:
|
|
103
|
+
raise ValueError(f"agent_transitions[{i}] missing required field: violation_response or action")
|
|
104
|
+
action = rule.get("violation_response", rule.get("action"))
|
|
105
|
+
if action not in ["block", "mask", "warning"]:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"agent_transitions[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Validate groupchat_message_check
|
|
111
|
+
if "groupchat_message_check" in inter_agent:
|
|
112
|
+
rule = inter_agent["groupchat_message_check"]
|
|
113
|
+
if not isinstance(rule, dict):
|
|
114
|
+
raise ValueError("groupchat_message_check must be a dictionary")
|
|
115
|
+
if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
|
|
116
|
+
raise ValueError("groupchat_message_check disallow_item must be a list")
|
|
117
|
+
|
|
118
|
+
def _validate_environment_safeguards(self) -> None:
|
|
119
|
+
"""Validate environment safeguards section."""
|
|
120
|
+
env_rules = self.policy["agent_environment_safeguards"]
|
|
121
|
+
if not isinstance(env_rules, dict):
|
|
122
|
+
raise ValueError("agent_environment_safeguards must be a dictionary")
|
|
123
|
+
|
|
124
|
+
# Validate tool_interaction rules
|
|
125
|
+
if "tool_interaction" in env_rules:
|
|
126
|
+
if not isinstance(env_rules["tool_interaction"], list):
|
|
127
|
+
raise ValueError("tool_interaction must be a list")
|
|
128
|
+
|
|
129
|
+
for i, rule in enumerate(env_rules["tool_interaction"]):
|
|
130
|
+
if not isinstance(rule, dict):
|
|
131
|
+
raise ValueError(f"tool_interaction[{i}] must be a dictionary")
|
|
132
|
+
|
|
133
|
+
# Check method validation - no default, must be explicit
|
|
134
|
+
if "check_method" not in rule:
|
|
135
|
+
raise ValueError(f"tool_interaction[{i}] missing required field: check_method")
|
|
136
|
+
check_method = rule["check_method"]
|
|
137
|
+
if check_method not in ["llm", "regex"]:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"tool_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Validate action - no default, must be explicit
|
|
143
|
+
if "violation_response" not in rule and "action" not in rule:
|
|
144
|
+
raise ValueError(f"tool_interaction[{i}] missing required field: violation_response or action")
|
|
145
|
+
action = rule.get("violation_response", rule.get("action"))
|
|
146
|
+
if action not in ["block", "mask", "warning"]:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"tool_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# All tool_interaction rules must have message_source and message_destination
|
|
152
|
+
if "message_source" not in rule or "message_destination" not in rule:
|
|
153
|
+
raise ValueError(f"tool_interaction[{i}] must have 'message_source' and 'message_destination'")
|
|
154
|
+
|
|
155
|
+
if check_method == "llm":
|
|
156
|
+
# LLM-based checking requires either custom_prompt or disallow_item
|
|
157
|
+
if "custom_prompt" not in rule and "disallow_item" not in rule:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"tool_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
|
|
160
|
+
)
|
|
161
|
+
if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
|
|
162
|
+
raise ValueError(f"tool_interaction[{i}] disallow_item must be a list")
|
|
163
|
+
|
|
164
|
+
elif check_method == "regex":
|
|
165
|
+
# Regex-based checking requires pattern
|
|
166
|
+
if "pattern" not in rule:
|
|
167
|
+
raise ValueError(f"tool_interaction[{i}] with check_method 'regex' must have 'pattern'")
|
|
168
|
+
if not isinstance(rule["pattern"], str):
|
|
169
|
+
raise ValueError(f"tool_interaction[{i}] pattern must be a string")
|
|
170
|
+
# Test regex pattern validity
|
|
171
|
+
try:
|
|
172
|
+
re.compile(rule["pattern"])
|
|
173
|
+
except re.error as e:
|
|
174
|
+
raise ValueError(f"tool_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
|
|
175
|
+
|
|
176
|
+
# Validate llm_interaction rules
|
|
177
|
+
if "llm_interaction" in env_rules:
|
|
178
|
+
if not isinstance(env_rules["llm_interaction"], list):
|
|
179
|
+
raise ValueError("llm_interaction must be a list")
|
|
180
|
+
|
|
181
|
+
for i, rule in enumerate(env_rules["llm_interaction"]):
|
|
182
|
+
if not isinstance(rule, dict):
|
|
183
|
+
raise ValueError(f"llm_interaction[{i}] must be a dictionary")
|
|
184
|
+
|
|
185
|
+
# Check method validation - no default, must be explicit
|
|
186
|
+
if "check_method" not in rule:
|
|
187
|
+
raise ValueError(f"llm_interaction[{i}] missing required field: check_method")
|
|
188
|
+
check_method = rule["check_method"]
|
|
189
|
+
if check_method not in ["llm", "regex"]:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"llm_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Validate action - no default, must be explicit
|
|
195
|
+
if "action" not in rule:
|
|
196
|
+
raise ValueError(f"llm_interaction[{i}] missing required field: action")
|
|
197
|
+
action = rule["action"]
|
|
198
|
+
if action not in ["block", "mask", "warning"]:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"llm_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# All llm_interaction rules must have message_source and message_destination
|
|
204
|
+
if "message_source" not in rule or "message_destination" not in rule:
|
|
205
|
+
raise ValueError(f"llm_interaction[{i}] must have 'message_source' and 'message_destination'")
|
|
206
|
+
|
|
207
|
+
if check_method == "llm":
|
|
208
|
+
# LLM-based checking requires either custom_prompt or disallow_item
|
|
209
|
+
if "custom_prompt" not in rule and "disallow_item" not in rule:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"llm_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
|
|
212
|
+
)
|
|
213
|
+
if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
|
|
214
|
+
raise ValueError(f"llm_interaction[{i}] disallow_item must be a list")
|
|
215
|
+
|
|
216
|
+
elif check_method == "regex":
|
|
217
|
+
# Regex-based checking requires pattern
|
|
218
|
+
if "pattern" not in rule:
|
|
219
|
+
raise ValueError(f"llm_interaction[{i}] with check_method 'regex' must have 'pattern'")
|
|
220
|
+
if not isinstance(rule["pattern"], str):
|
|
221
|
+
raise ValueError(f"llm_interaction[{i}] pattern must be a string")
|
|
222
|
+
# Test regex pattern validity
|
|
223
|
+
try:
|
|
224
|
+
re.compile(rule["pattern"])
|
|
225
|
+
except re.error as e:
|
|
226
|
+
raise ValueError(f"llm_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
|
|
227
|
+
|
|
228
|
+
# Validate user_interaction rules
|
|
229
|
+
if "user_interaction" in env_rules:
|
|
230
|
+
if not isinstance(env_rules["user_interaction"], list):
|
|
231
|
+
raise ValueError("user_interaction must be a list")
|
|
232
|
+
|
|
233
|
+
for i, rule in enumerate(env_rules["user_interaction"]):
|
|
234
|
+
if not isinstance(rule, dict):
|
|
235
|
+
raise ValueError(f"user_interaction[{i}] must be a dictionary")
|
|
236
|
+
|
|
237
|
+
# Check method validation - no default, must be explicit
|
|
238
|
+
if "check_method" not in rule:
|
|
239
|
+
raise ValueError(f"user_interaction[{i}] missing required field: check_method")
|
|
240
|
+
check_method = rule["check_method"]
|
|
241
|
+
if check_method not in ["llm", "regex"]:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"user_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Validate action - no default, must be explicit
|
|
247
|
+
if "action" not in rule:
|
|
248
|
+
raise ValueError(f"user_interaction[{i}] missing required field: action")
|
|
249
|
+
action = rule["action"]
|
|
250
|
+
if action not in ["block", "mask", "warning"]:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"user_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# All user_interaction rules must have message_source and message_destination
|
|
256
|
+
if "message_source" not in rule or "message_destination" not in rule:
|
|
257
|
+
raise ValueError(f"user_interaction[{i}] must have 'message_source' and 'message_destination'")
|
|
258
|
+
|
|
259
|
+
if check_method == "llm":
|
|
260
|
+
# LLM-based checking requires either custom_prompt or disallow_item
|
|
261
|
+
if "custom_prompt" not in rule and "disallow_item" not in rule:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"user_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
|
|
264
|
+
)
|
|
265
|
+
if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
|
|
266
|
+
raise ValueError(f"user_interaction[{i}] disallow_item must be a list")
|
|
267
|
+
|
|
268
|
+
elif check_method == "regex":
|
|
269
|
+
# Regex-based checking requires pattern
|
|
270
|
+
if "pattern" not in rule:
|
|
271
|
+
raise ValueError(f"user_interaction[{i}] with check_method 'regex' must have 'pattern'")
|
|
272
|
+
if not isinstance(rule["pattern"], str):
|
|
273
|
+
raise ValueError(f"user_interaction[{i}] pattern must be a string")
|
|
274
|
+
# Test regex pattern validity
|
|
275
|
+
try:
|
|
276
|
+
re.compile(rule["pattern"])
|
|
277
|
+
except re.error as e:
|
|
278
|
+
raise ValueError(f"user_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
|
|
279
|
+
|
|
280
|
+
def validate_agent_names(self, agent_names: list[str]) -> None:
|
|
281
|
+
"""Validate that agent names referenced in policy actually exist."""
|
|
282
|
+
available_agents = set(agent_names)
|
|
283
|
+
|
|
284
|
+
# Check inter-agent safeguards
|
|
285
|
+
if "inter_agent_safeguards" in self.policy:
|
|
286
|
+
inter_agent = self.policy["inter_agent_safeguards"]
|
|
287
|
+
|
|
288
|
+
# Check agent_transitions
|
|
289
|
+
for i, rule in enumerate(inter_agent.get("agent_transitions", [])):
|
|
290
|
+
src_agent = rule.get("message_source")
|
|
291
|
+
dst_agent = rule.get("message_destination")
|
|
292
|
+
|
|
293
|
+
# Skip wildcard patterns
|
|
294
|
+
if src_agent != "*" and src_agent not in available_agents:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"agent_transitions[{i}] references unknown source agent: '{src_agent}'. Available agents: {sorted(available_agents)}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if dst_agent != "*" and dst_agent not in available_agents:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"agent_transitions[{i}] references unknown destination agent: '{dst_agent}'. Available agents: {sorted(available_agents)}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Check environment safeguards
|
|
305
|
+
if "agent_environment_safeguards" in self.policy:
|
|
306
|
+
env_rules = self.policy["agent_environment_safeguards"]
|
|
307
|
+
|
|
308
|
+
# Check tool_interaction rules - only support message_source/message_destination format
|
|
309
|
+
for i, rule in enumerate(env_rules.get("tool_interaction", [])):
|
|
310
|
+
# Only validate message_source/message_destination format
|
|
311
|
+
if "message_source" in rule and "message_destination" in rule:
|
|
312
|
+
# Skip detailed validation since we can't distinguish agent vs tool names
|
|
313
|
+
pass
|
|
314
|
+
elif "pattern" in rule and "message_source" not in rule:
|
|
315
|
+
# Simple pattern rules are allowed
|
|
316
|
+
pass
|
|
317
|
+
else:
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f"tool_interaction[{i}] must use either (message_source, message_destination) or pattern-only format"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Check llm_interaction rules
|
|
323
|
+
for i, rule in enumerate(env_rules.get("llm_interaction", [])):
|
|
324
|
+
# New format
|
|
325
|
+
if "message_source" in rule and "message_destination" in rule:
|
|
326
|
+
src = rule["message_source"]
|
|
327
|
+
dst = rule["message_destination"]
|
|
328
|
+
|
|
329
|
+
# Check agent references (LLM interactions have agent <-> llm)
|
|
330
|
+
if src != "llm" and src.lower() != "llm" and src not in available_agents:
|
|
331
|
+
raise ValueError(
|
|
332
|
+
f"llm_interaction[{i}] references unknown agent: '{src}'. Available agents: {sorted(available_agents)}"
|
|
333
|
+
)
|
|
334
|
+
if dst != "llm" and dst.lower() != "llm" and dst not in available_agents:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"llm_interaction[{i}] references unknown agent: '{dst}'. Available agents: {sorted(available_agents)}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
elif "agent_name" in rule:
|
|
340
|
+
agent_name = rule["agent_name"]
|
|
341
|
+
if agent_name not in available_agents:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"llm_interaction[{i}] references unknown agent: '{agent_name}'. Available agents: {sorted(available_agents)}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Check user_interaction rules
|
|
347
|
+
for i, rule in enumerate(env_rules.get("user_interaction", [])):
|
|
348
|
+
agent_name = rule.get("agent")
|
|
349
|
+
if agent_name and agent_name not in available_agents:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"user_interaction[{i}] references unknown agent: '{agent_name}'. Available agents: {sorted(available_agents)}"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def validate_tool_names(self, agent_tool_mapping: dict[str, list[str]], agent_names: list[str]) -> None:
|
|
355
|
+
"""Validate that tool names referenced in policy actually exist and belong to the correct agents.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
agent_tool_mapping: Dict mapping agent names to their tool names
|
|
359
|
+
agent_names: List of available agent names
|
|
360
|
+
"""
|
|
361
|
+
available_agents = set(agent_names)
|
|
362
|
+
# Get all available tools across all agents
|
|
363
|
+
all_available_tools = set()
|
|
364
|
+
for tools in agent_tool_mapping.values():
|
|
365
|
+
all_available_tools.update(tools)
|
|
366
|
+
|
|
367
|
+
# Check environment safeguards for tool references
|
|
368
|
+
if "agent_environment_safeguards" in self.policy:
|
|
369
|
+
env_rules = self.policy["agent_environment_safeguards"]
|
|
370
|
+
|
|
371
|
+
# Check tool_interaction rules
|
|
372
|
+
for i, rule in enumerate(env_rules.get("tool_interaction", [])):
|
|
373
|
+
# Check message_source/message_destination format
|
|
374
|
+
if "message_source" in rule and "message_destination" in rule:
|
|
375
|
+
src = rule["message_source"]
|
|
376
|
+
dst = rule["message_destination"]
|
|
377
|
+
|
|
378
|
+
# Validate agent-tool relationships
|
|
379
|
+
self._validate_agent_tool_relationship(
|
|
380
|
+
i, "message_source", src, dst, available_agents, agent_tool_mapping, all_available_tools
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def _validate_agent_tool_relationship(
|
|
384
|
+
self,
|
|
385
|
+
rule_index: int,
|
|
386
|
+
src_field: str,
|
|
387
|
+
src: str,
|
|
388
|
+
dst: str,
|
|
389
|
+
available_agents: set[str],
|
|
390
|
+
agent_tool_mapping: dict[str, list[str]],
|
|
391
|
+
all_available_tools: set[str],
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Validate that agent-tool relationships in policy are correct."""
|
|
394
|
+
# Skip wildcards and special cases
|
|
395
|
+
if src == "*" or dst == "*" or src.lower() == "user" or dst.lower() == "user":
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Case 1: Agent -> Tool (agent uses tool)
|
|
399
|
+
if src in available_agents and dst not in available_agents:
|
|
400
|
+
# dst should be a tool that belongs to src agent
|
|
401
|
+
if dst not in all_available_tools:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"tool_interaction[{rule_index}] references unknown tool: '{dst}'. Available tools: {sorted(all_available_tools)}"
|
|
404
|
+
)
|
|
405
|
+
if dst not in agent_tool_mapping.get(src, []):
|
|
406
|
+
agent_tools = agent_tool_mapping.get(src, [])
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"tool_interaction[{rule_index}] agent '{src}' does not have access to tool '{dst}'. "
|
|
409
|
+
f"Agent's tools: {sorted(agent_tools)}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Case 2: Tool -> Agent (tool output to agent)
|
|
413
|
+
elif src not in available_agents and dst in available_agents:
|
|
414
|
+
# src should be a tool that belongs to dst agent
|
|
415
|
+
if src not in all_available_tools:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"tool_interaction[{rule_index}] references unknown tool: '{src}'. Available tools: {sorted(all_available_tools)}"
|
|
418
|
+
)
|
|
419
|
+
if src not in agent_tool_mapping.get(dst, []):
|
|
420
|
+
agent_tools = agent_tool_mapping.get(dst, [])
|
|
421
|
+
raise ValueError(
|
|
422
|
+
f"tool_interaction[{rule_index}] agent '{dst}' does not have access to tool '{src}'. "
|
|
423
|
+
f"Agent's tools: {sorted(agent_tools)}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Case 3: Tool -> Tool (unusual, but validate both exist)
|
|
427
|
+
elif src not in available_agents and dst not in available_agents:
|
|
428
|
+
if src not in all_available_tools:
|
|
429
|
+
raise ValueError(
|
|
430
|
+
f"tool_interaction[{rule_index}] references unknown tool: '{src}'. Available tools: {sorted(all_available_tools)}"
|
|
431
|
+
)
|
|
432
|
+
if dst not in all_available_tools:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"tool_interaction[{rule_index}] references unknown tool: '{dst}'. Available tools: {sorted(all_available_tools)}"
|
|
435
|
+
)
|