agent-killswitch 0.1.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.
- agent_killswitch/__init__.py +76 -0
- agent_killswitch/_version.py +5 -0
- agent_killswitch/backends/__init__.py +21 -0
- agent_killswitch/backends/base.py +95 -0
- agent_killswitch/backends/memory.py +120 -0
- agent_killswitch/backends/redis.py +230 -0
- agent_killswitch/core/__init__.py +36 -0
- agent_killswitch/core/budget_kill.py +281 -0
- agent_killswitch/core/cascading.py +280 -0
- agent_killswitch/core/circuit_breaker.py +323 -0
- agent_killswitch/core/enums.py +112 -0
- agent_killswitch/core/heartbeat.py +313 -0
- agent_killswitch/core/killswitch.py +581 -0
- agent_killswitch/core/models.py +173 -0
- agent_killswitch/decorators.py +302 -0
- agent_killswitch/integrations/__init__.py +17 -0
- agent_killswitch/integrations/asyncio_tasks.py +211 -0
- agent_killswitch/integrations/crewai.py +151 -0
- agent_killswitch/integrations/langchain.py +135 -0
- agent_killswitch/integrations/langgraph.py +143 -0
- agent_killswitch/integrations/openai_agents.py +151 -0
- agent_killswitch/py.typed +0 -0
- agent_killswitch-0.1.0.dist-info/METADATA +392 -0
- agent_killswitch-0.1.0.dist-info/RECORD +26 -0
- agent_killswitch-0.1.0.dist-info/WHEEL +4 -0
- agent_killswitch-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""CrewAI integration for agent-killswitch.
|
|
2
|
+
|
|
3
|
+
Provides a pre-action hook that checks the kill switch before
|
|
4
|
+
each CrewAI agent action.
|
|
5
|
+
|
|
6
|
+
Requires: pip install agent-killswitch[crewai]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class KillSwitchHook:
|
|
19
|
+
"""CrewAI pre-action hook that checks the kill switch.
|
|
20
|
+
|
|
21
|
+
This hook is designed to be called before each agent action in
|
|
22
|
+
a CrewAI workflow. If the kill switch is active, it prevents
|
|
23
|
+
the action from executing.
|
|
24
|
+
|
|
25
|
+
Example::
|
|
26
|
+
|
|
27
|
+
from agent_killswitch import KillSwitch
|
|
28
|
+
from agent_killswitch.integrations.crewai import KillSwitchHook
|
|
29
|
+
|
|
30
|
+
ks = KillSwitch()
|
|
31
|
+
hook = KillSwitchHook(killswitch=ks, crew_id="my-crew")
|
|
32
|
+
|
|
33
|
+
# In your CrewAI setup:
|
|
34
|
+
# Use hook.before_action as a step callback
|
|
35
|
+
if hook.before_action(agent_id="researcher"):
|
|
36
|
+
# Action is allowed
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
killswitch: The KillSwitch instance.
|
|
41
|
+
crew_id: The crew ID for scope checks.
|
|
42
|
+
on_blocked: Callback when an action is blocked by kill switch.
|
|
43
|
+
Signature: (agent_id: str, level: str, reason: str) -> None
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
killswitch: Any,
|
|
49
|
+
crew_id: str | None = None,
|
|
50
|
+
on_blocked: Callable[[str, str, str], None] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._ks = killswitch
|
|
53
|
+
self._crew_id = crew_id
|
|
54
|
+
self._on_blocked = on_blocked
|
|
55
|
+
|
|
56
|
+
def before_action(self, agent_id: str | None = None) -> bool:
|
|
57
|
+
"""Check if the agent action should proceed.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
agent_id: The agent attempting the action.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if the action should proceed, False if blocked.
|
|
64
|
+
"""
|
|
65
|
+
status = self._ks.check(
|
|
66
|
+
scope_target=agent_id,
|
|
67
|
+
crew_id=self._crew_id,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if status.is_killed:
|
|
71
|
+
level_str = status.level.value if status.level else "unknown"
|
|
72
|
+
reason = status.reason or ""
|
|
73
|
+
|
|
74
|
+
logger.warning(
|
|
75
|
+
"crewai_action_blocked",
|
|
76
|
+
extra={
|
|
77
|
+
"agent_id": agent_id,
|
|
78
|
+
"crew_id": self._crew_id,
|
|
79
|
+
"level": level_str,
|
|
80
|
+
"reason": reason,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if self._on_blocked is not None:
|
|
85
|
+
try:
|
|
86
|
+
self._on_blocked(agent_id or "", level_str, reason)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.error(
|
|
89
|
+
"on_blocked callback error",
|
|
90
|
+
extra={"error": str(exc)},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def create_step_callback(
|
|
98
|
+
self,
|
|
99
|
+
agent_id: str | None = None,
|
|
100
|
+
) -> Callable[[Any], Any]:
|
|
101
|
+
"""Create a CrewAI step callback function.
|
|
102
|
+
|
|
103
|
+
Returns a callable suitable for use as a CrewAI step_callback.
|
|
104
|
+
The callback checks the kill switch and raises RuntimeError
|
|
105
|
+
if the switch is active.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
agent_id: The agent ID.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Callback function.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def callback(step_output: Any) -> Any:
|
|
115
|
+
if not self.before_action(agent_id=agent_id):
|
|
116
|
+
raise RuntimeError(f"Kill switch active for agent={agent_id}, crew={self._crew_id}")
|
|
117
|
+
return step_output
|
|
118
|
+
|
|
119
|
+
return callback
|
|
120
|
+
|
|
121
|
+
def wrap_crew(
|
|
122
|
+
self,
|
|
123
|
+
crew: Any,
|
|
124
|
+
agent_id_field: str = "role",
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""Wrap a CrewAI crew with kill switch protection.
|
|
127
|
+
|
|
128
|
+
This method patches the crew to check the kill switch before
|
|
129
|
+
each agent action. Returns the modified crew.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
crew: The CrewAI Crew instance.
|
|
133
|
+
agent_id_field: Agent attribute to use as agent_id.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The crew with kill switch protection applied.
|
|
137
|
+
"""
|
|
138
|
+
if hasattr(crew, "step_callback"):
|
|
139
|
+
original = crew.step_callback
|
|
140
|
+
|
|
141
|
+
def wrapped_callback(step_output: Any) -> Any:
|
|
142
|
+
# Check kill switch
|
|
143
|
+
if not self.before_action(agent_id=self._crew_id):
|
|
144
|
+
raise RuntimeError(f"Kill switch active for crew={self._crew_id}")
|
|
145
|
+
if original is not None:
|
|
146
|
+
return original(step_output)
|
|
147
|
+
return step_output
|
|
148
|
+
|
|
149
|
+
crew.step_callback = wrapped_callback
|
|
150
|
+
|
|
151
|
+
return crew
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""LangChain integration for agent-killswitch.
|
|
2
|
+
|
|
3
|
+
Provides a callback handler that checks the kill switch before and
|
|
4
|
+
after each LLM call, tool invocation, and chain execution.
|
|
5
|
+
|
|
6
|
+
Requires: pip install agent-killswitch[langchain]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
19
|
+
except ImportError:
|
|
20
|
+
# Create a stub so the class can be defined without the dependency
|
|
21
|
+
class BaseCallbackHandler: # type: ignore[no-redef]
|
|
22
|
+
"""Stub for when langchain-core is not installed."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KillSwitchCallbackHandler(BaseCallbackHandler):
|
|
28
|
+
"""LangChain callback handler that enforces kill switch checks.
|
|
29
|
+
|
|
30
|
+
Checks the kill switch before each LLM call and tool invocation.
|
|
31
|
+
If the kill switch is active, raises an exception to halt the chain.
|
|
32
|
+
|
|
33
|
+
Example::
|
|
34
|
+
|
|
35
|
+
from langchain_openai import ChatOpenAI
|
|
36
|
+
from agent_killswitch import KillSwitch
|
|
37
|
+
from agent_killswitch.integrations.langchain import KillSwitchCallbackHandler
|
|
38
|
+
|
|
39
|
+
ks = KillSwitch()
|
|
40
|
+
handler = KillSwitchCallbackHandler(killswitch=ks, agent_id="my-agent")
|
|
41
|
+
|
|
42
|
+
llm = ChatOpenAI(callbacks=[handler])
|
|
43
|
+
llm.invoke("Hello") # Will check kill switch before calling
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
killswitch: The KillSwitch instance to check.
|
|
47
|
+
agent_id: The agent ID for scope-specific checks.
|
|
48
|
+
crew_id: Optional crew ID for crew scope checks.
|
|
49
|
+
raise_on_kill: If True, raise RuntimeError on kill. Default True.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
killswitch: Any,
|
|
55
|
+
agent_id: str | None = None,
|
|
56
|
+
crew_id: str | None = None,
|
|
57
|
+
raise_on_kill: bool = True,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._ks = killswitch
|
|
60
|
+
self._agent_id = agent_id
|
|
61
|
+
self._crew_id = crew_id
|
|
62
|
+
self._raise_on_kill = raise_on_kill
|
|
63
|
+
|
|
64
|
+
def _check(self, action: str) -> None:
|
|
65
|
+
"""Check the kill switch and raise if active.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
action: Description of the action being checked.
|
|
69
|
+
"""
|
|
70
|
+
status = self._ks.check(
|
|
71
|
+
scope_target=self._agent_id,
|
|
72
|
+
crew_id=self._crew_id,
|
|
73
|
+
)
|
|
74
|
+
if status.is_killed:
|
|
75
|
+
msg = (
|
|
76
|
+
f"Kill switch active during {action}: level={status.level}, reason={status.reason}"
|
|
77
|
+
)
|
|
78
|
+
logger.warning("langchain_kill_switch_triggered", extra={"action": action})
|
|
79
|
+
if self._raise_on_kill:
|
|
80
|
+
raise RuntimeError(msg)
|
|
81
|
+
|
|
82
|
+
def on_llm_start(
|
|
83
|
+
self,
|
|
84
|
+
serialized: dict[str, Any],
|
|
85
|
+
prompts: list[str],
|
|
86
|
+
*,
|
|
87
|
+
run_id: UUID,
|
|
88
|
+
parent_run_id: UUID | None = None,
|
|
89
|
+
tags: list[str] | None = None,
|
|
90
|
+
metadata: dict[str, Any] | None = None,
|
|
91
|
+
**kwargs: Any,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Check kill switch before LLM call."""
|
|
94
|
+
self._check("llm_start")
|
|
95
|
+
|
|
96
|
+
def on_llm_end(
|
|
97
|
+
self,
|
|
98
|
+
response: Any,
|
|
99
|
+
*,
|
|
100
|
+
run_id: UUID,
|
|
101
|
+
parent_run_id: UUID | None = None,
|
|
102
|
+
tags: list[str] | None = None,
|
|
103
|
+
**kwargs: Any,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Check kill switch after LLM call."""
|
|
106
|
+
self._check("llm_end")
|
|
107
|
+
|
|
108
|
+
def on_tool_start(
|
|
109
|
+
self,
|
|
110
|
+
serialized: dict[str, Any],
|
|
111
|
+
input_str: str,
|
|
112
|
+
*,
|
|
113
|
+
run_id: UUID,
|
|
114
|
+
parent_run_id: UUID | None = None,
|
|
115
|
+
tags: list[str] | None = None,
|
|
116
|
+
metadata: dict[str, Any] | None = None,
|
|
117
|
+
inputs: dict[str, Any] | None = None,
|
|
118
|
+
**kwargs: Any,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Check kill switch before tool invocation."""
|
|
121
|
+
self._check("tool_start")
|
|
122
|
+
|
|
123
|
+
def on_chain_start(
|
|
124
|
+
self,
|
|
125
|
+
serialized: dict[str, Any],
|
|
126
|
+
inputs: dict[str, Any],
|
|
127
|
+
*,
|
|
128
|
+
run_id: UUID,
|
|
129
|
+
parent_run_id: UUID | None = None,
|
|
130
|
+
tags: list[str] | None = None,
|
|
131
|
+
metadata: dict[str, Any] | None = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Check kill switch before chain execution."""
|
|
135
|
+
self._check("chain_start")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""LangGraph integration for agent-killswitch.
|
|
2
|
+
|
|
3
|
+
Provides a killswitch check node and conditional edge for use
|
|
4
|
+
in LangGraph state graphs.
|
|
5
|
+
|
|
6
|
+
Requires: pip install agent-killswitch[langgraph]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KillSwitchNode:
|
|
18
|
+
"""A LangGraph node that checks the kill switch.
|
|
19
|
+
|
|
20
|
+
Use this as a node in a LangGraph StateGraph. If the kill switch
|
|
21
|
+
is active, it sets a flag in the state that can be used with
|
|
22
|
+
conditional edges to route to a termination node.
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
from langgraph.graph import StateGraph
|
|
27
|
+
from agent_killswitch import KillSwitch
|
|
28
|
+
from agent_killswitch.integrations.langgraph import KillSwitchNode
|
|
29
|
+
|
|
30
|
+
ks = KillSwitch()
|
|
31
|
+
ks_node = KillSwitchNode(killswitch=ks, agent_id="graph-agent")
|
|
32
|
+
|
|
33
|
+
graph = StateGraph(dict)
|
|
34
|
+
graph.add_node("check_kill", ks_node)
|
|
35
|
+
graph.add_node("agent_work", agent_node)
|
|
36
|
+
graph.add_node("terminated", termination_node)
|
|
37
|
+
graph.add_conditional_edges(
|
|
38
|
+
"check_kill",
|
|
39
|
+
ks_node.should_continue,
|
|
40
|
+
{"continue": "agent_work", "killed": "terminated"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
killswitch: The KillSwitch instance.
|
|
45
|
+
agent_id: Agent ID for scope checks.
|
|
46
|
+
crew_id: Optional crew ID for crew scope checks.
|
|
47
|
+
kill_state_key: Key in state dict to set when killed.
|
|
48
|
+
Default "kill_switch_active".
|
|
49
|
+
kill_level_key: Key in state dict to set with kill level.
|
|
50
|
+
Default "kill_level".
|
|
51
|
+
kill_reason_key: Key in state dict to set with kill reason.
|
|
52
|
+
Default "kill_reason".
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
killswitch: Any,
|
|
58
|
+
agent_id: str | None = None,
|
|
59
|
+
crew_id: str | None = None,
|
|
60
|
+
kill_state_key: str = "kill_switch_active",
|
|
61
|
+
kill_level_key: str = "kill_level",
|
|
62
|
+
kill_reason_key: str = "kill_reason",
|
|
63
|
+
) -> None:
|
|
64
|
+
self._ks = killswitch
|
|
65
|
+
self._agent_id = agent_id
|
|
66
|
+
self._crew_id = crew_id
|
|
67
|
+
self._kill_state_key = kill_state_key
|
|
68
|
+
self._kill_level_key = kill_level_key
|
|
69
|
+
self._kill_reason_key = kill_reason_key
|
|
70
|
+
|
|
71
|
+
def __call__(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
"""Check the kill switch and update state.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
state: The current LangGraph state.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Updated state with kill switch status.
|
|
79
|
+
"""
|
|
80
|
+
status = self._ks.check(
|
|
81
|
+
scope_target=self._agent_id,
|
|
82
|
+
crew_id=self._crew_id,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result = dict(state)
|
|
86
|
+
result[self._kill_state_key] = status.is_killed
|
|
87
|
+
if status.is_killed:
|
|
88
|
+
result[self._kill_level_key] = status.level.value if status.level else None
|
|
89
|
+
result[self._kill_reason_key] = status.reason or ""
|
|
90
|
+
logger.warning(
|
|
91
|
+
"langgraph_kill_switch_active",
|
|
92
|
+
extra={
|
|
93
|
+
"agent_id": self._agent_id,
|
|
94
|
+
"level": result[self._kill_level_key],
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
result[self._kill_level_key] = None
|
|
99
|
+
result[self._kill_reason_key] = ""
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def should_continue(self, state: dict[str, Any]) -> str:
|
|
104
|
+
"""Conditional edge function: 'continue' or 'killed'.
|
|
105
|
+
|
|
106
|
+
Use with graph.add_conditional_edges() to route based on
|
|
107
|
+
kill switch status.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
state: The current LangGraph state.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
'killed' if kill switch is active, 'continue' otherwise.
|
|
114
|
+
"""
|
|
115
|
+
if state.get(self._kill_state_key, False):
|
|
116
|
+
return "killed"
|
|
117
|
+
return "continue"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_killswitch_node(
|
|
121
|
+
killswitch: Any,
|
|
122
|
+
agent_id: str | None = None,
|
|
123
|
+
crew_id: str | None = None,
|
|
124
|
+
) -> tuple[Any, Any]:
|
|
125
|
+
"""Convenience function to create a kill switch node and condition.
|
|
126
|
+
|
|
127
|
+
Returns a tuple of (node_function, condition_function) ready to
|
|
128
|
+
add to a LangGraph StateGraph.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
killswitch: The KillSwitch instance.
|
|
132
|
+
agent_id: Agent ID for scope checks.
|
|
133
|
+
crew_id: Optional crew ID.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Tuple of (node_callable, condition_callable).
|
|
137
|
+
"""
|
|
138
|
+
node = KillSwitchNode(
|
|
139
|
+
killswitch=killswitch,
|
|
140
|
+
agent_id=agent_id,
|
|
141
|
+
crew_id=crew_id,
|
|
142
|
+
)
|
|
143
|
+
return node, node.should_continue
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""OpenAI Agents SDK integration for agent-killswitch.
|
|
2
|
+
|
|
3
|
+
Provides a guardrail function and input guard compatible with the
|
|
4
|
+
OpenAI Agents SDK.
|
|
5
|
+
|
|
6
|
+
Requires: pip install agent-killswitch[openai]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KillSwitchGuardrail:
|
|
18
|
+
"""OpenAI Agents SDK guardrail that enforces kill switch checks.
|
|
19
|
+
|
|
20
|
+
Can be used as an input guardrail or output guardrail in the
|
|
21
|
+
OpenAI Agents SDK.
|
|
22
|
+
|
|
23
|
+
Example::
|
|
24
|
+
|
|
25
|
+
from agent_killswitch import KillSwitch
|
|
26
|
+
from agent_killswitch.integrations.openai_agents import KillSwitchGuardrail
|
|
27
|
+
|
|
28
|
+
ks = KillSwitch()
|
|
29
|
+
guardrail = KillSwitchGuardrail(killswitch=ks, agent_id="openai-agent")
|
|
30
|
+
|
|
31
|
+
# Use as input guardrail
|
|
32
|
+
result = guardrail.check_input("user message")
|
|
33
|
+
if not result["allowed"]:
|
|
34
|
+
print(f"Blocked: {result['reason']}")
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
killswitch: The KillSwitch instance.
|
|
38
|
+
agent_id: Agent ID for scope checks.
|
|
39
|
+
crew_id: Optional crew ID for crew scope checks.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
killswitch: Any,
|
|
45
|
+
agent_id: str | None = None,
|
|
46
|
+
crew_id: str | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._ks = killswitch
|
|
49
|
+
self._agent_id = agent_id
|
|
50
|
+
self._crew_id = crew_id
|
|
51
|
+
|
|
52
|
+
def check_input(self, user_input: Any) -> dict[str, Any]:
|
|
53
|
+
"""Check kill switch before processing input.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
user_input: The user input being processed.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary with keys:
|
|
60
|
+
- 'allowed': bool
|
|
61
|
+
- 'reason': str (if blocked)
|
|
62
|
+
- 'level': str (if blocked)
|
|
63
|
+
"""
|
|
64
|
+
status = self._ks.check(
|
|
65
|
+
scope_target=self._agent_id,
|
|
66
|
+
crew_id=self._crew_id,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if status.is_killed:
|
|
70
|
+
level_str = status.level.value if status.level else "unknown"
|
|
71
|
+
return {
|
|
72
|
+
"allowed": False,
|
|
73
|
+
"reason": status.reason or "Kill switch is active",
|
|
74
|
+
"level": level_str,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {"allowed": True, "reason": "", "level": ""}
|
|
78
|
+
|
|
79
|
+
def check_output(self, output: Any) -> dict[str, Any]:
|
|
80
|
+
"""Check kill switch before returning output.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
output: The agent output being validated.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary with keys:
|
|
87
|
+
- 'allowed': bool
|
|
88
|
+
- 'reason': str (if blocked)
|
|
89
|
+
- 'level': str (if blocked)
|
|
90
|
+
"""
|
|
91
|
+
return self.check_input(output)
|
|
92
|
+
|
|
93
|
+
def as_guardrail_function(self) -> Any:
|
|
94
|
+
"""Return a guardrail function compatible with the OpenAI SDK.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
An async function suitable for use as a guardrail.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
async def guardrail_fn(
|
|
101
|
+
context: Any,
|
|
102
|
+
agent: Any,
|
|
103
|
+
input_data: Any,
|
|
104
|
+
) -> Any | None:
|
|
105
|
+
"""Guardrail function that checks the kill switch.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
context: The run context.
|
|
109
|
+
agent: The agent being guarded.
|
|
110
|
+
input_data: The input being processed.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
None if allowed, or a GuardrailFunctionOutput if blocked.
|
|
114
|
+
"""
|
|
115
|
+
status = self._ks.check(
|
|
116
|
+
scope_target=self._agent_id,
|
|
117
|
+
crew_id=self._crew_id,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if status.is_killed:
|
|
121
|
+
level_str = status.level.value if status.level else "unknown"
|
|
122
|
+
reason = status.reason or "Kill switch is active"
|
|
123
|
+
logger.warning(
|
|
124
|
+
"openai_agents_guardrail_blocked",
|
|
125
|
+
extra={
|
|
126
|
+
"agent_id": self._agent_id,
|
|
127
|
+
"level": level_str,
|
|
128
|
+
"reason": reason,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Try to use the SDK's GuardrailFunctionOutput if available
|
|
133
|
+
try:
|
|
134
|
+
from openai.agents import GuardrailFunctionOutput, GuardrailTripwireTriggered
|
|
135
|
+
|
|
136
|
+
output = GuardrailFunctionOutput(
|
|
137
|
+
output_info={"blocked": True, "reason": reason, "level": level_str},
|
|
138
|
+
tripwire_triggered=True,
|
|
139
|
+
)
|
|
140
|
+
raise GuardrailTripwireTriggered(output)
|
|
141
|
+
except ImportError:
|
|
142
|
+
# The openai.agents extra is not installed. That is an expected,
|
|
143
|
+
# supported configuration - chaining the ImportError here would
|
|
144
|
+
# imply something went wrong, so suppress the context.
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"Kill switch active: level={level_str}, reason={reason}"
|
|
147
|
+
) from None
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
return guardrail_fn
|
|
File without changes
|