pawly 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.
- pawly/__init__.py +154 -0
- pawly/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/__pycache__/action_selection.cpython-311.pyc +0 -0
- pawly/__pycache__/action_selection.cpython-313.pyc +0 -0
- pawly/__pycache__/api.cpython-311.pyc +0 -0
- pawly/__pycache__/api.cpython-313.pyc +0 -0
- pawly/__pycache__/contracts.cpython-311.pyc +0 -0
- pawly/__pycache__/contracts.cpython-313.pyc +0 -0
- pawly/__pycache__/decision_engine.cpython-311.pyc +0 -0
- pawly/__pycache__/decision_engine.cpython-313.pyc +0 -0
- pawly/__pycache__/pawprint_loader.cpython-311.pyc +0 -0
- pawly/__pycache__/pawprint_loader.cpython-313.pyc +0 -0
- pawly/__pycache__/protected_oss.cpython-311.pyc +0 -0
- pawly/__pycache__/protected_oss.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_bootstrap.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_bootstrap.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_decision.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_decision.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_execute.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_execute.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_flow.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_flow.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_ids.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_ids.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_report.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_report.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_request.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_request.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_result.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_result.cpython-313.pyc +0 -0
- pawly/__pycache__/runtime_scoring.cpython-311.pyc +0 -0
- pawly/__pycache__/runtime_scoring.cpython-313.pyc +0 -0
- pawly/__pycache__/shield.cpython-311.pyc +0 -0
- pawly/__pycache__/shield.cpython-313.pyc +0 -0
- pawly/__pycache__/skill_registry.cpython-311.pyc +0 -0
- pawly/__pycache__/skill_registry.cpython-313.pyc +0 -0
- pawly/__pycache__/task_request.cpython-311.pyc +0 -0
- pawly/__pycache__/task_request.cpython-313.pyc +0 -0
- pawly/__pycache__/types.cpython-311.pyc +0 -0
- pawly/__pycache__/types.cpython-313.pyc +0 -0
- pawly/action_selection.py +64 -0
- pawly/adapters/__init__.py +25 -0
- pawly/adapters/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/adapters/__pycache__/claude_skills.cpython-311.pyc +0 -0
- pawly/adapters/__pycache__/claude_skills.cpython-313.pyc +0 -0
- pawly/adapters/__pycache__/openai_agents.cpython-311.pyc +0 -0
- pawly/adapters/__pycache__/openai_agents.cpython-313.pyc +0 -0
- pawly/adapters/claude_skills.py +210 -0
- pawly/adapters/openai_agents.py +208 -0
- pawly/api.py +57 -0
- pawly/approval/__init__.py +18 -0
- pawly/approval/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/callback.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/callback.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/handler.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/handler.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/models.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/models.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/queue.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/queue.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/router.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/router.cpython-313.pyc +0 -0
- pawly/approval/__pycache__/timeout.cpython-311.pyc +0 -0
- pawly/approval/__pycache__/timeout.cpython-313.pyc +0 -0
- pawly/approval/callback.py +16 -0
- pawly/approval/handler.py +34 -0
- pawly/approval/models.py +119 -0
- pawly/approval/queue.py +109 -0
- pawly/approval/router.py +66 -0
- pawly/approval/timeout.py +18 -0
- pawly/audit/__pycache__/diff.cpython-311.pyc +0 -0
- pawly/audit/__pycache__/diff.cpython-313.pyc +0 -0
- pawly/audit/__pycache__/events.cpython-311.pyc +0 -0
- pawly/audit/__pycache__/events.cpython-313.pyc +0 -0
- pawly/audit/__pycache__/ledger.cpython-311.pyc +0 -0
- pawly/audit/__pycache__/ledger.cpython-313.pyc +0 -0
- pawly/audit/__pycache__/replay.cpython-311.pyc +0 -0
- pawly/audit/__pycache__/replay.cpython-313.pyc +0 -0
- pawly/audit/diff.py +24 -0
- pawly/audit/events.py +156 -0
- pawly/audit/ledger.py +39 -0
- pawly/audit/replay.py +45 -0
- pawly/backends/__init__.py +39 -0
- pawly/backends/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/backends/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/backends/__pycache__/approval.cpython-311.pyc +0 -0
- pawly/backends/__pycache__/approval.cpython-313.pyc +0 -0
- pawly/backends/__pycache__/audit.cpython-311.pyc +0 -0
- pawly/backends/__pycache__/audit.cpython-313.pyc +0 -0
- pawly/backends/__pycache__/reviewer.cpython-311.pyc +0 -0
- pawly/backends/__pycache__/reviewer.cpython-313.pyc +0 -0
- pawly/backends/__pycache__/risk.cpython-311.pyc +0 -0
- pawly/backends/__pycache__/risk.cpython-313.pyc +0 -0
- pawly/backends/approval.py +85 -0
- pawly/backends/audit.py +189 -0
- pawly/backends/reviewer.py +127 -0
- pawly/backends/risk.py +71 -0
- pawly/budget/__pycache__/checker.cpython-311.pyc +0 -0
- pawly/budget/__pycache__/checker.cpython-313.pyc +0 -0
- pawly/budget/__pycache__/state.cpython-311.pyc +0 -0
- pawly/budget/__pycache__/state.cpython-313.pyc +0 -0
- pawly/budget/checker.py +37 -0
- pawly/budget/state.py +15 -0
- pawly/cli.py +44 -0
- pawly/contracts.py +153 -0
- pawly/decision_engine.py +6 -0
- pawly/escalation/__pycache__/patterns.cpython-311.pyc +0 -0
- pawly/escalation/__pycache__/patterns.cpython-313.pyc +0 -0
- pawly/escalation/patterns.py +33 -0
- pawly/gateway/__init__.py +4 -0
- pawly/gateway/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/gateway/__pycache__/adapter_support.cpython-311.pyc +0 -0
- pawly/gateway/__pycache__/adapter_support.cpython-313.pyc +0 -0
- pawly/gateway/__pycache__/protocol.cpython-311.pyc +0 -0
- pawly/gateway/__pycache__/protocol.cpython-313.pyc +0 -0
- pawly/gateway/__pycache__/result.cpython-311.pyc +0 -0
- pawly/gateway/__pycache__/result.cpython-313.pyc +0 -0
- pawly/gateway/__pycache__/wrapper.cpython-311.pyc +0 -0
- pawly/gateway/__pycache__/wrapper.cpython-313.pyc +0 -0
- pawly/gateway/adapter_support.py +56 -0
- pawly/gateway/protocol.py +24 -0
- pawly/gateway/result.py +67 -0
- pawly/gateway/wrapper.py +273 -0
- pawly/loader/__pycache__/schema_loader.cpython-311.pyc +0 -0
- pawly/loader/__pycache__/schema_loader.cpython-313.pyc +0 -0
- pawly/loader/__pycache__/yaml_loader.cpython-311.pyc +0 -0
- pawly/loader/__pycache__/yaml_loader.cpython-313.pyc +0 -0
- pawly/loader/schema_loader.py +51 -0
- pawly/loader/yaml_loader.py +164 -0
- pawly/memory/__pycache__/store.cpython-311.pyc +0 -0
- pawly/memory/__pycache__/store.cpython-313.pyc +0 -0
- pawly/memory/__pycache__/tiers.cpython-311.pyc +0 -0
- pawly/memory/__pycache__/tiers.cpython-313.pyc +0 -0
- pawly/memory/store.py +23 -0
- pawly/memory/tiers.py +9 -0
- pawly/middleware/__init__.py +1 -0
- pawly/middleware/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/middleware/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/middleware/__pycache__/hooks.cpython-311.pyc +0 -0
- pawly/middleware/__pycache__/hooks.cpython-313.pyc +0 -0
- pawly/middleware/hooks.py +20 -0
- pawly/pawprint_loader.py +301 -0
- pawly/performance/__pycache__/metrics.cpython-311.pyc +0 -0
- pawly/performance/__pycache__/metrics.cpython-313.pyc +0 -0
- pawly/performance/__pycache__/report.cpython-311.pyc +0 -0
- pawly/performance/__pycache__/report.cpython-313.pyc +0 -0
- pawly/performance/metrics.py +48 -0
- pawly/performance/report.py +44 -0
- pawly/policy/__init__.py +15 -0
- pawly/policy/__pycache__/__init__.cpython-311.pyc +0 -0
- pawly/policy/__pycache__/__init__.cpython-313.pyc +0 -0
- pawly/policy/__pycache__/base.cpython-311.pyc +0 -0
- pawly/policy/__pycache__/base.cpython-313.pyc +0 -0
- pawly/policy/__pycache__/heuristic.cpython-311.pyc +0 -0
- pawly/policy/__pycache__/heuristic.cpython-313.pyc +0 -0
- pawly/policy/__pycache__/resolve.cpython-311.pyc +0 -0
- pawly/policy/__pycache__/resolve.cpython-313.pyc +0 -0
- pawly/policy/base.py +84 -0
- pawly/policy/cloud.py +18 -0
- pawly/policy/heuristic.py +141 -0
- pawly/policy/resolve.py +60 -0
- pawly/policy_engine/__pycache__/decision.cpython-311.pyc +0 -0
- pawly/policy_engine/__pycache__/decision.cpython-313.pyc +0 -0
- pawly/policy_engine/__pycache__/engine.cpython-311.pyc +0 -0
- pawly/policy_engine/__pycache__/engine.cpython-313.pyc +0 -0
- pawly/policy_engine/__pycache__/models.cpython-311.pyc +0 -0
- pawly/policy_engine/__pycache__/models.cpython-313.pyc +0 -0
- pawly/policy_engine/__pycache__/runtime_overlay.cpython-311.pyc +0 -0
- pawly/policy_engine/__pycache__/runtime_overlay.cpython-313.pyc +0 -0
- pawly/policy_engine/decision.py +40 -0
- pawly/policy_engine/engine.py +152 -0
- pawly/policy_engine/models.py +62 -0
- pawly/policy_engine/risk.py +24 -0
- pawly/policy_engine/runtime_overlay.py +37 -0
- pawly/protected_oss.py +168 -0
- pawly/runtime.py +1004 -0
- pawly/runtime_bootstrap.py +104 -0
- pawly/runtime_decision.py +52 -0
- pawly/runtime_execute.py +59 -0
- pawly/runtime_flow.py +50 -0
- pawly/runtime_ids.py +19 -0
- pawly/runtime_report.py +18 -0
- pawly/runtime_request.py +23 -0
- pawly/runtime_result.py +142 -0
- pawly/runtime_scoring.py +25 -0
- pawly/schemas/decision.schema.json +38 -0
- pawly/schemas/intent.schema.json +33 -0
- pawly/schemas/report.schema.json +36 -0
- pawly/shield.py +436 -0
- pawly/skill_registry.py +31 -0
- pawly/task_request.py +27 -0
- pawly/types.py +15 -0
- pawly/validator/__pycache__/validator.cpython-311.pyc +0 -0
- pawly/validator/__pycache__/validator.cpython-313.pyc +0 -0
- pawly/validator/validator.py +177 -0
- pawly-0.1.0.dist-info/METADATA +7 -0
- pawly-0.1.0.dist-info/RECORD +206 -0
- pawly-0.1.0.dist-info/WHEEL +4 -0
- pawly-0.1.0.dist-info/entry_points.txt +2 -0
- pawly-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import importlib
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from pawly.backends.risk import LocalRiskProvider, RiskProvider
|
|
10
|
+
from pawly.pawprint_loader import PawprintConfig
|
|
11
|
+
from pawly.policy_engine.engine import evaluate_pawprint
|
|
12
|
+
from pawly.types import Intent, PolicyEvaluation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
_CONFIG_WARNING = "Pawly Cloud not configured. Falling back to rule-based decision."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReviewerPolicy(Protocol):
|
|
20
|
+
name: str
|
|
21
|
+
|
|
22
|
+
def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RulePolicy:
|
|
27
|
+
name = "rules"
|
|
28
|
+
reviewer_mode = "rule"
|
|
29
|
+
|
|
30
|
+
def __init__(self, risk_provider: RiskProvider | None = None) -> None:
|
|
31
|
+
self.risk_provider = risk_provider or LocalRiskProvider()
|
|
32
|
+
|
|
33
|
+
def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
|
|
34
|
+
del context
|
|
35
|
+
return evaluate_pawprint(
|
|
36
|
+
intent,
|
|
37
|
+
pawprint,
|
|
38
|
+
risk_provider=self.risk_provider,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def review(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
|
|
42
|
+
return self.evaluate(intent, pawprint, context)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CloudReviewerStub:
|
|
46
|
+
name = "cloud-review"
|
|
47
|
+
|
|
48
|
+
def evaluate(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
|
|
49
|
+
del context
|
|
50
|
+
del intent, pawprint
|
|
51
|
+
raise NotImplementedError("Cloud reviewer backends are optional extensions and are not part of the OSS runtime path.")
|
|
52
|
+
|
|
53
|
+
def review(self, intent: Intent, pawprint: PawprintConfig, context: dict[str, Any] | None = None) -> PolicyEvaluation:
|
|
54
|
+
return self.evaluate(intent, pawprint, context)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CloudReviewer(CloudReviewerStub):
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_env(cls) -> "CloudReviewer | None":
|
|
60
|
+
cloud_reviewer_cls = _load_external_cloud_reviewer()
|
|
61
|
+
if cloud_reviewer_cls is None:
|
|
62
|
+
return None
|
|
63
|
+
return cloud_reviewer_cls.from_env()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_policy(
|
|
67
|
+
policy: str = "rules",
|
|
68
|
+
*,
|
|
69
|
+
policy_impl: ReviewerPolicy | None = None,
|
|
70
|
+
risk_provider: RiskProvider | None = None,
|
|
71
|
+
) -> ReviewerPolicy:
|
|
72
|
+
if policy_impl is not None:
|
|
73
|
+
return policy_impl
|
|
74
|
+
if policy == "rules":
|
|
75
|
+
return RulePolicy(risk_provider=risk_provider)
|
|
76
|
+
if policy == "cloud":
|
|
77
|
+
cloud_reviewer = CloudReviewer.from_env()
|
|
78
|
+
if cloud_reviewer is not None:
|
|
79
|
+
return cloud_reviewer
|
|
80
|
+
LOGGER.warning(_CONFIG_WARNING)
|
|
81
|
+
return RulePolicy(risk_provider=risk_provider)
|
|
82
|
+
raise ValueError(f"unsupported policy: {policy}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def evaluate_reviewer_policy(
|
|
86
|
+
policy: ReviewerPolicy,
|
|
87
|
+
intent: Intent,
|
|
88
|
+
pawprint: PawprintConfig,
|
|
89
|
+
context: dict[str, Any] | None = None,
|
|
90
|
+
) -> PolicyEvaluation:
|
|
91
|
+
evaluate = getattr(policy, "evaluate", None)
|
|
92
|
+
if callable(evaluate):
|
|
93
|
+
return evaluate(intent, pawprint, context)
|
|
94
|
+
|
|
95
|
+
review = getattr(policy, "review", None)
|
|
96
|
+
if not callable(review):
|
|
97
|
+
raise TypeError("reviewer policy must provide evaluate(...) or review(...)")
|
|
98
|
+
|
|
99
|
+
parameters = inspect.signature(review).parameters
|
|
100
|
+
if "context" in parameters or len(parameters) >= 3:
|
|
101
|
+
return review(intent, pawprint, context)
|
|
102
|
+
return review(intent, pawprint)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
ReviewerBackend = ReviewerPolicy
|
|
106
|
+
RuleReviewer = RulePolicy
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_reviewer_backend(
|
|
110
|
+
reviewer: str = "rules",
|
|
111
|
+
*,
|
|
112
|
+
reviewer_backend: ReviewerBackend | None = None,
|
|
113
|
+
risk_provider: RiskProvider | None = None,
|
|
114
|
+
) -> ReviewerBackend:
|
|
115
|
+
return resolve_policy(
|
|
116
|
+
policy=reviewer,
|
|
117
|
+
policy_impl=reviewer_backend,
|
|
118
|
+
risk_provider=risk_provider,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_external_cloud_reviewer():
|
|
123
|
+
try:
|
|
124
|
+
module = importlib.import_module("pawly_cloud")
|
|
125
|
+
except ModuleNotFoundError:
|
|
126
|
+
return None
|
|
127
|
+
return getattr(module, "CloudReviewer", None)
|
pawly/backends/risk.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from pawly.escalation.patterns import text_matches_rule
|
|
7
|
+
from pawly.types import Intent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
EXTERNAL_SIDE_EFFECT_RULES = (
|
|
11
|
+
"issue refund",
|
|
12
|
+
"change account details",
|
|
13
|
+
"delete customer records",
|
|
14
|
+
"approve exception",
|
|
15
|
+
"send external message",
|
|
16
|
+
"publish update",
|
|
17
|
+
"modify billing",
|
|
18
|
+
"legal advice",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class RiskContext:
|
|
24
|
+
intent: Intent
|
|
25
|
+
ask_first_matches: list[str]
|
|
26
|
+
never_matches: list[str]
|
|
27
|
+
capability_matched: bool
|
|
28
|
+
handoff_matches: list[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RiskProvider(Protocol):
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
def score(self, context: RiskContext) -> float:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LocalRiskProvider:
|
|
39
|
+
name = "local-rules"
|
|
40
|
+
|
|
41
|
+
def score(self, context: RiskContext) -> float:
|
|
42
|
+
if context.never_matches:
|
|
43
|
+
return 0.98
|
|
44
|
+
|
|
45
|
+
score = 0.1
|
|
46
|
+
if context.ask_first_matches:
|
|
47
|
+
score += 0.35
|
|
48
|
+
if context.handoff_matches:
|
|
49
|
+
score += 0.2
|
|
50
|
+
if not context.capability_matched:
|
|
51
|
+
score += 0.35
|
|
52
|
+
if _has_external_side_effect(context.intent):
|
|
53
|
+
score += 0.15
|
|
54
|
+
if context.intent.confidence < 0.7:
|
|
55
|
+
score += 0.15
|
|
56
|
+
return round(min(score, 0.95), 2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AdvancedRiskProviderStub:
|
|
60
|
+
name = "cloud-risk"
|
|
61
|
+
|
|
62
|
+
def score(self, context: RiskContext) -> float:
|
|
63
|
+
del context
|
|
64
|
+
raise NotImplementedError("Cloud risk providers are optional extensions and are not part of the OSS runtime path.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _has_external_side_effect(intent: Intent) -> bool:
|
|
68
|
+
text = " ".join(
|
|
69
|
+
part for part in [intent.summary, intent.action.name, intent.action.target or ""] if part
|
|
70
|
+
)
|
|
71
|
+
return any(text_matches_rule(text, rule) for rule in EXTERNAL_SIDE_EFFECT_RULES)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pawly/budget/checker.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from pawly.budget.state import BudgetState
|
|
6
|
+
from pawly.types import Intent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class BudgetResult:
|
|
11
|
+
exhausted: bool
|
|
12
|
+
warnings: list[str] = field(default_factory=list)
|
|
13
|
+
consumed: dict[str, float] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_budget(agent_config: dict, state: BudgetState, intent: Intent) -> BudgetResult:
|
|
17
|
+
warnings: list[str] = []
|
|
18
|
+
consumed: dict[str, float] = {}
|
|
19
|
+
exhausted = False
|
|
20
|
+
for limit in agent_config.get("budget", {}).get("limits", []):
|
|
21
|
+
name = limit.get("name", "unnamed-limit")
|
|
22
|
+
amount = float(limit.get("amount", 0))
|
|
23
|
+
unit = limit.get("unit", "count")
|
|
24
|
+
increment = float(intent.metadata.get(name, 1 if unit == "count" else 0))
|
|
25
|
+
current = state.consume(name, increment)
|
|
26
|
+
consumed[name] = current
|
|
27
|
+
if amount and current >= amount:
|
|
28
|
+
if limit.get("enforcement") == "hard":
|
|
29
|
+
exhausted = True
|
|
30
|
+
warnings.append(f"budget limit reached for {name}: {current:g}/{amount:g}")
|
|
31
|
+
else:
|
|
32
|
+
for threshold in agent_config.get("budget", {}).get("warning_thresholds", []):
|
|
33
|
+
if threshold.get("limit") == name:
|
|
34
|
+
percent = float(threshold.get("percent", 0))
|
|
35
|
+
if amount and current >= amount * (percent / 100.0):
|
|
36
|
+
warnings.append(f"budget warning for {name}: {current:g}/{amount:g}")
|
|
37
|
+
return BudgetResult(exhausted=exhausted, warnings=list(dict.fromkeys(warnings)), consumed=consumed)
|
pawly/budget/state.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BudgetState:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._usage = defaultdict(float)
|
|
9
|
+
|
|
10
|
+
def consume(self, name: str, amount: float) -> float:
|
|
11
|
+
self._usage[name] += amount
|
|
12
|
+
return self._usage[name]
|
|
13
|
+
|
|
14
|
+
def snapshot(self) -> dict[str, float]:
|
|
15
|
+
return dict(self._usage)
|
pawly/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from pawly.decision_engine import DecisionEngine
|
|
9
|
+
from pawly.validator.validator import SchemaValidationError
|
|
10
|
+
|
|
11
|
+
DEFAULT_AUDIT_LOG = "pawly.audit.jsonl"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="pawly", description="Aploy Pawly execution-boundary controller CLI")
|
|
16
|
+
parser.add_argument("--agent", required=True, help="Path to a Pawprint worker card")
|
|
17
|
+
parser.add_argument("--task", required=True, help="Task description to evaluate")
|
|
18
|
+
parser.add_argument("--action", required=True, help="Action name to evaluate")
|
|
19
|
+
parser.add_argument("--confidence", required=True, type=float, help="Model confidence in the proposed action")
|
|
20
|
+
parser.add_argument("--audit-log", default=DEFAULT_AUDIT_LOG, help="JSONL audit log path (defaults to ./pawly.audit.jsonl)")
|
|
21
|
+
parser.add_argument("--report", action="store_true", help="Print a report after evaluation")
|
|
22
|
+
return parser
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv: list[str] | None = None) -> int:
|
|
26
|
+
parser = build_parser()
|
|
27
|
+
args = parser.parse_args(argv)
|
|
28
|
+
try:
|
|
29
|
+
runtime = DecisionEngine(args.agent, audit_path=args.audit_log)
|
|
30
|
+
decision = runtime.evaluate(task=args.task, action=args.action, confidence=args.confidence)
|
|
31
|
+
decision["audit_log"] = str(Path(args.audit_log).resolve())
|
|
32
|
+
print(json.dumps(decision, indent=2, sort_keys=True))
|
|
33
|
+
if args.report:
|
|
34
|
+
print(json.dumps(runtime.build_report(), indent=2, sort_keys=True))
|
|
35
|
+
return 0
|
|
36
|
+
except SchemaValidationError as exc:
|
|
37
|
+
parser.error(str(exc))
|
|
38
|
+
except Exception as exc: # pragma: no cover - CLI safety
|
|
39
|
+
print(json.dumps({"error": str(exc)}), file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__": # pragma: no cover
|
|
44
|
+
raise SystemExit(main())
|
pawly/contracts.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DecisionState(str, Enum):
|
|
9
|
+
ALLOW = "allow"
|
|
10
|
+
DENY = "deny"
|
|
11
|
+
REQUIRE_APPROVAL = "require_approval"
|
|
12
|
+
SIMULATE = "simulate"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DecisionSource(str, Enum):
|
|
16
|
+
RULE = "rule"
|
|
17
|
+
CLOUD = "cloud"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IntentSource(str, Enum):
|
|
21
|
+
TOOL_CALL = "tool_call"
|
|
22
|
+
PLANNER_OUTPUT = "planner_output"
|
|
23
|
+
EXECUTION_REQUEST = "execution_request"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class Action:
|
|
28
|
+
name: str
|
|
29
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
target: str | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, payload: dict[str, Any]) -> "Action":
|
|
34
|
+
return cls(
|
|
35
|
+
name=str(payload["name"]),
|
|
36
|
+
arguments=dict(payload.get("arguments", {})),
|
|
37
|
+
target=None if payload.get("target") is None else str(payload["target"]),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
payload = {"name": self.name, "arguments": self.arguments}
|
|
42
|
+
if self.target is not None:
|
|
43
|
+
payload["target"] = self.target
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class Intent:
|
|
49
|
+
intent_id: str
|
|
50
|
+
source: IntentSource
|
|
51
|
+
action: Action
|
|
52
|
+
summary: str = ""
|
|
53
|
+
confidence: float = 0.0
|
|
54
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_dict(cls, payload: dict[str, Any]) -> "Intent":
|
|
58
|
+
context = payload.get("context", {})
|
|
59
|
+
return cls(
|
|
60
|
+
intent_id=str(payload["intent_id"]),
|
|
61
|
+
source=IntentSource(payload["source"]),
|
|
62
|
+
action=Action.from_dict(payload["action"]),
|
|
63
|
+
summary=str(context.get("summary", "")),
|
|
64
|
+
confidence=float(context.get("confidence", 0.0)),
|
|
65
|
+
metadata=dict(payload.get("metadata", {})),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def text(self) -> str:
|
|
69
|
+
return f"{self.summary} {self.action.name}".strip()
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"intent_id": self.intent_id,
|
|
74
|
+
"source": self.source.value,
|
|
75
|
+
"action": self.action.to_dict(),
|
|
76
|
+
"context": {
|
|
77
|
+
"summary": self.summary,
|
|
78
|
+
"confidence": self.confidence,
|
|
79
|
+
},
|
|
80
|
+
"metadata": self.metadata,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(slots=True)
|
|
85
|
+
class PolicyScore:
|
|
86
|
+
risk_score: float | None = None
|
|
87
|
+
reason_codes: list[str] = field(default_factory=list)
|
|
88
|
+
matched_rules: list[str] = field(default_factory=list)
|
|
89
|
+
audit_tags: list[str] = field(default_factory=list)
|
|
90
|
+
uncertainty: float | None = None
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
payload: dict[str, Any] = {}
|
|
94
|
+
if self.risk_score is not None:
|
|
95
|
+
payload["risk_score"] = self.risk_score
|
|
96
|
+
if self.reason_codes:
|
|
97
|
+
payload["reason_codes"] = list(self.reason_codes)
|
|
98
|
+
if self.matched_rules:
|
|
99
|
+
payload["matched_rules"] = list(self.matched_rules)
|
|
100
|
+
if self.audit_tags:
|
|
101
|
+
payload["audit_tags"] = list(self.audit_tags)
|
|
102
|
+
if self.uncertainty is not None:
|
|
103
|
+
payload["uncertainty"] = self.uncertainty
|
|
104
|
+
return payload
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(slots=True)
|
|
108
|
+
class Decision:
|
|
109
|
+
type: DecisionState
|
|
110
|
+
reason: str
|
|
111
|
+
source: str | None = None
|
|
112
|
+
reason_codes: list[str] = field(default_factory=list)
|
|
113
|
+
matched_rules: list[str] = field(default_factory=list)
|
|
114
|
+
risk_score: float | None = None
|
|
115
|
+
rewritten_action: Action | None = None
|
|
116
|
+
audit_tags: list[str] = field(default_factory=list)
|
|
117
|
+
worker_id: str | None = None
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, payload: dict[str, Any]) -> "Decision":
|
|
121
|
+
rewritten_action = payload.get("rewritten_action")
|
|
122
|
+
return cls(
|
|
123
|
+
type=DecisionState(payload["type"]),
|
|
124
|
+
reason=str(payload["reason"]),
|
|
125
|
+
source=None if payload.get("source") is None else str(payload["source"]),
|
|
126
|
+
reason_codes=[str(item) for item in payload.get("reason_codes", [])],
|
|
127
|
+
matched_rules=[str(item) for item in payload.get("matched_rules", [])],
|
|
128
|
+
risk_score=None if payload.get("risk_score") is None else float(payload["risk_score"]),
|
|
129
|
+
rewritten_action=Action.from_dict(rewritten_action) if isinstance(rewritten_action, dict) else None,
|
|
130
|
+
audit_tags=[str(item) for item in payload.get("audit_tags", [])],
|
|
131
|
+
worker_id=None if payload.get("worker_id") is None else str(payload["worker_id"]),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict[str, Any]:
|
|
135
|
+
payload: dict[str, Any] = {
|
|
136
|
+
"type": self.type.value,
|
|
137
|
+
"reason": self.reason,
|
|
138
|
+
}
|
|
139
|
+
if self.source is not None:
|
|
140
|
+
payload["source"] = self.source
|
|
141
|
+
if self.reason_codes:
|
|
142
|
+
payload["reason_codes"] = list(self.reason_codes)
|
|
143
|
+
if self.matched_rules:
|
|
144
|
+
payload["matched_rules"] = list(self.matched_rules)
|
|
145
|
+
if self.risk_score is not None:
|
|
146
|
+
payload["risk_score"] = self.risk_score
|
|
147
|
+
if self.rewritten_action is not None:
|
|
148
|
+
payload["rewritten_action"] = self.rewritten_action.to_dict()
|
|
149
|
+
if self.audit_tags:
|
|
150
|
+
payload["audit_tags"] = list(self.audit_tags)
|
|
151
|
+
if self.worker_id is not None:
|
|
152
|
+
payload["worker_id"] = self.worker_id
|
|
153
|
+
return payload
|
pawly/decision_engine.py
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
KEYWORD_ALIASES = {
|
|
6
|
+
"pricing": {"pricing", "price", "discount", "quote"},
|
|
7
|
+
"contract": {"contract", "agreement", "terms"},
|
|
8
|
+
"refund": {"refund", "credit", "chargeback"},
|
|
9
|
+
"security": {"security", "breach", "fraud", "compromise"},
|
|
10
|
+
"legal": {"legal", "lawsuit", "attorney"},
|
|
11
|
+
"fairness": {"fairness", "bias", "discrimination", "accommodation"},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_tokens(text: str) -> set[str]:
|
|
16
|
+
tokens = set(re.findall(r"[a-z0-9]+", text.lower()))
|
|
17
|
+
expanded = set(tokens)
|
|
18
|
+
for canonical, aliases in KEYWORD_ALIASES.items():
|
|
19
|
+
if tokens & aliases:
|
|
20
|
+
expanded.add(canonical)
|
|
21
|
+
expanded.update(aliases)
|
|
22
|
+
return expanded
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def text_matches_rule(text: str, rule_text: str) -> bool:
|
|
26
|
+
text_tokens = normalize_tokens(text)
|
|
27
|
+
rule_tokens = normalize_tokens(rule_text)
|
|
28
|
+
if not rule_tokens:
|
|
29
|
+
return False
|
|
30
|
+
if rule_text.lower() in text.lower():
|
|
31
|
+
return True
|
|
32
|
+
overlap = text_tokens & rule_tokens
|
|
33
|
+
return len(overlap) >= max(1, min(2, len(rule_tokens) // 2 or 1))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from pawly.gateway import ExecutionGateway, GatewayProtocol
|
|
6
|
+
from pawly.runtime import PawlyRuntime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
AdapterExecutor = Callable[[T], Any]
|
|
12
|
+
AdapterRemap = Callable[[T, Any], T]
|
|
13
|
+
AuditHook = Callable[[dict[str, Any]], None]
|
|
14
|
+
AuditPayloadBuilder = Callable[[dict[str, Any]], dict[str, Any]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_gateway(
|
|
18
|
+
runtime: PawlyRuntime,
|
|
19
|
+
*,
|
|
20
|
+
approval_handler: object | None = None,
|
|
21
|
+
gateway: GatewayProtocol | None = None,
|
|
22
|
+
) -> GatewayProtocol:
|
|
23
|
+
return gateway or ExecutionGateway(runtime, approval_handler=approval_handler)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def metadata_from_intent(intent: Any) -> dict[str, Any]:
|
|
27
|
+
metadata = dict(intent.metadata)
|
|
28
|
+
for key, value in intent.action.arguments.items():
|
|
29
|
+
if key != "task":
|
|
30
|
+
metadata[key] = value
|
|
31
|
+
return metadata
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def execute_adapter_action(
|
|
35
|
+
*,
|
|
36
|
+
gateway: GatewayProtocol,
|
|
37
|
+
item: T,
|
|
38
|
+
task: str,
|
|
39
|
+
action: str,
|
|
40
|
+
confidence: float,
|
|
41
|
+
metadata: dict[str, Any],
|
|
42
|
+
executor: AdapterExecutor[T],
|
|
43
|
+
remap: AdapterRemap[T],
|
|
44
|
+
audit_hook: AuditHook | None = None,
|
|
45
|
+
audit_payload: AuditPayloadBuilder | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
outcome = gateway.execute(
|
|
48
|
+
task=task,
|
|
49
|
+
action=action,
|
|
50
|
+
confidence=confidence,
|
|
51
|
+
metadata=metadata,
|
|
52
|
+
executor=lambda intent: executor(remap(item, intent)),
|
|
53
|
+
)
|
|
54
|
+
if audit_hook is not None and audit_payload is not None:
|
|
55
|
+
audit_hook(audit_payload(outcome))
|
|
56
|
+
return outcome
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Protocol
|
|
4
|
+
|
|
5
|
+
from pawly.contracts import Intent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ExecutorFunc = Callable[[Intent], Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GatewayProtocol(Protocol):
|
|
12
|
+
def execute(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
task: str,
|
|
16
|
+
action: str,
|
|
17
|
+
confidence: float,
|
|
18
|
+
executor: ExecutorFunc,
|
|
19
|
+
metadata: dict[str, Any] | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def execute_intent(self, intent: Intent, executor: ExecutorFunc) -> dict[str, Any]:
|
|
24
|
+
...
|