sondera-harness 0.6.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.
- sondera/__init__.py +111 -0
- sondera/__main__.py +4 -0
- sondera/adk/__init__.py +3 -0
- sondera/adk/analyze.py +222 -0
- sondera/adk/plugin.py +387 -0
- sondera/cli.py +22 -0
- sondera/exceptions.py +167 -0
- sondera/harness/__init__.py +6 -0
- sondera/harness/abc.py +102 -0
- sondera/harness/cedar/__init__.py +0 -0
- sondera/harness/cedar/harness.py +363 -0
- sondera/harness/cedar/schema.py +225 -0
- sondera/harness/sondera/__init__.py +0 -0
- sondera/harness/sondera/_grpc.py +354 -0
- sondera/harness/sondera/harness.py +890 -0
- sondera/langgraph/__init__.py +15 -0
- sondera/langgraph/analyze.py +543 -0
- sondera/langgraph/exceptions.py +19 -0
- sondera/langgraph/graph.py +210 -0
- sondera/langgraph/middleware.py +454 -0
- sondera/proto/google/protobuf/any_pb2.py +37 -0
- sondera/proto/google/protobuf/any_pb2.pyi +14 -0
- sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/duration_pb2.py +37 -0
- sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
- sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/empty_pb2.py +37 -0
- sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
- sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/struct_pb2.py +47 -0
- sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
- sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
- sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
- sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
- sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
- sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
- sondera/proto/sondera/__init__.py +0 -0
- sondera/proto/sondera/core/__init__.py +0 -0
- sondera/proto/sondera/core/v1/__init__.py +0 -0
- sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
- sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
- sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
- sondera/proto/sondera/harness/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
- sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
- sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
- sondera/py.typed +0 -0
- sondera/settings.py +20 -0
- sondera/strands/__init__.py +5 -0
- sondera/strands/analyze.py +244 -0
- sondera/strands/harness.py +333 -0
- sondera/tui/__init__.py +0 -0
- sondera/tui/app.py +309 -0
- sondera/tui/screens/__init__.py +5 -0
- sondera/tui/screens/adjudication.py +184 -0
- sondera/tui/screens/agent.py +158 -0
- sondera/tui/screens/trajectory.py +158 -0
- sondera/tui/widgets/__init__.py +23 -0
- sondera/tui/widgets/agent_card.py +94 -0
- sondera/tui/widgets/agent_list.py +73 -0
- sondera/tui/widgets/recent_adjudications.py +52 -0
- sondera/tui/widgets/recent_trajectories.py +54 -0
- sondera/tui/widgets/summary.py +57 -0
- sondera/tui/widgets/tool_card.py +33 -0
- sondera/tui/widgets/violation_panel.py +72 -0
- sondera/tui/widgets/violations_list.py +78 -0
- sondera/tui/widgets/violations_summary.py +104 -0
- sondera/types.py +346 -0
- sondera_harness-0.6.0.dist-info/METADATA +323 -0
- sondera_harness-0.6.0.dist-info/RECORD +77 -0
- sondera_harness-0.6.0.dist-info/WHEEL +5 -0
- sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
- sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
- sondera_harness-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Strands Agent analysis utilities."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any, get_type_hints
|
|
8
|
+
|
|
9
|
+
from sondera.types import Agent, Parameter, SourceCode, Tool
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_function_source(func: Callable) -> tuple[str, str]:
|
|
15
|
+
"""Extract source code and language from a function."""
|
|
16
|
+
try:
|
|
17
|
+
source = inspect.getsource(func)
|
|
18
|
+
return "python", source
|
|
19
|
+
except (OSError, TypeError):
|
|
20
|
+
return "python", f"# Source code not available for {func.__name__}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _analyze_function_parameters(func: Callable) -> list[Parameter]:
|
|
24
|
+
"""Analyze function parameters and return Sondera format Parameters."""
|
|
25
|
+
parameters = []
|
|
26
|
+
sig = inspect.signature(func)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
type_hints = get_type_hints(func)
|
|
30
|
+
except Exception:
|
|
31
|
+
type_hints = {}
|
|
32
|
+
|
|
33
|
+
for param_name, param in sig.parameters.items():
|
|
34
|
+
if param_name in ["tool_context", "self", "cls"]:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
param_type = "Any"
|
|
38
|
+
if param.annotation != inspect.Parameter.empty:
|
|
39
|
+
if isinstance(param.annotation, type):
|
|
40
|
+
param_type = param.annotation.__name__
|
|
41
|
+
else:
|
|
42
|
+
param_type = str(param.annotation)
|
|
43
|
+
elif param_name in type_hints:
|
|
44
|
+
hint = type_hints[param_name]
|
|
45
|
+
param_type = hint.__name__ if isinstance(hint, type) else str(hint)
|
|
46
|
+
|
|
47
|
+
description = f"Parameter {param_name}"
|
|
48
|
+
if func.__doc__:
|
|
49
|
+
lines = func.__doc__.split("\n")
|
|
50
|
+
for line in lines:
|
|
51
|
+
if param_name in line:
|
|
52
|
+
description = line.strip()
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
parameters.append(
|
|
56
|
+
Parameter(name=param_name, description=description, type=param_type)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return parameters
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_function_return_type(func: Callable) -> str:
|
|
63
|
+
"""Extract the return type from a function."""
|
|
64
|
+
sig = inspect.signature(func)
|
|
65
|
+
if sig.return_annotation != inspect.Signature.empty:
|
|
66
|
+
if isinstance(sig.return_annotation, type):
|
|
67
|
+
return sig.return_annotation.__name__
|
|
68
|
+
else:
|
|
69
|
+
return str(sig.return_annotation)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
type_hints = get_type_hints(func)
|
|
73
|
+
if "return" in type_hints:
|
|
74
|
+
hint = type_hints["return"]
|
|
75
|
+
if isinstance(hint, type):
|
|
76
|
+
return hint.__name__
|
|
77
|
+
else:
|
|
78
|
+
return str(hint)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return "Any"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract_strands_tool_schema(tool: Any) -> tuple[str | None, str | None]:
|
|
86
|
+
"""Extract JSON schemas from a Strands tool.
|
|
87
|
+
|
|
88
|
+
Strands tools decorated with @tool have a tool_spec attribute containing:
|
|
89
|
+
- name: tool name
|
|
90
|
+
- description: tool description
|
|
91
|
+
- inputSchema: dict with 'json' key containing the JSON schema
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
tool: A Strands tool (decorated function or tool object)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (parameters_json_schema, response_json_schema)
|
|
98
|
+
"""
|
|
99
|
+
parameters_json_schema = None
|
|
100
|
+
response_json_schema = None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Check for tool_spec attribute (Strands @tool decorated functions)
|
|
104
|
+
tool_spec = getattr(tool, "tool_spec", None)
|
|
105
|
+
if tool_spec and isinstance(tool_spec, dict):
|
|
106
|
+
# Extract inputSchema from tool_spec
|
|
107
|
+
input_schema = tool_spec.get("inputSchema", {})
|
|
108
|
+
if input_schema:
|
|
109
|
+
# Strands stores the JSON schema under the 'json' key
|
|
110
|
+
json_schema = input_schema.get("json", input_schema)
|
|
111
|
+
if json_schema:
|
|
112
|
+
parameters_json_schema = json.dumps(json_schema)
|
|
113
|
+
|
|
114
|
+
# Try to generate response schema from return type annotation
|
|
115
|
+
if callable(tool):
|
|
116
|
+
return_type = _get_function_return_type(tool)
|
|
117
|
+
if return_type and return_type != "Any":
|
|
118
|
+
response_json_schema = json.dumps(
|
|
119
|
+
{
|
|
120
|
+
"type": _python_type_to_json_schema_type(return_type),
|
|
121
|
+
"description": f"Return value of type {return_type}",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.debug(f"Could not extract JSON schema from tool: {e}")
|
|
126
|
+
|
|
127
|
+
return parameters_json_schema, response_json_schema
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _python_type_to_json_schema_type(python_type: str) -> str:
|
|
131
|
+
"""Convert Python type name to JSON Schema type."""
|
|
132
|
+
type_mapping = {
|
|
133
|
+
"str": "string",
|
|
134
|
+
"int": "integer",
|
|
135
|
+
"float": "number",
|
|
136
|
+
"bool": "boolean",
|
|
137
|
+
"list": "array",
|
|
138
|
+
"dict": "object",
|
|
139
|
+
"None": "null",
|
|
140
|
+
"NoneType": "null",
|
|
141
|
+
}
|
|
142
|
+
return type_mapping.get(python_type, "string")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _extract_tool_info(tool: Any) -> dict[str, Any]:
|
|
146
|
+
"""Extract all tool information from a Strands tool.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
tool: A Strands tool
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary with tool name, description, and schemas
|
|
153
|
+
"""
|
|
154
|
+
# Get tool_spec if available (Strands @tool decorated functions)
|
|
155
|
+
tool_spec = getattr(tool, "tool_spec", None)
|
|
156
|
+
|
|
157
|
+
if tool_spec and isinstance(tool_spec, dict):
|
|
158
|
+
tool_name = tool_spec.get(
|
|
159
|
+
"name", getattr(tool, "tool_name", getattr(tool, "__name__", "unknown"))
|
|
160
|
+
)
|
|
161
|
+
tool_description = tool_spec.get("description", "")
|
|
162
|
+
else:
|
|
163
|
+
tool_name = getattr(
|
|
164
|
+
tool,
|
|
165
|
+
"tool_name",
|
|
166
|
+
getattr(tool, "name", getattr(tool, "__name__", "unknown")),
|
|
167
|
+
)
|
|
168
|
+
tool_description = getattr(
|
|
169
|
+
tool, "description", tool.__doc__ or f"Tool {tool_name}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"name": tool_name,
|
|
174
|
+
"description": tool_description,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def format_strands_agent(agent: Any) -> Agent:
|
|
179
|
+
"""Transform a Strands agent into Sondera Agent format.
|
|
180
|
+
|
|
181
|
+
Extracts tool metadata including JSON schemas for parameters and responses.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
agent: The Strands agent instance
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Sondera Agent representation with full tool schemas
|
|
188
|
+
"""
|
|
189
|
+
# Extract agent metadata
|
|
190
|
+
agent_name = getattr(agent, "name", "strands-agent")
|
|
191
|
+
agent_id = agent_name
|
|
192
|
+
system_prompt = getattr(agent, "system_prompt", "")
|
|
193
|
+
description = getattr(agent, "description", f"Strands agent: {agent_name}")
|
|
194
|
+
|
|
195
|
+
# Extract tools
|
|
196
|
+
tools = []
|
|
197
|
+
agent_tools = getattr(agent, "tools", []) or []
|
|
198
|
+
|
|
199
|
+
for tool in agent_tools:
|
|
200
|
+
try:
|
|
201
|
+
# Extract tool info from tool_spec or attributes
|
|
202
|
+
tool_info = _extract_tool_info(tool)
|
|
203
|
+
tool_name = tool_info["name"]
|
|
204
|
+
tool_description = tool_info["description"]
|
|
205
|
+
|
|
206
|
+
# Extract JSON schemas
|
|
207
|
+
parameters_json_schema, response_json_schema = _extract_strands_tool_schema(
|
|
208
|
+
tool
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Analyze function for additional info
|
|
212
|
+
if callable(tool):
|
|
213
|
+
parameters = _analyze_function_parameters(tool)
|
|
214
|
+
response_type = _get_function_return_type(tool)
|
|
215
|
+
language, source_code = _get_function_source(tool)
|
|
216
|
+
else:
|
|
217
|
+
parameters = []
|
|
218
|
+
response_type = "Any"
|
|
219
|
+
language, source_code = "python", f"# Tool object: {tool_name}"
|
|
220
|
+
|
|
221
|
+
tools.append(
|
|
222
|
+
Tool(
|
|
223
|
+
name=tool_name,
|
|
224
|
+
description=tool_description.strip()
|
|
225
|
+
if isinstance(tool_description, str)
|
|
226
|
+
else str(tool_description),
|
|
227
|
+
parameters=parameters,
|
|
228
|
+
parameters_json_schema=parameters_json_schema,
|
|
229
|
+
response=response_type,
|
|
230
|
+
response_json_schema=response_json_schema,
|
|
231
|
+
source=SourceCode(language=language, code=source_code),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f"Could not analyze tool {tool}: {e}")
|
|
236
|
+
|
|
237
|
+
return Agent(
|
|
238
|
+
id=agent_id,
|
|
239
|
+
provider_id="strands",
|
|
240
|
+
name=agent_name,
|
|
241
|
+
instruction=system_prompt,
|
|
242
|
+
description=description,
|
|
243
|
+
tools=tools,
|
|
244
|
+
)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Sondera Harness Hook for Strands Agent SDK integration."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from strands.hooks import HookProvider, HookRegistry
|
|
7
|
+
from strands.hooks.events import (
|
|
8
|
+
AfterInvocationEvent,
|
|
9
|
+
AfterModelCallEvent,
|
|
10
|
+
AfterToolCallEvent,
|
|
11
|
+
BeforeInvocationEvent,
|
|
12
|
+
BeforeModelCallEvent,
|
|
13
|
+
BeforeToolCallEvent,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from sondera.harness import Harness
|
|
17
|
+
from sondera.strands.analyze import format_strands_agent
|
|
18
|
+
from sondera.types import (
|
|
19
|
+
PromptContent,
|
|
20
|
+
Role,
|
|
21
|
+
Stage,
|
|
22
|
+
ToolRequestContent,
|
|
23
|
+
ToolResponseContent,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SonderaHarnessHook(HookProvider):
|
|
30
|
+
"""Sondera Harness Hook for Strands integration.
|
|
31
|
+
|
|
32
|
+
This hook implements the HookProvider protocol to integrate with
|
|
33
|
+
Strands' hook system. It uses dependency injection for the Harness
|
|
34
|
+
instance, allowing flexibility in choosing RemoteHarness or LocalHarness.
|
|
35
|
+
|
|
36
|
+
The hook intercepts agent execution at key lifecycle points:
|
|
37
|
+
- Before/after invocation: Initialize and finalize trajectory
|
|
38
|
+
- Before/after model call: Evaluate model requests and responses
|
|
39
|
+
- Before/after tool call: Evaluate tool calls and results
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
from strands import Agent
|
|
44
|
+
from sondera.strands import SonderaHarnessHook
|
|
45
|
+
from sondera.harness import RemoteHarness
|
|
46
|
+
|
|
47
|
+
# Create harness instance
|
|
48
|
+
harness = RemoteHarness(
|
|
49
|
+
sondera_harness_endpoint="localhost:50051",
|
|
50
|
+
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create hook with harness using dependency injection
|
|
54
|
+
hook = SonderaHarnessHook(harness=harness)
|
|
55
|
+
|
|
56
|
+
# Create Strands agent with hooks
|
|
57
|
+
agent = Agent(
|
|
58
|
+
system_prompt="You are a helpful assistant",
|
|
59
|
+
model="anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
60
|
+
hooks=[hook],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Run agent (hooks will fire automatically)
|
|
64
|
+
response = agent("What is 5 + 3?")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Hook Lifecycle:
|
|
68
|
+
1. BeforeInvocationEvent - Initialize trajectory
|
|
69
|
+
2. BeforeModelCallEvent - Pre-model guardrails
|
|
70
|
+
3. AfterModelCallEvent - Post-model guardrails
|
|
71
|
+
4. BeforeToolCallEvent - Pre-tool guardrails (can cancel with event.cancel_tool)
|
|
72
|
+
5. AfterToolCallEvent - Post-tool guardrails (can modify event.result)
|
|
73
|
+
6. AfterInvocationEvent - Finalize trajectory
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
harness: Harness,
|
|
79
|
+
*,
|
|
80
|
+
logger_instance: logging.Logger | None = None,
|
|
81
|
+
):
|
|
82
|
+
"""Initialize the Strands Harness Hook.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
harness: The Sondera Harness instance to use for policy enforcement.
|
|
86
|
+
Can be RemoteHarness for production or LocalHarness for testing.
|
|
87
|
+
logger_instance: Optional custom logger instance.
|
|
88
|
+
"""
|
|
89
|
+
self._harness = harness
|
|
90
|
+
self._log = logger_instance or logger
|
|
91
|
+
self._strands_agent: Any | None = None
|
|
92
|
+
|
|
93
|
+
# -------------------------------------------------------------------------
|
|
94
|
+
# HookProvider interface - required by Strands
|
|
95
|
+
# -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
|
|
98
|
+
"""Register all Strands lifecycle hooks.
|
|
99
|
+
|
|
100
|
+
This method is called by Strands when the agent is constructed with
|
|
101
|
+
hooks=[SonderaHarnessHook(harness=...)].
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
registry: The Strands hook registry
|
|
105
|
+
**kwargs: Additional keyword arguments (unused)
|
|
106
|
+
"""
|
|
107
|
+
registry.add_callback(BeforeInvocationEvent, self._on_before_invocation)
|
|
108
|
+
registry.add_callback(AfterInvocationEvent, self._on_after_invocation)
|
|
109
|
+
registry.add_callback(BeforeModelCallEvent, self._on_before_model_call)
|
|
110
|
+
registry.add_callback(AfterModelCallEvent, self._on_after_model_call)
|
|
111
|
+
registry.add_callback(BeforeToolCallEvent, self._on_before_tool_call)
|
|
112
|
+
registry.add_callback(AfterToolCallEvent, self._on_after_tool_call)
|
|
113
|
+
|
|
114
|
+
self._log.info("[SonderaHarness] Registered Strands hooks")
|
|
115
|
+
|
|
116
|
+
# -------------------------------------------------------------------------
|
|
117
|
+
# Invocation Callbacks
|
|
118
|
+
# -------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
async def _on_before_invocation(self, event: BeforeInvocationEvent) -> None:
|
|
121
|
+
"""Callback for BeforeInvocationEvent - Initialize trajectory."""
|
|
122
|
+
try:
|
|
123
|
+
self._strands_agent = event.agent
|
|
124
|
+
agent = format_strands_agent(event.agent)
|
|
125
|
+
await self._harness.initialize(agent=agent)
|
|
126
|
+
self._log.debug(
|
|
127
|
+
f"[SonderaHarness] Initialized trajectory {self._harness.trajectory_id}"
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self._log.error(
|
|
131
|
+
f"[SonderaHarness] Error in before_invocation: {e}", exc_info=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def _on_after_invocation(self, event: AfterInvocationEvent) -> None:
|
|
135
|
+
"""Callback for AfterInvocationEvent - Finalize trajectory."""
|
|
136
|
+
try:
|
|
137
|
+
trajectory_id = self._harness.trajectory_id
|
|
138
|
+
await self._harness.finalize()
|
|
139
|
+
self._log.info(f"[SonderaHarness] Finalized trajectory {trajectory_id}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
self._log.error(
|
|
142
|
+
f"[SonderaHarness] Error in after_invocation: {e}", exc_info=True
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
# Model Callbacks
|
|
147
|
+
# -------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
async def _on_before_model_call(self, event: BeforeModelCallEvent) -> None:
|
|
150
|
+
"""Callback for BeforeModelCallEvent - Pre-model guardrails."""
|
|
151
|
+
try:
|
|
152
|
+
if not self._harness.trajectory_id:
|
|
153
|
+
self._log.warning(
|
|
154
|
+
"[SonderaHarness] No active trajectory for before_model_call"
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
content = self._extract_text_from_event(event)
|
|
159
|
+
adjudication = await self._harness.adjudicate(
|
|
160
|
+
Stage.PRE_MODEL, Role.MODEL, PromptContent(text=content)
|
|
161
|
+
)
|
|
162
|
+
self._log.info(
|
|
163
|
+
f"[SonderaHarness] Before model adjudication for trajectory {self._harness.trajectory_id}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if adjudication.is_denied:
|
|
167
|
+
self._log.warning(
|
|
168
|
+
f"[SonderaHarness] Model call blocked: {adjudication.reason}"
|
|
169
|
+
)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self._log.error(
|
|
172
|
+
f"[SonderaHarness] Error in before_model_call: {e}", exc_info=True
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def _on_after_model_call(self, event: AfterModelCallEvent) -> None:
|
|
176
|
+
"""Callback for AfterModelCallEvent - Post-model guardrails."""
|
|
177
|
+
try:
|
|
178
|
+
if not self._harness.trajectory_id:
|
|
179
|
+
self._log.warning(
|
|
180
|
+
"[SonderaHarness] No active trajectory for after_model_call"
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
content = self._extract_text_from_event(event)
|
|
185
|
+
if not content:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
adjudication = await self._harness.adjudicate(
|
|
189
|
+
Stage.POST_MODEL, Role.MODEL, PromptContent(text=content)
|
|
190
|
+
)
|
|
191
|
+
self._log.info(
|
|
192
|
+
f"[SonderaHarness] After model adjudication for trajectory {self._harness.trajectory_id}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if adjudication.is_denied:
|
|
196
|
+
self._log.warning(
|
|
197
|
+
f"[SonderaHarness] Model response blocked: {adjudication.reason}"
|
|
198
|
+
)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self._log.error(
|
|
201
|
+
f"[SonderaHarness] Error in after_model_call: {e}", exc_info=True
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# -------------------------------------------------------------------------
|
|
205
|
+
# Tool Callbacks
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
async def _on_before_tool_call(self, event: BeforeToolCallEvent) -> None:
|
|
209
|
+
"""Callback for BeforeToolCallEvent - Pre-tool guardrails."""
|
|
210
|
+
try:
|
|
211
|
+
if not self._harness.trajectory_id:
|
|
212
|
+
self._log.warning(
|
|
213
|
+
"[SonderaHarness] No active trajectory for before_tool_call"
|
|
214
|
+
)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
tool_name = event.tool_use.get("name", "unknown")
|
|
218
|
+
tool_input = event.tool_use.get("input", {})
|
|
219
|
+
|
|
220
|
+
adjudication = await self._harness.adjudicate(
|
|
221
|
+
Stage.PRE_TOOL,
|
|
222
|
+
Role.TOOL,
|
|
223
|
+
ToolRequestContent(
|
|
224
|
+
tool_id=tool_name,
|
|
225
|
+
args=tool_input
|
|
226
|
+
if isinstance(tool_input, dict)
|
|
227
|
+
else {"input": tool_input},
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
self._log.info(
|
|
231
|
+
f"[SonderaHarness] Before tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if adjudication.is_denied:
|
|
235
|
+
# Cancel the tool call using Strands' cancel_tool mechanism
|
|
236
|
+
event.cancel_tool = f"Tool blocked by policy: {adjudication.reason}"
|
|
237
|
+
self._log.warning(
|
|
238
|
+
f"[SonderaHarness] Blocked tool '{tool_name}': {adjudication.reason}"
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self._log.error(
|
|
242
|
+
f"[SonderaHarness] Error in before_tool_call: {e}", exc_info=True
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
async def _on_after_tool_call(self, event: AfterToolCallEvent) -> None:
|
|
246
|
+
"""Callback for AfterToolCallEvent - Post-tool guardrails."""
|
|
247
|
+
try:
|
|
248
|
+
if not self._harness.trajectory_id:
|
|
249
|
+
self._log.warning(
|
|
250
|
+
"[SonderaHarness] No active trajectory for after_tool_call"
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
tool_name = event.tool_use.get("name", "unknown")
|
|
255
|
+
|
|
256
|
+
adjudication = await self._harness.adjudicate(
|
|
257
|
+
Stage.POST_TOOL,
|
|
258
|
+
Role.TOOL,
|
|
259
|
+
ToolResponseContent(tool_id=tool_name, response=event.result),
|
|
260
|
+
)
|
|
261
|
+
self._log.info(
|
|
262
|
+
f"[SonderaHarness] After tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if adjudication.is_denied:
|
|
266
|
+
# Modify the result to indicate policy violation
|
|
267
|
+
event.result = {
|
|
268
|
+
"content": [
|
|
269
|
+
{"text": f"Tool result blocked: {adjudication.reason}"}
|
|
270
|
+
],
|
|
271
|
+
"status": "error",
|
|
272
|
+
"toolUseId": event.tool_use.get("toolUseId", ""),
|
|
273
|
+
}
|
|
274
|
+
self._log.warning(
|
|
275
|
+
f"[SonderaHarness] Tool result blocked: {adjudication.reason}"
|
|
276
|
+
)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self._log.error(
|
|
279
|
+
f"[SonderaHarness] Error in after_tool_call: {e}", exc_info=True
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# -------------------------------------------------------------------------
|
|
283
|
+
# Helper methods
|
|
284
|
+
# -------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def _extract_text_from_event(self, event: Any) -> str:
|
|
287
|
+
"""Extract text content from Strands events for adjudication.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
event: The Strands hook event
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Extracted text content for adjudication
|
|
294
|
+
"""
|
|
295
|
+
if isinstance(event, BeforeModelCallEvent):
|
|
296
|
+
try:
|
|
297
|
+
if (
|
|
298
|
+
hasattr(event.agent, "conversation_manager")
|
|
299
|
+
and event.agent.conversation_manager
|
|
300
|
+
):
|
|
301
|
+
messages = []
|
|
302
|
+
conv_mgr = event.agent.conversation_manager
|
|
303
|
+
messages_attr = getattr(conv_mgr, "messages", None)
|
|
304
|
+
if messages_attr:
|
|
305
|
+
for msg in messages_attr:
|
|
306
|
+
role = (
|
|
307
|
+
msg.get("role", "unknown")
|
|
308
|
+
if hasattr(msg, "get")
|
|
309
|
+
else getattr(msg, "role", "unknown")
|
|
310
|
+
)
|
|
311
|
+
content = (
|
|
312
|
+
msg.get("content", "")
|
|
313
|
+
if hasattr(msg, "get")
|
|
314
|
+
else getattr(msg, "content", "")
|
|
315
|
+
)
|
|
316
|
+
messages.append(f"{role}: {content}")
|
|
317
|
+
if messages:
|
|
318
|
+
return "\n".join(messages)
|
|
319
|
+
except (AttributeError, TypeError) as e:
|
|
320
|
+
self._log.debug(f"Could not extract conversation: {e}")
|
|
321
|
+
return ""
|
|
322
|
+
|
|
323
|
+
if isinstance(event, AfterModelCallEvent):
|
|
324
|
+
if event.stop_response and event.stop_response.message:
|
|
325
|
+
content = (
|
|
326
|
+
event.stop_response.message.get("content", "")
|
|
327
|
+
if hasattr(event.stop_response.message, "get")
|
|
328
|
+
else getattr(event.stop_response.message, "content", "")
|
|
329
|
+
)
|
|
330
|
+
return str(content)
|
|
331
|
+
return ""
|
|
332
|
+
|
|
333
|
+
return ""
|
sondera/tui/__init__.py
ADDED
|
File without changes
|