Skip to content

Commit b21a6de

Browse files
feat: add workflow context, escalation routing, semantic rules
1 parent 075abc5 commit b21a6de

14 files changed

Lines changed: 1058 additions & 14 deletions

File tree

examples/agent_pipeline/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from gateframe.core.contract import ValidationContract
2+
from gateframe.core.escalation import EscalationRoute
3+
from gateframe.core.failure import FailureMode
4+
from gateframe.rules.semantic import SemanticRule
5+
from gateframe.rules.structural import StructuralRule
6+
7+
from .models import ActionPlanOutput, ClassificationOutput, ExecutionOutput, SummaryOutput
8+
9+
classification_contract = ValidationContract(
10+
name="classification",
11+
rules=[
12+
StructuralRule(schema=ClassificationOutput),
13+
SemanticRule(
14+
check=lambda output, **ctx: output.get("confidence", 0) >= 0.5,
15+
name="minimum_confidence",
16+
description="Classification confidence must be at least 0.5",
17+
failure_mode=FailureMode.SOFT_FAIL,
18+
failure_message="Classification confidence below 0.5 threshold.",
19+
),
20+
],
21+
on_hard_fail=EscalationRoute.ABORT,
22+
on_soft_fail=EscalationRoute.FLAG_AND_CONTINUE,
23+
)
24+
25+
action_plan_contract = ValidationContract(
26+
name="action_plan",
27+
rules=[
28+
StructuralRule(schema=ActionPlanOutput),
29+
SemanticRule(
30+
check=lambda output, **ctx: len(output.get("steps", [])) > 0,
31+
name="has_steps",
32+
description="Action plan must have at least one step",
33+
failure_mode=FailureMode.HARD_FAIL,
34+
failure_message="Action plan has no steps.",
35+
),
36+
SemanticRule(
37+
check=lambda output, **ctx: (
38+
output.get("requires_approval", False)
39+
if output.get("estimated_risk") == "high"
40+
else True
41+
),
42+
name="high_risk_needs_approval",
43+
description="High risk actions must require approval",
44+
failure_mode=FailureMode.SOFT_FAIL,
45+
failure_message="High risk plan does not require approval.",
46+
),
47+
],
48+
on_hard_fail=EscalationRoute.HUMAN_REVIEW,
49+
on_soft_fail=EscalationRoute.FLAG_AND_CONTINUE,
50+
)
51+
52+
execution_contract = ValidationContract(
53+
name="execution",
54+
rules=[
55+
StructuralRule(schema=ExecutionOutput),
56+
SemanticRule(
57+
check=lambda output, **ctx: (
58+
output.get("success", False) or len(output.get("details", "")) > 10
59+
),
60+
name="failure_explained",
61+
description="Failed executions must have detailed explanation",
62+
failure_mode=FailureMode.SOFT_FAIL,
63+
failure_message="Execution failed without sufficient explanation.",
64+
),
65+
],
66+
)
67+
68+
summary_contract = ValidationContract(
69+
name="summary",
70+
rules=[
71+
StructuralRule(schema=SummaryOutput),
72+
],
73+
)

examples/agent_pipeline/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pydantic import BaseModel
2+
3+
4+
class ClassificationOutput(BaseModel):
5+
category: str
6+
confidence: float
7+
reasoning: str
8+
9+
10+
class ActionPlanOutput(BaseModel):
11+
steps: list[str]
12+
estimated_risk: str
13+
requires_approval: bool
14+
15+
16+
class ExecutionOutput(BaseModel):
17+
action_taken: str
18+
success: bool
19+
details: str
20+
21+
22+
class SummaryOutput(BaseModel):
23+
outcome: str
24+
actions_completed: list[str]
25+
follow_up_needed: bool

examples/agent_pipeline/run.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Agent pipeline example: 4-step workflow with confidence degradation and escalation."""
2+
3+
from gateframe.audit.log import AuditLog
4+
from gateframe.core.context import WorkflowContext
5+
from gateframe.core.escalation import EscalationRouter
6+
7+
from .contracts import (
8+
action_plan_contract,
9+
classification_contract,
10+
execution_contract,
11+
summary_contract,
12+
)
13+
14+
15+
def main() -> None:
16+
ctx = WorkflowContext(
17+
workflow_id="incident_response_001",
18+
escalation_threshold=0.5,
19+
)
20+
router = EscalationRouter()
21+
audit = AuditLog()
22+
23+
# Step 1: Classification -- passes cleanly
24+
step1_output = {
25+
"category": "infrastructure",
26+
"confidence": 0.92,
27+
"reasoning": "CPU usage spike correlated with deployment at 14:32 UTC",
28+
}
29+
result1 = classification_contract.validate(step1_output)
30+
ctx.update(result1)
31+
audit.record(result1, workflow_context=ctx)
32+
print(f"Step 1 (classification): passed={result1.passed}, confidence={ctx.confidence:.2f}")
33+
34+
# Step 2: Action plan -- soft fail (high risk without approval)
35+
step2_output = {
36+
"steps": ["Roll back deployment", "Check error logs", "Notify team"],
37+
"estimated_risk": "high",
38+
"requires_approval": False, # this triggers soft fail
39+
}
40+
result2 = action_plan_contract.validate(step2_output)
41+
ctx.update(result2)
42+
audit.record(result2, workflow_context=ctx)
43+
print(f"Step 2 (action_plan): passed={result2.passed}, confidence={ctx.confidence:.2f}")
44+
45+
# Step 3: Execution -- soft fail (failed without explanation)
46+
step3_output = {
47+
"action_taken": "Attempted rollback",
48+
"success": False,
49+
"details": "Timeout", # too short, triggers soft fail
50+
}
51+
result3 = execution_contract.validate(step3_output)
52+
ctx.update(result3)
53+
audit.record(result3, workflow_context=ctx)
54+
print(f"Step 3 (execution): passed={result3.passed}, confidence={ctx.confidence:.2f}")
55+
56+
# Step 4: Summary -- passes structurally
57+
step4_output = {
58+
"outcome": "Partial resolution",
59+
"actions_completed": ["Rollback attempted", "Logs checked"],
60+
"follow_up_needed": True,
61+
}
62+
result4 = summary_contract.validate(step4_output)
63+
ctx.update(result4)
64+
audit.record(result4, workflow_context=ctx)
65+
print(f"Step 4 (summary): passed={result4.passed}, confidence={ctx.confidence:.2f}")
66+
67+
# Check threshold
68+
print(f"\nThreshold breached: {ctx.threshold_breached}")
69+
if ctx.threshold_breached:
70+
escalation = router.route_threshold_breach(ctx)
71+
if escalation:
72+
print(f"Escalation: {escalation.route.value} -- {escalation.reason}")
73+
74+
# Audit trail
75+
print(f"\nAudit entries: {len(audit.entries)}")
76+
for entry in audit.entries:
77+
data = entry.to_dict()
78+
print(f" [{data['contract_name']}] passed={data['passed']}", end="")
79+
if data.get("confidence") is not None:
80+
print(f", confidence={data['confidence']:.2f}", end="")
81+
print()
82+
83+
84+
if __name__ == "__main__":
85+
main()

gateframe/audit/log.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import annotations
22

33
from datetime import datetime, timezone
4-
from typing import Any
4+
from typing import TYPE_CHECKING, Any
55

66
import structlog
77

88
from gateframe.core.contract import ValidationResult
99

10+
if TYPE_CHECKING:
11+
from gateframe.core.context import WorkflowContext
12+
1013
logger = structlog.get_logger()
1114

1215

@@ -18,9 +21,15 @@ class AuditEntry:
1821
"rules_applied",
1922
"rules_failed",
2023
"failures",
24+
"workflow_id",
25+
"confidence",
2126
)
2227

23-
def __init__(self, result: ValidationResult) -> None:
28+
def __init__(
29+
self,
30+
result: ValidationResult,
31+
workflow_context: WorkflowContext | None = None,
32+
) -> None:
2433
self.timestamp = datetime.now(timezone.utc)
2534
self.contract_name = result.contract_name
2635
self.passed = result.passed
@@ -34,32 +43,45 @@ def __init__(self, result: ValidationResult) -> None:
3443
}
3544
for f in result.failures
3645
]
46+
self.workflow_id = workflow_context.workflow_id if workflow_context else None
47+
self.confidence = workflow_context.confidence if workflow_context else None
3748

3849
def to_dict(self) -> dict[str, Any]:
39-
return {
50+
data: dict[str, Any] = {
4051
"timestamp": self.timestamp.isoformat(),
4152
"contract_name": self.contract_name,
4253
"passed": self.passed,
4354
"rules_applied": self.rules_applied,
4455
"rules_failed": self.rules_failed,
4556
"failures": self.failures,
4657
}
58+
if self.workflow_id is not None:
59+
data["workflow_id"] = self.workflow_id
60+
data["confidence"] = self.confidence
61+
return data
4762

4863

4964
class AuditLog:
5065
def __init__(self) -> None:
5166
self._entries: list[AuditEntry] = []
5267

53-
def record(self, result: ValidationResult) -> None:
54-
entry = AuditEntry(result)
68+
def record(
69+
self,
70+
result: ValidationResult,
71+
workflow_context: WorkflowContext | None = None,
72+
) -> None:
73+
entry = AuditEntry(result, workflow_context=workflow_context)
5574
self._entries.append(entry)
56-
logger.info(
57-
"validation_event",
58-
contract=entry.contract_name,
59-
passed=entry.passed,
60-
rules_applied=entry.rules_applied,
61-
rules_failed=entry.rules_failed,
62-
)
75+
log_kwargs: dict[str, Any] = {
76+
"contract": entry.contract_name,
77+
"passed": entry.passed,
78+
"rules_applied": entry.rules_applied,
79+
"rules_failed": entry.rules_failed,
80+
}
81+
if entry.workflow_id is not None:
82+
log_kwargs["workflow_id"] = entry.workflow_id
83+
log_kwargs["confidence"] = entry.confidence
84+
logger.info("validation_event", **log_kwargs)
6385

6486
@property
6587
def entries(self) -> list[AuditEntry]:

0 commit comments

Comments
 (0)