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,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
5
|
+
|
|
6
|
+
from pawly.gateway import GatewayProtocol
|
|
7
|
+
from pawly.gateway.adapter_support import build_gateway, execute_adapter_action, metadata_from_intent
|
|
8
|
+
from pawly.runtime import PawlyRuntime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class OpenAIAgentAction:
|
|
13
|
+
task: str
|
|
14
|
+
tool_name: str
|
|
15
|
+
confidence: float
|
|
16
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
17
|
+
raw_item: Any = None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_value(
|
|
21
|
+
cls,
|
|
22
|
+
value: Any,
|
|
23
|
+
*,
|
|
24
|
+
task: str | None = None,
|
|
25
|
+
confidence: float | None = None,
|
|
26
|
+
payload: Mapping[str, Any] | None = None,
|
|
27
|
+
tool_name_field: str = "tool_name",
|
|
28
|
+
task_field: str = "task",
|
|
29
|
+
confidence_field: str = "confidence",
|
|
30
|
+
payload_field: str = "payload",
|
|
31
|
+
) -> "OpenAIAgentAction":
|
|
32
|
+
tool_name = _extract_value(value, tool_name_field)
|
|
33
|
+
resolved_task = task if task is not None else _extract_value(value, task_field, required=False, default="")
|
|
34
|
+
resolved_confidence = confidence if confidence is not None else _extract_value(
|
|
35
|
+
value,
|
|
36
|
+
confidence_field,
|
|
37
|
+
required=False,
|
|
38
|
+
default=1.0,
|
|
39
|
+
)
|
|
40
|
+
resolved_payload = dict(payload) if payload is not None else dict(
|
|
41
|
+
_extract_value(value, payload_field, required=False, default={}) or {}
|
|
42
|
+
)
|
|
43
|
+
return cls(
|
|
44
|
+
task=str(resolved_task),
|
|
45
|
+
tool_name=str(tool_name),
|
|
46
|
+
confidence=float(resolved_confidence),
|
|
47
|
+
payload=resolved_payload,
|
|
48
|
+
raw_item=value,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
ExecutorHook = Callable[[OpenAIAgentAction], Any]
|
|
53
|
+
AuditHook = Callable[[dict[str, Any]], None]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OpenAIAgentsPawAdapter:
|
|
57
|
+
"""Gateway-backed adapter for OpenAI-style tool invocations."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
runtime: PawlyRuntime,
|
|
62
|
+
audit_hook: AuditHook | None = None,
|
|
63
|
+
approval_handler=None,
|
|
64
|
+
gateway: GatewayProtocol | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.gateway = build_gateway(runtime, approval_handler=approval_handler, gateway=gateway)
|
|
67
|
+
self.audit_hook = audit_hook
|
|
68
|
+
|
|
69
|
+
def execute_tool(self, action: OpenAIAgentAction, executor: ExecutorHook) -> dict[str, Any]:
|
|
70
|
+
return execute_adapter_action(
|
|
71
|
+
gateway=self.gateway,
|
|
72
|
+
item=action,
|
|
73
|
+
task=action.task,
|
|
74
|
+
action=action.tool_name,
|
|
75
|
+
confidence=action.confidence,
|
|
76
|
+
metadata=action.payload,
|
|
77
|
+
executor=executor,
|
|
78
|
+
remap=lambda current, intent: replace(
|
|
79
|
+
current,
|
|
80
|
+
task=intent.summary,
|
|
81
|
+
tool_name=intent.action.name,
|
|
82
|
+
confidence=intent.confidence,
|
|
83
|
+
payload=metadata_from_intent(intent),
|
|
84
|
+
),
|
|
85
|
+
audit_hook=self.audit_hook,
|
|
86
|
+
audit_payload=lambda outcome: {
|
|
87
|
+
"framework": "openai-agents",
|
|
88
|
+
"event": "tool-execution",
|
|
89
|
+
"tool_name": action.tool_name,
|
|
90
|
+
"decision_type": outcome["type"],
|
|
91
|
+
"executed": outcome["execution"]["executed"],
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def execute_native_tool(
|
|
96
|
+
self,
|
|
97
|
+
value: Any,
|
|
98
|
+
executor: ExecutorHook,
|
|
99
|
+
*,
|
|
100
|
+
task: str | None = None,
|
|
101
|
+
confidence: float | None = None,
|
|
102
|
+
payload: Mapping[str, Any] | None = None,
|
|
103
|
+
tool_name_field: str = "tool_name",
|
|
104
|
+
task_field: str = "task",
|
|
105
|
+
confidence_field: str = "confidence",
|
|
106
|
+
payload_field: str = "payload",
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
return self.execute_tool(
|
|
109
|
+
OpenAIAgentAction.from_value(
|
|
110
|
+
value,
|
|
111
|
+
task=task,
|
|
112
|
+
confidence=confidence,
|
|
113
|
+
payload=payload,
|
|
114
|
+
tool_name_field=tool_name_field,
|
|
115
|
+
task_field=task_field,
|
|
116
|
+
confidence_field=confidence_field,
|
|
117
|
+
payload_field=payload_field,
|
|
118
|
+
),
|
|
119
|
+
executor,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def wrap_openai_tool_executor(
|
|
124
|
+
runtime: PawlyRuntime,
|
|
125
|
+
executor: ExecutorHook,
|
|
126
|
+
*,
|
|
127
|
+
audit_hook: AuditHook | None = None,
|
|
128
|
+
approval_handler=None,
|
|
129
|
+
gateway: GatewayProtocol | None = None,
|
|
130
|
+
task: str | None = None,
|
|
131
|
+
confidence: float | None = None,
|
|
132
|
+
payload: Mapping[str, Any] | None = None,
|
|
133
|
+
tool_name_field: str = "tool_name",
|
|
134
|
+
task_field: str = "task",
|
|
135
|
+
confidence_field: str = "confidence",
|
|
136
|
+
payload_field: str = "payload",
|
|
137
|
+
) -> Callable[[Any], dict[str, Any]]:
|
|
138
|
+
adapter = OpenAIAgentsPawAdapter(
|
|
139
|
+
runtime,
|
|
140
|
+
audit_hook=audit_hook,
|
|
141
|
+
approval_handler=approval_handler,
|
|
142
|
+
gateway=gateway,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def wrapped(value: Any) -> dict[str, Any]:
|
|
146
|
+
return adapter.execute_native_tool(
|
|
147
|
+
value,
|
|
148
|
+
executor,
|
|
149
|
+
task=task,
|
|
150
|
+
confidence=confidence,
|
|
151
|
+
payload=payload,
|
|
152
|
+
tool_name_field=tool_name_field,
|
|
153
|
+
task_field=task_field,
|
|
154
|
+
confidence_field=confidence_field,
|
|
155
|
+
payload_field=payload_field,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return wrapped
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def wrap_openai_tools(
|
|
162
|
+
runtime: PawlyRuntime,
|
|
163
|
+
tools: Iterable[Any],
|
|
164
|
+
*,
|
|
165
|
+
audit_hook: AuditHook | None = None,
|
|
166
|
+
approval_handler=None,
|
|
167
|
+
gateway: GatewayProtocol | None = None,
|
|
168
|
+
executor_field: str = "executor",
|
|
169
|
+
tool_name_field: str = "tool_name",
|
|
170
|
+
task: str | None = None,
|
|
171
|
+
confidence: float | None = None,
|
|
172
|
+
payload: Mapping[str, Any] | None = None,
|
|
173
|
+
task_field: str = "task",
|
|
174
|
+
confidence_field: str = "confidence",
|
|
175
|
+
payload_field: str = "payload",
|
|
176
|
+
) -> dict[str, Callable[[Any], dict[str, Any]]]:
|
|
177
|
+
wrapped_tools: dict[str, Callable[[Any], dict[str, Any]]] = {}
|
|
178
|
+
for tool in tools:
|
|
179
|
+
tool_name = str(_extract_value(tool, tool_name_field))
|
|
180
|
+
executor = _extract_value(tool, executor_field)
|
|
181
|
+
if not callable(executor):
|
|
182
|
+
raise TypeError(f"executor for tool '{tool_name}' must be callable")
|
|
183
|
+
wrapped_tools[tool_name] = wrap_openai_tool_executor(
|
|
184
|
+
runtime,
|
|
185
|
+
executor,
|
|
186
|
+
audit_hook=audit_hook,
|
|
187
|
+
approval_handler=approval_handler,
|
|
188
|
+
gateway=gateway,
|
|
189
|
+
task=task,
|
|
190
|
+
confidence=confidence,
|
|
191
|
+
payload=payload,
|
|
192
|
+
tool_name_field=tool_name_field,
|
|
193
|
+
task_field=task_field,
|
|
194
|
+
confidence_field=confidence_field,
|
|
195
|
+
payload_field=payload_field,
|
|
196
|
+
)
|
|
197
|
+
return wrapped_tools
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _extract_value(value: Any, field: str, *, required: bool = True, default: Any = None) -> Any:
|
|
201
|
+
if isinstance(value, Mapping):
|
|
202
|
+
if field in value:
|
|
203
|
+
return value[field]
|
|
204
|
+
elif hasattr(value, field):
|
|
205
|
+
return getattr(value, field)
|
|
206
|
+
if required:
|
|
207
|
+
raise ValueError(f"unable to extract required field '{field}'")
|
|
208
|
+
return default
|
pawly/api.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pawly.action_selection import ActionDecision
|
|
8
|
+
from pawly.contracts import Action
|
|
9
|
+
from pawly.pawprint_loader import PawprintConfig
|
|
10
|
+
from pawly.runtime import DecisionEngine
|
|
11
|
+
from pawly.skill_registry import SkillRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def decide(
|
|
15
|
+
pawprint: str | Path,
|
|
16
|
+
*,
|
|
17
|
+
state: Mapping[str, Any] | None,
|
|
18
|
+
actions: Sequence[Action],
|
|
19
|
+
pawprint_config: PawprintConfig | None = None,
|
|
20
|
+
**engine_kwargs: Any,
|
|
21
|
+
) -> ActionDecision:
|
|
22
|
+
engine = DecisionEngine(pawprint, **engine_kwargs)
|
|
23
|
+
return engine.decide_actions(state, actions, pawprint_config)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(
|
|
27
|
+
pawprint: str | Path,
|
|
28
|
+
*,
|
|
29
|
+
task: str,
|
|
30
|
+
action: str,
|
|
31
|
+
confidence: float,
|
|
32
|
+
metadata: dict[str, Any] | None = None,
|
|
33
|
+
**engine_kwargs: Any,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
engine = DecisionEngine(pawprint, **engine_kwargs)
|
|
36
|
+
return engine.evaluate(task, action, confidence, metadata)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_actions(
|
|
40
|
+
pawprint: str | Path,
|
|
41
|
+
*,
|
|
42
|
+
state: Mapping[str, Any] | None,
|
|
43
|
+
actions: Sequence[Action],
|
|
44
|
+
context: Mapping[str, Any] | None = None,
|
|
45
|
+
skill_registry: SkillRegistry | None = None,
|
|
46
|
+
pawprint_config: PawprintConfig | None = None,
|
|
47
|
+
**engine_kwargs: Any,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
engine = DecisionEngine(pawprint, **engine_kwargs)
|
|
50
|
+
if skill_registry is not None:
|
|
51
|
+
engine.register_skills(skill_registry)
|
|
52
|
+
return engine.run_actions(
|
|
53
|
+
state=state,
|
|
54
|
+
actions=actions,
|
|
55
|
+
context=context,
|
|
56
|
+
pawprint_config=pawprint_config,
|
|
57
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .callback import CallbackApprovalNotifier
|
|
2
|
+
from .handler import ApprovalHandler, StaticApprovalHandler
|
|
3
|
+
from .models import ApprovalRecord, ApprovalRequest, ApprovalResponse, ApprovalStatus
|
|
4
|
+
from .queue import FileApprovalQueue, InMemoryApprovalQueue
|
|
5
|
+
from .router import ApprovalRouter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ApprovalHandler",
|
|
9
|
+
"ApprovalRecord",
|
|
10
|
+
"ApprovalRequest",
|
|
11
|
+
"ApprovalResponse",
|
|
12
|
+
"ApprovalRouter",
|
|
13
|
+
"ApprovalStatus",
|
|
14
|
+
"CallbackApprovalNotifier",
|
|
15
|
+
"FileApprovalQueue",
|
|
16
|
+
"InMemoryApprovalQueue",
|
|
17
|
+
"StaticApprovalHandler",
|
|
18
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from pawly.approval.models import ApprovalRecord
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CallbackHook = Callable[[ApprovalRecord], None]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CallbackApprovalNotifier:
|
|
12
|
+
def __init__(self, callback: CallbackHook) -> None:
|
|
13
|
+
self.callback = callback
|
|
14
|
+
|
|
15
|
+
def notify(self, record: ApprovalRecord) -> None:
|
|
16
|
+
self.callback(record)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from pawly.approval.models import ApprovalRequest, ApprovalResponse, ApprovalStatus
|
|
6
|
+
from pawly.types import IntentAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApprovalHandler(Protocol):
|
|
10
|
+
def review(self, request: ApprovalRequest) -> ApprovalResponse: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StaticApprovalHandler:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
approved: bool,
|
|
18
|
+
reviewer: str = "local-reviewer",
|
|
19
|
+
notes: list[str] | None = None,
|
|
20
|
+
edited_action: IntentAction | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.approved = approved
|
|
23
|
+
self.reviewer = reviewer
|
|
24
|
+
self.notes = notes or []
|
|
25
|
+
self.edited_action = edited_action
|
|
26
|
+
|
|
27
|
+
def review(self, request: ApprovalRequest) -> ApprovalResponse:
|
|
28
|
+
del request
|
|
29
|
+
return ApprovalResponse(
|
|
30
|
+
status=ApprovalStatus.APPROVED if self.approved else ApprovalStatus.REJECTED,
|
|
31
|
+
reviewer=self.reviewer,
|
|
32
|
+
notes=list(self.notes),
|
|
33
|
+
edited_action=self.edited_action,
|
|
34
|
+
)
|
pawly/approval/models.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from pawly.contracts import Decision
|
|
10
|
+
from pawly.types import Intent, IntentAction
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def utc_now() -> datetime:
|
|
14
|
+
return datetime.now(timezone.utc)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApprovalStatus(str, Enum):
|
|
18
|
+
PENDING = "pending"
|
|
19
|
+
APPROVED = "approved"
|
|
20
|
+
REJECTED = "rejected"
|
|
21
|
+
EXPIRED = "expired"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class ApprovalRequest:
|
|
26
|
+
intent: Intent
|
|
27
|
+
decision: Decision
|
|
28
|
+
record: "ApprovalRecord | None" = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ApprovalResponse:
|
|
33
|
+
status: ApprovalStatus
|
|
34
|
+
reviewer: str | None = "local-reviewer"
|
|
35
|
+
notes: list[str] = field(default_factory=list)
|
|
36
|
+
edited_action: IntentAction | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class ApprovalRecord:
|
|
41
|
+
record_id: str
|
|
42
|
+
intent: Intent
|
|
43
|
+
proposed_action: IntentAction
|
|
44
|
+
edited_action: IntentAction | None
|
|
45
|
+
status: ApprovalStatus
|
|
46
|
+
created_at: str
|
|
47
|
+
updated_at: str
|
|
48
|
+
expires_at: str | None
|
|
49
|
+
decision: Decision
|
|
50
|
+
reviewer: str | None = None
|
|
51
|
+
notes: list[str] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def create(
|
|
55
|
+
cls,
|
|
56
|
+
*,
|
|
57
|
+
intent: Intent,
|
|
58
|
+
decision: Decision,
|
|
59
|
+
expires_at: datetime | None,
|
|
60
|
+
) -> "ApprovalRecord":
|
|
61
|
+
now = utc_now().isoformat()
|
|
62
|
+
return cls(
|
|
63
|
+
record_id=f"approval-{uuid4().hex}",
|
|
64
|
+
intent=intent,
|
|
65
|
+
proposed_action=intent.action,
|
|
66
|
+
edited_action=None,
|
|
67
|
+
status=ApprovalStatus.PENDING,
|
|
68
|
+
created_at=now,
|
|
69
|
+
updated_at=now,
|
|
70
|
+
expires_at=expires_at.isoformat() if expires_at is not None else None,
|
|
71
|
+
decision=decision,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def apply_response(self, response: ApprovalResponse) -> None:
|
|
75
|
+
self.status = response.status
|
|
76
|
+
self.reviewer = response.reviewer
|
|
77
|
+
self.notes = list(response.notes)
|
|
78
|
+
self.edited_action = response.edited_action
|
|
79
|
+
self.updated_at = utc_now().isoformat()
|
|
80
|
+
|
|
81
|
+
def mark_expired(self) -> None:
|
|
82
|
+
self.status = ApprovalStatus.EXPIRED
|
|
83
|
+
self.updated_at = utc_now().isoformat()
|
|
84
|
+
if "approval request expired" not in self.notes:
|
|
85
|
+
self.notes.append("approval request expired")
|
|
86
|
+
|
|
87
|
+
def approved_action(self) -> IntentAction:
|
|
88
|
+
return self.edited_action or self.proposed_action
|
|
89
|
+
|
|
90
|
+
def approved_intent(self) -> Intent:
|
|
91
|
+
action = self.approved_action()
|
|
92
|
+
return Intent(
|
|
93
|
+
intent_id=self.intent.intent_id,
|
|
94
|
+
source=self.intent.source,
|
|
95
|
+
action=action,
|
|
96
|
+
summary=self.intent.summary,
|
|
97
|
+
confidence=self.intent.confidence,
|
|
98
|
+
metadata=self.intent.metadata,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def to_dict(self) -> dict[str, Any]:
|
|
102
|
+
payload = {
|
|
103
|
+
"record_id": self.record_id,
|
|
104
|
+
"intent": self.intent.to_dict(),
|
|
105
|
+
"proposed_action": self.proposed_action.to_dict(),
|
|
106
|
+
"status": self.status.value,
|
|
107
|
+
"created_at": self.created_at,
|
|
108
|
+
"updated_at": self.updated_at,
|
|
109
|
+
"decision": self.decision.to_dict(),
|
|
110
|
+
}
|
|
111
|
+
if self.edited_action is not None:
|
|
112
|
+
payload["edited_action"] = self.edited_action.to_dict()
|
|
113
|
+
if self.expires_at is not None:
|
|
114
|
+
payload["expires_at"] = self.expires_at
|
|
115
|
+
if self.reviewer is not None:
|
|
116
|
+
payload["reviewer"] = self.reviewer
|
|
117
|
+
if self.notes:
|
|
118
|
+
payload["notes"] = self.notes
|
|
119
|
+
return payload
|
pawly/approval/queue.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from pawly.contracts import Action, Decision, Intent, IntentSource
|
|
8
|
+
from pawly.approval.models import ApprovalRecord, ApprovalStatus
|
|
9
|
+
from pawly.approval.timeout import is_expired
|
|
10
|
+
from pawly.types import IntentAction
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApprovalQueue(Protocol):
|
|
14
|
+
def create(self, record: ApprovalRecord) -> ApprovalRecord: ...
|
|
15
|
+
def get(self, record_id: str) -> ApprovalRecord | None: ...
|
|
16
|
+
def update(self, record: ApprovalRecord) -> ApprovalRecord: ...
|
|
17
|
+
def list_pending(self) -> list[ApprovalRecord]: ...
|
|
18
|
+
def expire_due(self) -> list[ApprovalRecord]: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InMemoryApprovalQueue:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._records: dict[str, ApprovalRecord] = {}
|
|
24
|
+
|
|
25
|
+
def create(self, record: ApprovalRecord) -> ApprovalRecord:
|
|
26
|
+
self._records[record.record_id] = record
|
|
27
|
+
return record
|
|
28
|
+
|
|
29
|
+
def get(self, record_id: str) -> ApprovalRecord | None:
|
|
30
|
+
return self._records.get(record_id)
|
|
31
|
+
|
|
32
|
+
def update(self, record: ApprovalRecord) -> ApprovalRecord:
|
|
33
|
+
self._records[record.record_id] = record
|
|
34
|
+
return record
|
|
35
|
+
|
|
36
|
+
def list_pending(self) -> list[ApprovalRecord]:
|
|
37
|
+
return [record for record in self._records.values() if record.status == ApprovalStatus.PENDING]
|
|
38
|
+
|
|
39
|
+
def expire_due(self) -> list[ApprovalRecord]:
|
|
40
|
+
expired: list[ApprovalRecord] = []
|
|
41
|
+
for record in self.list_pending():
|
|
42
|
+
if is_expired(record):
|
|
43
|
+
record.mark_expired()
|
|
44
|
+
self.update(record)
|
|
45
|
+
expired.append(record)
|
|
46
|
+
return expired
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FileApprovalQueue:
|
|
50
|
+
def __init__(self, path: str | Path) -> None:
|
|
51
|
+
self.path = Path(path)
|
|
52
|
+
|
|
53
|
+
def create(self, record: ApprovalRecord) -> ApprovalRecord:
|
|
54
|
+
records = self._load()
|
|
55
|
+
records[record.record_id] = record
|
|
56
|
+
self._save(records)
|
|
57
|
+
return record
|
|
58
|
+
|
|
59
|
+
def get(self, record_id: str) -> ApprovalRecord | None:
|
|
60
|
+
return self._load().get(record_id)
|
|
61
|
+
|
|
62
|
+
def update(self, record: ApprovalRecord) -> ApprovalRecord:
|
|
63
|
+
records = self._load()
|
|
64
|
+
records[record.record_id] = record
|
|
65
|
+
self._save(records)
|
|
66
|
+
return record
|
|
67
|
+
|
|
68
|
+
def list_pending(self) -> list[ApprovalRecord]:
|
|
69
|
+
return [record for record in self._load().values() if record.status == ApprovalStatus.PENDING]
|
|
70
|
+
|
|
71
|
+
def expire_due(self) -> list[ApprovalRecord]:
|
|
72
|
+
records = self._load()
|
|
73
|
+
expired: list[ApprovalRecord] = []
|
|
74
|
+
for record in records.values():
|
|
75
|
+
if record.status == ApprovalStatus.PENDING and is_expired(record):
|
|
76
|
+
record.mark_expired()
|
|
77
|
+
expired.append(record)
|
|
78
|
+
self._save(records)
|
|
79
|
+
return expired
|
|
80
|
+
|
|
81
|
+
def _load(self) -> dict[str, ApprovalRecord]:
|
|
82
|
+
if not self.path.exists():
|
|
83
|
+
return {}
|
|
84
|
+
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
|
85
|
+
return {record_id: _record_from_dict(item) for record_id, item in payload.items()}
|
|
86
|
+
|
|
87
|
+
def _save(self, records: dict[str, ApprovalRecord]) -> None:
|
|
88
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
payload = {record_id: record.to_dict() for record_id, record in records.items()}
|
|
90
|
+
self.path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _record_from_dict(value: dict) -> ApprovalRecord:
|
|
94
|
+
intent = Intent.from_dict(value["intent"])
|
|
95
|
+
proposed = value["proposed_action"]
|
|
96
|
+
edited = value.get("edited_action")
|
|
97
|
+
return ApprovalRecord(
|
|
98
|
+
record_id=value["record_id"],
|
|
99
|
+
intent=intent,
|
|
100
|
+
proposed_action=Action.from_dict(proposed),
|
|
101
|
+
edited_action=Action.from_dict(edited) if edited is not None else None,
|
|
102
|
+
status=ApprovalStatus(value["status"]),
|
|
103
|
+
created_at=value["created_at"],
|
|
104
|
+
updated_at=value["updated_at"],
|
|
105
|
+
expires_at=value.get("expires_at"),
|
|
106
|
+
decision=Decision.from_dict(value["decision"]),
|
|
107
|
+
reviewer=value.get("reviewer"),
|
|
108
|
+
notes=value.get("notes", []),
|
|
109
|
+
)
|
pawly/approval/router.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pawly.contracts import Decision
|
|
4
|
+
from pawly.approval.handler import ApprovalHandler
|
|
5
|
+
from pawly.approval.models import ApprovalRecord, ApprovalRequest, ApprovalResponse, ApprovalStatus
|
|
6
|
+
from pawly.approval.queue import ApprovalQueue, InMemoryApprovalQueue
|
|
7
|
+
from pawly.approval.timeout import expiry_from_now
|
|
8
|
+
from pawly.types import Intent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApprovalRouter:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
queue: ApprovalQueue | None = None,
|
|
16
|
+
handler: ApprovalHandler | None = None,
|
|
17
|
+
timeout_seconds: int | None = 300,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.queue = queue or InMemoryApprovalQueue()
|
|
20
|
+
self.handler = handler
|
|
21
|
+
self.timeout_seconds = timeout_seconds
|
|
22
|
+
|
|
23
|
+
def submit(self, intent: Intent, decision: Decision) -> ApprovalRecord:
|
|
24
|
+
record = ApprovalRecord.create(
|
|
25
|
+
intent=intent,
|
|
26
|
+
decision=decision,
|
|
27
|
+
expires_at=expiry_from_now(self.timeout_seconds),
|
|
28
|
+
)
|
|
29
|
+
self.queue.create(record)
|
|
30
|
+
if self.handler is not None:
|
|
31
|
+
response = self.handler.review(ApprovalRequest(intent=intent, decision=decision, record=record))
|
|
32
|
+
return self.apply_response(record.record_id, response)
|
|
33
|
+
self.queue.expire_due()
|
|
34
|
+
return self.get(record.record_id)
|
|
35
|
+
|
|
36
|
+
def get(self, record_id: str) -> ApprovalRecord:
|
|
37
|
+
self.queue.expire_due()
|
|
38
|
+
record = self.queue.get(record_id)
|
|
39
|
+
if record is None:
|
|
40
|
+
raise KeyError(record_id)
|
|
41
|
+
return record
|
|
42
|
+
|
|
43
|
+
def apply_response(self, record_id: str, response: ApprovalResponse) -> ApprovalRecord:
|
|
44
|
+
record = self.get(record_id)
|
|
45
|
+
record.apply_response(response)
|
|
46
|
+
self.queue.update(record)
|
|
47
|
+
self.queue.expire_due()
|
|
48
|
+
return self.get(record_id)
|
|
49
|
+
|
|
50
|
+
def expire_due(self) -> list[ApprovalRecord]:
|
|
51
|
+
return self.queue.expire_due()
|
|
52
|
+
|
|
53
|
+
def status_payload(self, record: ApprovalRecord) -> dict:
|
|
54
|
+
return {
|
|
55
|
+
"record_id": record.record_id,
|
|
56
|
+
"status": record.status.value,
|
|
57
|
+
"approved": record.status == ApprovalStatus.APPROVED,
|
|
58
|
+
"reviewer": record.reviewer,
|
|
59
|
+
"notes": record.notes,
|
|
60
|
+
"proposed_action": record.proposed_action.to_dict(),
|
|
61
|
+
"edited_action": record.edited_action.to_dict() if record.edited_action is not None else None,
|
|
62
|
+
"decision": record.decision.to_dict(),
|
|
63
|
+
"created_at": record.created_at,
|
|
64
|
+
"updated_at": record.updated_at,
|
|
65
|
+
"expires_at": record.expires_at,
|
|
66
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
from pawly.approval.models import ApprovalRecord
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def expiry_from_now(timeout_seconds: int | None) -> datetime | None:
|
|
9
|
+
if timeout_seconds is None:
|
|
10
|
+
return None
|
|
11
|
+
return datetime.now(timezone.utc) + timedelta(seconds=timeout_seconds)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_expired(record: ApprovalRecord, *, now: datetime | None = None) -> bool:
|
|
15
|
+
if record.expires_at is None:
|
|
16
|
+
return False
|
|
17
|
+
current = now or datetime.now(timezone.utc)
|
|
18
|
+
return current >= datetime.fromisoformat(record.expires_at)
|
|
Binary file
|
|
Binary file
|