agent-control-models 7.3.0__tar.gz → 7.3.2__tar.gz
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_control_models-7.3.0 → agent_control_models-7.3.2}/PKG-INFO +1 -1
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/pyproject.toml +1 -1
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/__init__.py +10 -0
- agent_control_models-7.3.2/src/agent_control_models/actions.py +53 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/controls.py +13 -2
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/observability.py +28 -14
- agent_control_models-7.3.2/tests/test_actions.py +60 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/.gitignore +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/README.md +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/agent.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/base.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/errors.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/evaluation.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/health.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/policy.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/py.typed +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/server.py +0 -0
- {agent_control_models-7.3.0 → agent_control_models-7.3.2}/tests/test_controls.py +0 -0
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/__init__.py
RENAMED
|
@@ -7,6 +7,12 @@ try:
|
|
|
7
7
|
except PackageNotFoundError:
|
|
8
8
|
__version__ = "0.0.0.dev"
|
|
9
9
|
|
|
10
|
+
from .actions import (
|
|
11
|
+
ActionDecision,
|
|
12
|
+
expand_action_filter,
|
|
13
|
+
normalize_action,
|
|
14
|
+
normalize_action_list,
|
|
15
|
+
)
|
|
10
16
|
from .agent import (
|
|
11
17
|
BUILTIN_STEP_TYPES,
|
|
12
18
|
STEP_TYPE_LLM,
|
|
@@ -91,6 +97,7 @@ __all__ = [
|
|
|
91
97
|
"STEP_TYPE_TOOL",
|
|
92
98
|
"STEP_TYPE_LLM",
|
|
93
99
|
"BUILTIN_STEP_TYPES",
|
|
100
|
+
"ActionDecision",
|
|
94
101
|
# Policy
|
|
95
102
|
"Policy",
|
|
96
103
|
# Evaluation
|
|
@@ -107,6 +114,9 @@ __all__ = [
|
|
|
107
114
|
"EvaluatorSpec",
|
|
108
115
|
"EvaluatorResult",
|
|
109
116
|
"SteeringContext",
|
|
117
|
+
"normalize_action",
|
|
118
|
+
"normalize_action_list",
|
|
119
|
+
"expand_action_filter",
|
|
110
120
|
# Error models
|
|
111
121
|
"ProblemDetail",
|
|
112
122
|
"ErrorCode",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shared control-action types and normalization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Literal, cast
|
|
7
|
+
|
|
8
|
+
type ActionDecision = Literal["deny", "steer", "observe"]
|
|
9
|
+
|
|
10
|
+
_OBSERVE_ACTION_ALIASES = frozenset({"allow", "observe", "warn", "log"})
|
|
11
|
+
_ACTION_QUERY_EXPANSION: dict[ActionDecision, tuple[str, ...]] = {
|
|
12
|
+
"deny": ("deny",),
|
|
13
|
+
"steer": ("steer",),
|
|
14
|
+
"observe": ("observe", "allow", "warn", "log"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_action(action: str) -> ActionDecision:
|
|
19
|
+
"""Normalize a public or legacy action name to the canonical action."""
|
|
20
|
+
if action in _OBSERVE_ACTION_ALIASES:
|
|
21
|
+
return "observe"
|
|
22
|
+
if action in ("deny", "steer"):
|
|
23
|
+
return cast(ActionDecision, action)
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"Invalid action. Expected one of: deny, steer, observe "
|
|
26
|
+
"(legacy aliases allow/warn/log are also accepted temporarily)."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_action_list(actions: Sequence[str]) -> list[ActionDecision]:
|
|
31
|
+
"""Normalize a list of actions while preserving order and removing duplicates."""
|
|
32
|
+
normalized: list[ActionDecision] = []
|
|
33
|
+
seen: set[ActionDecision] = set()
|
|
34
|
+
for action in actions:
|
|
35
|
+
canonical = normalize_action(action)
|
|
36
|
+
if canonical in seen:
|
|
37
|
+
continue
|
|
38
|
+
seen.add(canonical)
|
|
39
|
+
normalized.append(canonical)
|
|
40
|
+
return normalized
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def expand_action_filter(actions: Sequence[ActionDecision]) -> list[str]:
|
|
44
|
+
"""Expand canonical action filters to include legacy stored event values."""
|
|
45
|
+
expanded: list[str] = []
|
|
46
|
+
seen: set[str] = set()
|
|
47
|
+
for action in actions:
|
|
48
|
+
for candidate in _ACTION_QUERY_EXPANSION[action]:
|
|
49
|
+
if candidate in seen:
|
|
50
|
+
continue
|
|
51
|
+
seen.add(candidate)
|
|
52
|
+
expanded.append(candidate)
|
|
53
|
+
return expanded
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/controls.py
RENAMED
|
@@ -10,6 +10,7 @@ from uuid import uuid4
|
|
|
10
10
|
import re2
|
|
11
11
|
from pydantic import ConfigDict, Field, ValidationInfo, field_validator, model_validator
|
|
12
12
|
|
|
13
|
+
from .actions import ActionDecision, normalize_action
|
|
13
14
|
from .base import BaseModel
|
|
14
15
|
|
|
15
16
|
|
|
@@ -265,7 +266,7 @@ class SteeringContext(BaseModel):
|
|
|
265
266
|
class ControlAction(BaseModel):
|
|
266
267
|
"""What to do when control matches."""
|
|
267
268
|
|
|
268
|
-
decision:
|
|
269
|
+
decision: ActionDecision = Field(
|
|
269
270
|
..., description="Action to take when control is triggered"
|
|
270
271
|
)
|
|
271
272
|
steering_context: SteeringContext | None = Field(
|
|
@@ -277,6 +278,11 @@ class ControlAction(BaseModel):
|
|
|
277
278
|
)
|
|
278
279
|
)
|
|
279
280
|
|
|
281
|
+
@field_validator("decision", mode="before")
|
|
282
|
+
@classmethod
|
|
283
|
+
def normalize_decision(cls, value: str) -> ActionDecision:
|
|
284
|
+
return normalize_action(value)
|
|
285
|
+
|
|
280
286
|
|
|
281
287
|
MAX_CONDITION_DEPTH = 6
|
|
282
288
|
|
|
@@ -649,7 +655,7 @@ class ControlMatch(BaseModel):
|
|
|
649
655
|
)
|
|
650
656
|
control_id: int = Field(..., description="Database ID of the control")
|
|
651
657
|
control_name: str = Field(..., description="Name of the control")
|
|
652
|
-
action:
|
|
658
|
+
action: ActionDecision = Field(
|
|
653
659
|
..., description="Action configured for this control"
|
|
654
660
|
)
|
|
655
661
|
result: EvaluatorResult = Field(
|
|
@@ -659,3 +665,8 @@ class ControlMatch(BaseModel):
|
|
|
659
665
|
None,
|
|
660
666
|
description="Steering context for steer actions if configured"
|
|
661
667
|
)
|
|
668
|
+
|
|
669
|
+
@field_validator("action", mode="before")
|
|
670
|
+
@classmethod
|
|
671
|
+
def normalize_action_value(cls, value: str) -> ActionDecision:
|
|
672
|
+
return normalize_action(value)
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/observability.py
RENAMED
|
@@ -14,6 +14,7 @@ from uuid import uuid4
|
|
|
14
14
|
|
|
15
15
|
from pydantic import Field, field_validator
|
|
16
16
|
|
|
17
|
+
from .actions import ActionDecision, normalize_action, normalize_action_list
|
|
17
18
|
from .agent import AGENT_NAME_MIN_LENGTH, AGENT_NAME_PATTERN, normalize_agent_name
|
|
18
19
|
from .base import BaseModel
|
|
19
20
|
|
|
@@ -42,7 +43,7 @@ class ControlExecutionEvent(BaseModel):
|
|
|
42
43
|
control_name: Name of the control (denormalized for queries)
|
|
43
44
|
check_stage: "pre" (before execution) or "post" (after execution)
|
|
44
45
|
applies_to: "llm_call" or "tool_call"
|
|
45
|
-
action: The action taken (
|
|
46
|
+
action: The action taken (deny, steer, observe)
|
|
46
47
|
matched: Whether the control evaluator matched
|
|
47
48
|
confidence: Confidence score from the evaluator (0.0-1.0)
|
|
48
49
|
timestamp: When the control was executed (UTC)
|
|
@@ -90,7 +91,7 @@ class ControlExecutionEvent(BaseModel):
|
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
# Result
|
|
93
|
-
action:
|
|
94
|
+
action: ActionDecision = Field(
|
|
94
95
|
..., description="Action taken by the control"
|
|
95
96
|
)
|
|
96
97
|
matched: bool = Field(
|
|
@@ -160,6 +161,11 @@ class ControlExecutionEvent(BaseModel):
|
|
|
160
161
|
def validate_and_normalize_agent_name(cls, value: str) -> str:
|
|
161
162
|
return normalize_agent_name(str(value))
|
|
162
163
|
|
|
164
|
+
@field_validator("action", mode="before")
|
|
165
|
+
@classmethod
|
|
166
|
+
def normalize_event_action(cls, value: str) -> ActionDecision:
|
|
167
|
+
return normalize_action(value)
|
|
168
|
+
|
|
163
169
|
model_config = {
|
|
164
170
|
"json_schema_extra": {
|
|
165
171
|
"examples": [
|
|
@@ -265,7 +271,7 @@ class EventQueryRequest(BaseModel):
|
|
|
265
271
|
control_execution_id: Filter by specific event ID
|
|
266
272
|
agent_name: Filter by agent identifier
|
|
267
273
|
control_ids: Filter by control IDs
|
|
268
|
-
actions: Filter by actions (
|
|
274
|
+
actions: Filter by actions (deny, steer, observe)
|
|
269
275
|
matched: Filter by matched status
|
|
270
276
|
check_stages: Filter by check stages (pre, post)
|
|
271
277
|
applies_to: Filter by call type (llm_call, tool_call)
|
|
@@ -293,7 +299,7 @@ class EventQueryRequest(BaseModel):
|
|
|
293
299
|
control_ids: list[int] | None = Field(
|
|
294
300
|
default=None, description="Filter by control IDs"
|
|
295
301
|
)
|
|
296
|
-
actions: list[
|
|
302
|
+
actions: list[ActionDecision] | None = Field(
|
|
297
303
|
default=None, description="Filter by actions"
|
|
298
304
|
)
|
|
299
305
|
matched: bool | None = Field(default=None, description="Filter by matched status")
|
|
@@ -318,7 +324,7 @@ class EventQueryRequest(BaseModel):
|
|
|
318
324
|
{"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"},
|
|
319
325
|
{
|
|
320
326
|
"agent_name": "my-agent",
|
|
321
|
-
"actions": ["deny", "
|
|
327
|
+
"actions": ["deny", "observe"],
|
|
322
328
|
"start_time": "2025-01-09T00:00:00Z",
|
|
323
329
|
"limit": 50,
|
|
324
330
|
},
|
|
@@ -335,6 +341,15 @@ class EventQueryRequest(BaseModel):
|
|
|
335
341
|
return None
|
|
336
342
|
return normalize_agent_name(str(value))
|
|
337
343
|
|
|
344
|
+
@field_validator("actions", mode="before")
|
|
345
|
+
@classmethod
|
|
346
|
+
def normalize_actions_filter(
|
|
347
|
+
cls, value: list[str] | None
|
|
348
|
+
) -> list[ActionDecision] | None:
|
|
349
|
+
if value is None:
|
|
350
|
+
return None
|
|
351
|
+
return normalize_action_list(value)
|
|
352
|
+
|
|
338
353
|
|
|
339
354
|
class EventQueryResponse(BaseModel):
|
|
340
355
|
"""
|
|
@@ -368,14 +383,15 @@ class ControlStats(BaseModel):
|
|
|
368
383
|
execution_count: Total number of executions
|
|
369
384
|
match_count: Number of times the control matched
|
|
370
385
|
non_match_count: Number of times the control did not match
|
|
371
|
-
allow_count: Number of allow actions
|
|
372
386
|
deny_count: Number of deny actions
|
|
373
387
|
steer_count: Number of steer actions
|
|
374
|
-
|
|
375
|
-
log_count: Number of log actions
|
|
388
|
+
observe_count: Number of observe actions
|
|
376
389
|
error_count: Number of errors during evaluation
|
|
377
390
|
avg_confidence: Average confidence score
|
|
378
391
|
avg_duration_ms: Average execution duration in milliseconds
|
|
392
|
+
|
|
393
|
+
Invariant:
|
|
394
|
+
deny_count + steer_count + observe_count == match_count
|
|
379
395
|
"""
|
|
380
396
|
|
|
381
397
|
control_id: int = Field(..., description="Control ID")
|
|
@@ -383,11 +399,9 @@ class ControlStats(BaseModel):
|
|
|
383
399
|
execution_count: int = Field(..., ge=0, description="Total executions")
|
|
384
400
|
match_count: int = Field(..., ge=0, description="Total matches")
|
|
385
401
|
non_match_count: int = Field(..., ge=0, description="Total non-matches")
|
|
386
|
-
allow_count: int = Field(..., ge=0, description="Allow actions")
|
|
387
402
|
deny_count: int = Field(..., ge=0, description="Deny actions")
|
|
388
403
|
steer_count: int = Field(..., ge=0, description="Steer actions")
|
|
389
|
-
|
|
390
|
-
log_count: int = Field(..., ge=0, description="Log actions")
|
|
404
|
+
observe_count: int = Field(..., ge=0, description="Observe actions")
|
|
391
405
|
error_count: int = Field(..., ge=0, description="Evaluation errors")
|
|
392
406
|
avg_confidence: float = Field(..., ge=0.0, le=1.0, description="Average confidence")
|
|
393
407
|
avg_duration_ms: float | None = Field(
|
|
@@ -460,7 +474,7 @@ class TimeseriesBucket(BaseModel):
|
|
|
460
474
|
error_count: int = Field(..., ge=0, description="Errors in bucket")
|
|
461
475
|
action_counts: dict[str, int] = Field(
|
|
462
476
|
default_factory=dict,
|
|
463
|
-
description="Action breakdown: {
|
|
477
|
+
description="Action breakdown: {deny, steer, observe}",
|
|
464
478
|
)
|
|
465
479
|
avg_confidence: float | None = Field(
|
|
466
480
|
default=None, ge=0.0, le=1.0, description="Average confidence score"
|
|
@@ -476,7 +490,7 @@ class StatsTotals(BaseModel):
|
|
|
476
490
|
|
|
477
491
|
Invariant: execution_count = match_count + non_match_count + error_count
|
|
478
492
|
|
|
479
|
-
Matches have actions (
|
|
493
|
+
Matches have actions (deny, steer, observe) tracked in action_counts.
|
|
480
494
|
sum(action_counts.values()) == match_count
|
|
481
495
|
|
|
482
496
|
Attributes:
|
|
@@ -494,7 +508,7 @@ class StatsTotals(BaseModel):
|
|
|
494
508
|
error_count: int = Field(default=0, ge=0, description="Total errors")
|
|
495
509
|
action_counts: dict[str, int] = Field(
|
|
496
510
|
default_factory=dict,
|
|
497
|
-
description="Action breakdown for matches: {
|
|
511
|
+
description="Action breakdown for matches: {deny, steer, observe}",
|
|
498
512
|
)
|
|
499
513
|
timeseries: list[TimeseriesBucket] | None = Field(
|
|
500
514
|
default=None,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for shared control-action compatibility behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from agent_control_models import (
|
|
7
|
+
ControlAction,
|
|
8
|
+
ControlExecutionEvent,
|
|
9
|
+
ControlMatch,
|
|
10
|
+
EventQueryRequest,
|
|
11
|
+
EvaluatorResult,
|
|
12
|
+
expand_action_filter,
|
|
13
|
+
)
|
|
14
|
+
from pydantic import ValidationError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_event_query_actions_normalize_and_expand_for_legacy_observability() -> None:
|
|
18
|
+
# Given: a query that mixes canonical and legacy advisory action names
|
|
19
|
+
query = EventQueryRequest(
|
|
20
|
+
actions=["warn", "observe", "deny", "log", "deny", "steer", "allow", "steer"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# When: expanding the normalized public action filter for stored event rows
|
|
24
|
+
expanded = expand_action_filter(query.actions or [])
|
|
25
|
+
|
|
26
|
+
# Then: the public filter is canonicalized, deduped, and expanded for legacy rows
|
|
27
|
+
assert query.actions == ["observe", "deny", "steer"]
|
|
28
|
+
assert expanded == ["observe", "allow", "warn", "log", "deny", "steer"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_invalid_action_is_rejected_across_public_model_boundaries() -> None:
|
|
32
|
+
# Given: the same invalid action at each public model boundary
|
|
33
|
+
invalid_action = "block"
|
|
34
|
+
invalid_builders = [
|
|
35
|
+
lambda: ControlAction.model_validate({"decision": invalid_action}),
|
|
36
|
+
lambda: ControlMatch(
|
|
37
|
+
control_id=123,
|
|
38
|
+
control_name="pii-check",
|
|
39
|
+
action=invalid_action,
|
|
40
|
+
result=EvaluatorResult(matched=True, confidence=0.9),
|
|
41
|
+
),
|
|
42
|
+
lambda: ControlExecutionEvent(
|
|
43
|
+
trace_id="trace-123",
|
|
44
|
+
span_id="span-123",
|
|
45
|
+
agent_name="test-agent",
|
|
46
|
+
control_id=123,
|
|
47
|
+
control_name="pii-check",
|
|
48
|
+
check_stage="pre",
|
|
49
|
+
applies_to="llm_call",
|
|
50
|
+
action=invalid_action,
|
|
51
|
+
matched=True,
|
|
52
|
+
confidence=0.9,
|
|
53
|
+
),
|
|
54
|
+
lambda: EventQueryRequest(actions=[invalid_action]),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for build_invalid_model in invalid_builders:
|
|
58
|
+
# When / Then: validation fails before the invalid action can enter the system
|
|
59
|
+
with pytest.raises(ValidationError, match="Invalid action"):
|
|
60
|
+
build_invalid_model()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/errors.py
RENAMED
|
File without changes
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/evaluation.py
RENAMED
|
File without changes
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/health.py
RENAMED
|
File without changes
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/policy.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_control_models-7.3.0 → agent_control_models-7.3.2}/src/agent_control_models/server.py
RENAMED
|
File without changes
|
|
File without changes
|