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
pawly/pawprint_loader.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pawly.loader.yaml_loader import load_yaml_file
|
|
10
|
+
from pawly.validator.validator import PawprintValidator, SchemaValidationError
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
PROTECTED_SKILL_WARNING = (
|
|
15
|
+
"This skill declares skill-protection metadata. Open-source Pawly provides baseline protection only. "
|
|
16
|
+
"For stronger skill-protection controls and managed enforcement, upgrade at https://aploy.ai."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
PRIVATE_SKILL_FIELDS = {
|
|
20
|
+
"raw_prompt",
|
|
21
|
+
"core_prompt",
|
|
22
|
+
"private_prompt",
|
|
23
|
+
"private_rubric",
|
|
24
|
+
"private_examples",
|
|
25
|
+
"private_assets",
|
|
26
|
+
"private_notes",
|
|
27
|
+
"internal_rules",
|
|
28
|
+
"developer_secret",
|
|
29
|
+
"hidden_instructions",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
PUBLIC_SKILL_CARD_FIELDS = {
|
|
33
|
+
"input_schema",
|
|
34
|
+
"output_schema",
|
|
35
|
+
"public_usage_notes",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class SkillProtection:
|
|
41
|
+
level: str
|
|
42
|
+
raw_prompt_visible_to_model: bool
|
|
43
|
+
examples_visible_to_model: bool
|
|
44
|
+
allow_prompt_export: bool
|
|
45
|
+
allow_training_use: bool
|
|
46
|
+
allow_distillation: bool
|
|
47
|
+
require_no_train_route: bool
|
|
48
|
+
watermark_outputs: bool
|
|
49
|
+
monitor_extraction: bool
|
|
50
|
+
max_calls_per_user_per_day: float | None = None
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
payload = {
|
|
54
|
+
"level": self.level,
|
|
55
|
+
"raw_prompt_visible_to_model": self.raw_prompt_visible_to_model,
|
|
56
|
+
"examples_visible_to_model": self.examples_visible_to_model,
|
|
57
|
+
"allow_prompt_export": self.allow_prompt_export,
|
|
58
|
+
"allow_training_use": self.allow_training_use,
|
|
59
|
+
"allow_distillation": self.allow_distillation,
|
|
60
|
+
"require_no_train_route": self.require_no_train_route,
|
|
61
|
+
"watermark_outputs": self.watermark_outputs,
|
|
62
|
+
"monitor_extraction": self.monitor_extraction,
|
|
63
|
+
}
|
|
64
|
+
if self.max_calls_per_user_per_day is not None:
|
|
65
|
+
payload["max_calls_per_user_per_day"] = self.max_calls_per_user_per_day
|
|
66
|
+
return payload
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class SkillLicense:
|
|
71
|
+
type: str
|
|
72
|
+
attribution_required: bool
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"type": self.type,
|
|
77
|
+
"attribution_required": self.attribution_required,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True)
|
|
82
|
+
class SkillMetadata:
|
|
83
|
+
protection: SkillProtection | None = None
|
|
84
|
+
license: SkillLicense | None = None
|
|
85
|
+
model_visible_context: dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict[str, Any]:
|
|
88
|
+
payload: dict[str, Any] = {}
|
|
89
|
+
if self.protection is not None:
|
|
90
|
+
payload["protection"] = self.protection.to_dict()
|
|
91
|
+
if self.license is not None:
|
|
92
|
+
payload["license"] = self.license.to_dict()
|
|
93
|
+
if self.model_visible_context:
|
|
94
|
+
payload["model_visible_context"] = dict(self.model_visible_context)
|
|
95
|
+
return payload
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True)
|
|
99
|
+
class ProtectionConfig:
|
|
100
|
+
level: str = "standard"
|
|
101
|
+
assets: list[str] = field(default_factory=list)
|
|
102
|
+
handling: str = "auto"
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict[str, Any]:
|
|
105
|
+
return {
|
|
106
|
+
"level": self.level,
|
|
107
|
+
"assets": list(self.assets),
|
|
108
|
+
"handling": self.handling,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(slots=True)
|
|
113
|
+
class PawprintConfig:
|
|
114
|
+
id: str
|
|
115
|
+
name: str
|
|
116
|
+
description: str
|
|
117
|
+
capabilities: list[str] = field(default_factory=list)
|
|
118
|
+
allowed_actions: list[str] = field(default_factory=list)
|
|
119
|
+
review_actions: list[str] = field(default_factory=list)
|
|
120
|
+
blocked_actions: list[str] = field(default_factory=list)
|
|
121
|
+
protection: ProtectionConfig | None = None
|
|
122
|
+
skill_metadata: SkillMetadata | None = None
|
|
123
|
+
model_visible_skill_context: dict[str, Any] = field(default_factory=dict)
|
|
124
|
+
|
|
125
|
+
def to_dict(self) -> dict[str, Any]:
|
|
126
|
+
payload = {
|
|
127
|
+
"id": self.id,
|
|
128
|
+
"name": self.name,
|
|
129
|
+
"description": self.description,
|
|
130
|
+
"capabilities": list(self.capabilities),
|
|
131
|
+
"actions": {
|
|
132
|
+
"allowed": list(self.allowed_actions),
|
|
133
|
+
"requiring_review": list(self.review_actions),
|
|
134
|
+
"blocked": list(self.blocked_actions),
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
if self.protection is not None:
|
|
138
|
+
payload["protection"] = self.protection.to_dict()
|
|
139
|
+
if self.skill_metadata is not None:
|
|
140
|
+
payload["skill"] = self.skill_metadata.to_dict()
|
|
141
|
+
if self.model_visible_skill_context:
|
|
142
|
+
payload["model_visible_skill_context"] = dict(self.model_visible_skill_context)
|
|
143
|
+
return payload
|
|
144
|
+
|
|
145
|
+
def resolved_protection(self) -> ProtectionConfig:
|
|
146
|
+
if self.protection is not None:
|
|
147
|
+
return self.protection
|
|
148
|
+
return ProtectionConfig()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(slots=True)
|
|
152
|
+
class LoadedPawprint:
|
|
153
|
+
source_path: Path
|
|
154
|
+
raw_document: dict[str, Any]
|
|
155
|
+
config: PawprintConfig
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def load_pawprint_file(path: str | Path, validator: PawprintValidator | None = None) -> LoadedPawprint:
|
|
159
|
+
resolved_path = Path(path)
|
|
160
|
+
raw_document = _load_document(resolved_path)
|
|
161
|
+
validated_validator = validator or PawprintValidator()
|
|
162
|
+
validation = validated_validator.validate_agent_config(raw_document)
|
|
163
|
+
if not validation.valid:
|
|
164
|
+
raise SchemaValidationError("; ".join(validation.errors))
|
|
165
|
+
return LoadedPawprint(
|
|
166
|
+
source_path=resolved_path,
|
|
167
|
+
raw_document=raw_document,
|
|
168
|
+
config=parse_pawprint_document(raw_document),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def parse_pawprint_document(raw_document: dict[str, Any]) -> PawprintConfig:
|
|
173
|
+
metadata = _mapping(raw_document.get("metadata"))
|
|
174
|
+
boundaries = _mapping(raw_document.get("boundaries"))
|
|
175
|
+
protection = _mapping(raw_document.get("protection"))
|
|
176
|
+
skill = _mapping(raw_document.get("skill"))
|
|
177
|
+
capabilities = raw_document.get("capabilities", [])
|
|
178
|
+
skill_metadata = _parse_skill_metadata(metadata, skill)
|
|
179
|
+
|
|
180
|
+
return PawprintConfig(
|
|
181
|
+
id=str(metadata["id"]),
|
|
182
|
+
name=str(metadata["name"]),
|
|
183
|
+
description=str(metadata["description"]),
|
|
184
|
+
capabilities=[
|
|
185
|
+
str(item.get("name", "")).strip()
|
|
186
|
+
for item in capabilities
|
|
187
|
+
if isinstance(item, dict) and str(item.get("name", "")).strip()
|
|
188
|
+
],
|
|
189
|
+
allowed_actions=_string_list(boundaries.get("allow", [])),
|
|
190
|
+
review_actions=_string_list(boundaries.get("review", [])),
|
|
191
|
+
blocked_actions=_string_list(boundaries.get("block", [])),
|
|
192
|
+
protection=_parse_protection_config(protection),
|
|
193
|
+
skill_metadata=skill_metadata,
|
|
194
|
+
model_visible_skill_context={} if skill_metadata is None else dict(skill_metadata.model_visible_context),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _load_document(path: Path) -> dict[str, Any]:
|
|
199
|
+
suffix = path.suffix.lower()
|
|
200
|
+
if suffix == ".json":
|
|
201
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
202
|
+
else:
|
|
203
|
+
data = load_yaml_file(path)
|
|
204
|
+
if not isinstance(data, dict):
|
|
205
|
+
raise SchemaValidationError("Pawprint document must be a JSON or YAML object")
|
|
206
|
+
return data
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _mapping(value: Any) -> dict[str, Any]:
|
|
210
|
+
if isinstance(value, dict):
|
|
211
|
+
return value
|
|
212
|
+
return {}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _string_list(value: Any) -> list[str]:
|
|
216
|
+
if not isinstance(value, list):
|
|
217
|
+
return []
|
|
218
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _parse_skill_metadata(metadata: dict[str, Any], skill: dict[str, Any]) -> SkillMetadata | None:
|
|
222
|
+
if not skill:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
protection = _parse_skill_protection(skill.get("protection"))
|
|
226
|
+
if protection is not None and protection.level in {"protected", "vault"}:
|
|
227
|
+
LOGGER.warning(PROTECTED_SKILL_WARNING)
|
|
228
|
+
|
|
229
|
+
license = _parse_skill_license(skill.get("license"))
|
|
230
|
+
return SkillMetadata(
|
|
231
|
+
protection=protection,
|
|
232
|
+
license=license,
|
|
233
|
+
model_visible_context=build_model_visible_skill_context(metadata, skill),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _parse_skill_protection(value: Any) -> SkillProtection | None:
|
|
238
|
+
mapping = _mapping(value)
|
|
239
|
+
if not mapping:
|
|
240
|
+
return None
|
|
241
|
+
limit = mapping.get("max_calls_per_user_per_day")
|
|
242
|
+
max_calls = None if limit is None else float(limit)
|
|
243
|
+
return SkillProtection(
|
|
244
|
+
level=str(mapping.get("level", "")),
|
|
245
|
+
raw_prompt_visible_to_model=bool(mapping.get("raw_prompt_visible_to_model")),
|
|
246
|
+
examples_visible_to_model=bool(mapping.get("examples_visible_to_model")),
|
|
247
|
+
allow_prompt_export=bool(mapping.get("allow_prompt_export")),
|
|
248
|
+
allow_training_use=bool(mapping.get("allow_training_use")),
|
|
249
|
+
allow_distillation=bool(mapping.get("allow_distillation")),
|
|
250
|
+
require_no_train_route=bool(mapping.get("require_no_train_route")),
|
|
251
|
+
watermark_outputs=bool(mapping.get("watermark_outputs")),
|
|
252
|
+
monitor_extraction=bool(mapping.get("monitor_extraction")),
|
|
253
|
+
max_calls_per_user_per_day=max_calls,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _parse_skill_license(value: Any) -> SkillLicense | None:
|
|
258
|
+
mapping = _mapping(value)
|
|
259
|
+
if not mapping:
|
|
260
|
+
return None
|
|
261
|
+
return SkillLicense(
|
|
262
|
+
type=str(mapping.get("type", "")),
|
|
263
|
+
attribution_required=bool(mapping.get("attribution_required")),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_protection_config(value: Any) -> ProtectionConfig | None:
|
|
268
|
+
mapping = _mapping(value)
|
|
269
|
+
if not mapping:
|
|
270
|
+
return None
|
|
271
|
+
return ProtectionConfig(
|
|
272
|
+
level=str(mapping.get("level", "standard")).strip() or "standard",
|
|
273
|
+
assets=_string_list(mapping.get("assets", [])),
|
|
274
|
+
handling=str(mapping.get("handling", "auto")).strip() or "auto",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def build_model_visible_skill_context(metadata: dict[str, Any], skill: dict[str, Any]) -> dict[str, Any]:
|
|
279
|
+
context: dict[str, Any] = {}
|
|
280
|
+
name = metadata.get("name")
|
|
281
|
+
description = metadata.get("description")
|
|
282
|
+
if isinstance(name, str) and name.strip():
|
|
283
|
+
context["name"] = name
|
|
284
|
+
if isinstance(description, str) and description.strip():
|
|
285
|
+
context["description"] = description
|
|
286
|
+
|
|
287
|
+
for key in PUBLIC_SKILL_CARD_FIELDS:
|
|
288
|
+
value = skill.get(key)
|
|
289
|
+
if value is not None:
|
|
290
|
+
context[key] = value
|
|
291
|
+
|
|
292
|
+
public_card = skill.get("public_skill_card")
|
|
293
|
+
if isinstance(public_card, dict):
|
|
294
|
+
for key, value in public_card.items():
|
|
295
|
+
if key in PRIVATE_SKILL_FIELDS:
|
|
296
|
+
continue
|
|
297
|
+
context[key] = value
|
|
298
|
+
|
|
299
|
+
for key in PRIVATE_SKILL_FIELDS:
|
|
300
|
+
context.pop(key, None)
|
|
301
|
+
return context
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def summarize_decisions(decisions: list[dict]) -> dict:
|
|
7
|
+
counts = Counter(item.get("type", "unknown") for item in decisions)
|
|
8
|
+
total = sum(counts.values())
|
|
9
|
+
return {
|
|
10
|
+
"total_decisions": total,
|
|
11
|
+
"allow": counts.get("allow", 0),
|
|
12
|
+
"deny": counts.get("deny", 0),
|
|
13
|
+
"require_approval": counts.get("require_approval", 0),
|
|
14
|
+
"simulate": counts.get("simulate", 0),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def summarize_policy_evaluations(decisions: list[dict]) -> dict:
|
|
19
|
+
counts = Counter(item.get("policy_evaluation", {}).get("type", "unknown") for item in decisions)
|
|
20
|
+
total = sum(counts.values())
|
|
21
|
+
return {
|
|
22
|
+
"total_policy_evaluations": total,
|
|
23
|
+
"allow": counts.get("allow", 0),
|
|
24
|
+
"deny": counts.get("deny", 0),
|
|
25
|
+
"require_approval": counts.get("require_approval", 0),
|
|
26
|
+
"simulate": counts.get("simulate", 0),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def summarize_runtime_overlays(decisions: list[dict]) -> dict:
|
|
31
|
+
overlay_decisions = 0
|
|
32
|
+
budget_exhausted = 0
|
|
33
|
+
budget_warning_decisions = 0
|
|
34
|
+
for item in decisions:
|
|
35
|
+
overlays = item.get("runtime_overlays", {})
|
|
36
|
+
budget = overlays.get("budget", {})
|
|
37
|
+
has_overlay = bool(overlays.get("overlay_applied"))
|
|
38
|
+
if budget.get("exhausted"):
|
|
39
|
+
budget_exhausted += 1
|
|
40
|
+
if budget.get("warnings"):
|
|
41
|
+
budget_warning_decisions += 1
|
|
42
|
+
if has_overlay:
|
|
43
|
+
overlay_decisions += 1
|
|
44
|
+
return {
|
|
45
|
+
"overlay_decisions": overlay_decisions,
|
|
46
|
+
"budget_exhausted": budget_exhausted,
|
|
47
|
+
"budget_warning_decisions": budget_warning_decisions,
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from pawly.pawprint_loader import PawprintConfig
|
|
6
|
+
from pawly.performance.metrics import summarize_decisions, summarize_policy_evaluations, summarize_runtime_overlays
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_daily_report(agent_config: PawprintConfig, decisions: list[dict]) -> dict:
|
|
10
|
+
metrics = summarize_decisions(decisions)
|
|
11
|
+
policy_metrics = summarize_policy_evaluations(decisions)
|
|
12
|
+
overlay_metrics = summarize_runtime_overlays(decisions)
|
|
13
|
+
report = {
|
|
14
|
+
"worker_id": agent_config.id,
|
|
15
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
16
|
+
"summary": {
|
|
17
|
+
"status": "failed" if metrics["deny"] else "needs-review" if (metrics["require_approval"] or metrics["simulate"]) else "ok",
|
|
18
|
+
"notes": [
|
|
19
|
+
f"total_decisions={metrics['total_decisions']}",
|
|
20
|
+
f"allow={metrics['allow']}",
|
|
21
|
+
f"require_approval={metrics['require_approval']}",
|
|
22
|
+
f"deny={metrics['deny']}",
|
|
23
|
+
f"simulate={metrics['simulate']}",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
"policy_summary": {
|
|
27
|
+
"status": "failed" if policy_metrics["deny"] else "needs-review" if (policy_metrics["require_approval"] or policy_metrics["simulate"]) else "ok",
|
|
28
|
+
"notes": [
|
|
29
|
+
f"total_policy_evaluations={policy_metrics['total_policy_evaluations']}",
|
|
30
|
+
f"allow={policy_metrics['allow']}",
|
|
31
|
+
f"require_approval={policy_metrics['require_approval']}",
|
|
32
|
+
f"deny={policy_metrics['deny']}",
|
|
33
|
+
f"simulate={policy_metrics['simulate']}",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
"runtime_overlay_summary": {
|
|
37
|
+
"notes": [
|
|
38
|
+
f"overlay_decisions={overlay_metrics['overlay_decisions']}",
|
|
39
|
+
f"budget_exhausted={overlay_metrics['budget_exhausted']}",
|
|
40
|
+
f"budget_warning_decisions={overlay_metrics['budget_warning_decisions']}",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
return report
|
pawly/policy/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Action-scoring policy abstractions for Pawly OSS."""
|
|
2
|
+
|
|
3
|
+
from .base import Policy
|
|
4
|
+
from .heuristic import DefaultOssPolicy, HeuristicPolicy
|
|
5
|
+
from .resolve import resolve_action_routing_policies, resolve_reviewer_policy, resolve_reviewer_selection, resolve_scoring_policy
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DefaultOssPolicy",
|
|
9
|
+
"HeuristicPolicy",
|
|
10
|
+
"Policy",
|
|
11
|
+
"resolve_action_routing_policies",
|
|
12
|
+
"resolve_reviewer_policy",
|
|
13
|
+
"resolve_reviewer_selection",
|
|
14
|
+
"resolve_scoring_policy",
|
|
15
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pawly/policy/base.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pawly.contracts import Action, PolicyScore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScoringPolicyUnavailableError(RuntimeError):
|
|
11
|
+
"""Raised when a scoring policy is configured but cannot be used."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Policy(ABC):
|
|
15
|
+
"""Scores already-filtered candidate actions without executing or constraining them."""
|
|
16
|
+
|
|
17
|
+
name = "custom"
|
|
18
|
+
source = "custom"
|
|
19
|
+
policy_source = "custom"
|
|
20
|
+
supports_scoring = None
|
|
21
|
+
supports_scoring_decision = False
|
|
22
|
+
|
|
23
|
+
def source_name(self) -> str:
|
|
24
|
+
return str(getattr(self, "source", getattr(self, "policy_source", "custom")))
|
|
25
|
+
|
|
26
|
+
def is_scoring_capable(self) -> bool:
|
|
27
|
+
support_mode = getattr(self, "supports_scoring", getattr(self, "supports_scoring_decision", False))
|
|
28
|
+
if support_mode is None:
|
|
29
|
+
support_mode = getattr(self, "supports_scoring_decision", False)
|
|
30
|
+
return bool(support_mode is True)
|
|
31
|
+
|
|
32
|
+
def scoring_support_mode(self) -> str:
|
|
33
|
+
support_mode = getattr(self, "supports_scoring", getattr(self, "supports_scoring_decision", False))
|
|
34
|
+
if support_mode is None:
|
|
35
|
+
support_mode = getattr(self, "supports_scoring_decision", False)
|
|
36
|
+
if support_mode is True:
|
|
37
|
+
return "native"
|
|
38
|
+
if support_mode == "fallback":
|
|
39
|
+
return "fallback"
|
|
40
|
+
return "none"
|
|
41
|
+
|
|
42
|
+
def is_scoring_available(self) -> bool:
|
|
43
|
+
return self.is_scoring_capable()
|
|
44
|
+
|
|
45
|
+
def scoring_unavailable_reason(self) -> str | None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def local_policy(self) -> "Policy":
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def fallback_scoring_policy(self) -> "Policy | None":
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def evaluate(self, state: Any, actions: Sequence[Action]) -> list[PolicyScore]:
|
|
56
|
+
"""Return one score per action in the same order as the input sequence."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_SOURCE_PREFIX = "source:"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def score_source(score: PolicyScore, default_source: str = "custom") -> str:
|
|
63
|
+
for tag in score.audit_tags:
|
|
64
|
+
if tag.startswith(_SOURCE_PREFIX):
|
|
65
|
+
return tag.split(":", 1)[1]
|
|
66
|
+
return default_source
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def tag_scores(scores: Sequence[PolicyScore], source: str) -> list[PolicyScore]:
|
|
70
|
+
tagged_scores: list[PolicyScore] = []
|
|
71
|
+
source_tag = f"{_SOURCE_PREFIX}{source}"
|
|
72
|
+
for score in scores:
|
|
73
|
+
audit_tags = [tag for tag in score.audit_tags if not tag.startswith(_SOURCE_PREFIX)]
|
|
74
|
+
audit_tags.append(source_tag)
|
|
75
|
+
tagged_scores.append(
|
|
76
|
+
PolicyScore(
|
|
77
|
+
risk_score=score.risk_score,
|
|
78
|
+
reason_codes=list(score.reason_codes),
|
|
79
|
+
matched_rules=list(score.matched_rules),
|
|
80
|
+
audit_tags=audit_tags,
|
|
81
|
+
uncertainty=score.uncertainty,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return tagged_scores
|
pawly/policy/cloud.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Compatibility shim for the optional external pawly-cloud package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str):
|
|
9
|
+
if name != "CloudPolicy":
|
|
10
|
+
raise AttributeError(name)
|
|
11
|
+
try:
|
|
12
|
+
module = import_module("pawly_cloud")
|
|
13
|
+
except ModuleNotFoundError as exc:
|
|
14
|
+
raise ModuleNotFoundError(
|
|
15
|
+
"CloudPolicy moved to the optional sibling package 'pawly-cloud'. "
|
|
16
|
+
"Install or add 'pawly-cloud' and import CloudPolicy from 'pawly_cloud'."
|
|
17
|
+
) from exc
|
|
18
|
+
return getattr(module, "CloudPolicy")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pawly.contracts import Action, PolicyScore
|
|
7
|
+
|
|
8
|
+
from .base import Policy, tag_scores
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_HIGH_IMPACT_TERMS = frozenset({"approve", "delete", "deploy", "publish", "refund", "remove", "send"})
|
|
12
|
+
_LOW_IMPACT_TERMS = frozenset({"answer", "draft", "fetch", "list", "preview", "read", "summarize", "view"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HeuristicPolicy(Policy):
|
|
16
|
+
"""Deterministic OSS baseline for ranking already-allowed actions."""
|
|
17
|
+
|
|
18
|
+
name = "heuristic"
|
|
19
|
+
source = "heuristic"
|
|
20
|
+
policy_source = "heuristic"
|
|
21
|
+
supports_scoring = "fallback"
|
|
22
|
+
supports_scoring_decision = False
|
|
23
|
+
|
|
24
|
+
def evaluate(self, state: Any, actions: Sequence[Action]) -> list[PolicyScore]:
|
|
25
|
+
preferred_targets = _preferred_targets(state)
|
|
26
|
+
recent_failures = _recent_failures(state)
|
|
27
|
+
return tag_scores(
|
|
28
|
+
[
|
|
29
|
+
_score_action(
|
|
30
|
+
action,
|
|
31
|
+
preferred_targets=preferred_targets,
|
|
32
|
+
recent_failures=recent_failures,
|
|
33
|
+
)
|
|
34
|
+
for action in actions
|
|
35
|
+
],
|
|
36
|
+
self.source_name(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DefaultOssPolicy = HeuristicPolicy
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _score_action(
|
|
44
|
+
action: Action,
|
|
45
|
+
*,
|
|
46
|
+
preferred_targets: set[str],
|
|
47
|
+
recent_failures: set[str],
|
|
48
|
+
) -> PolicyScore:
|
|
49
|
+
score = 0.5
|
|
50
|
+
reason_codes: list[str] = []
|
|
51
|
+
matched_rules: list[str] = []
|
|
52
|
+
audit_tags: list[str] = ["policy:heuristic"]
|
|
53
|
+
|
|
54
|
+
if action.target:
|
|
55
|
+
normalized_target = action.target.strip().lower()
|
|
56
|
+
if normalized_target in preferred_targets:
|
|
57
|
+
score -= 0.1
|
|
58
|
+
reason_codes.append("preferred_target")
|
|
59
|
+
audit_tags.append(f"target:{normalized_target}")
|
|
60
|
+
else:
|
|
61
|
+
score += 0.05
|
|
62
|
+
reason_codes.append("unrecognized_target")
|
|
63
|
+
else:
|
|
64
|
+
score += 0.03
|
|
65
|
+
reason_codes.append("missing_target")
|
|
66
|
+
|
|
67
|
+
normalized_name = action.name.strip().lower()
|
|
68
|
+
if normalized_name in recent_failures:
|
|
69
|
+
score += 0.2
|
|
70
|
+
reason_codes.append("recent_failure")
|
|
71
|
+
audit_tags.append("history:retry")
|
|
72
|
+
|
|
73
|
+
action_terms = _tokenize(action.name)
|
|
74
|
+
if action_terms & _LOW_IMPACT_TERMS:
|
|
75
|
+
score -= 0.05
|
|
76
|
+
reason_codes.append("low_friction_action")
|
|
77
|
+
|
|
78
|
+
complexity_penalty = _complexity_penalty(action)
|
|
79
|
+
if complexity_penalty:
|
|
80
|
+
score += complexity_penalty
|
|
81
|
+
reason_codes.append("action_complexity")
|
|
82
|
+
|
|
83
|
+
impact_adjustment, impact_code = _impact_adjustment(action.name)
|
|
84
|
+
score += impact_adjustment
|
|
85
|
+
if impact_code is not None:
|
|
86
|
+
reason_codes.append(impact_code)
|
|
87
|
+
|
|
88
|
+
return PolicyScore(
|
|
89
|
+
risk_score=_clamp(score),
|
|
90
|
+
reason_codes=reason_codes,
|
|
91
|
+
matched_rules=matched_rules,
|
|
92
|
+
audit_tags=audit_tags,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _preferred_targets(state: Any) -> set[str]:
|
|
97
|
+
if not isinstance(state, Mapping):
|
|
98
|
+
return set()
|
|
99
|
+
raw_targets = state.get("preferred_targets", [])
|
|
100
|
+
if not isinstance(raw_targets, Sequence) or isinstance(raw_targets, (str, bytes)):
|
|
101
|
+
return set()
|
|
102
|
+
return {str(item).strip().lower() for item in raw_targets if str(item).strip()}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _recent_failures(state: Any) -> set[str]:
|
|
106
|
+
if not isinstance(state, Mapping):
|
|
107
|
+
return set()
|
|
108
|
+
raw_failures = state.get("recent_failures", [])
|
|
109
|
+
if not isinstance(raw_failures, Sequence) or isinstance(raw_failures, (str, bytes)):
|
|
110
|
+
return set()
|
|
111
|
+
return {str(item).strip().lower() for item in raw_failures if str(item).strip()}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _complexity_penalty(action: Action) -> float:
|
|
115
|
+
penalty = 0.0
|
|
116
|
+
if len(action.arguments) > 4:
|
|
117
|
+
penalty += 0.05
|
|
118
|
+
for value in action.arguments.values():
|
|
119
|
+
if isinstance(value, Mapping):
|
|
120
|
+
penalty += 0.05
|
|
121
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
122
|
+
penalty += 0.03
|
|
123
|
+
return min(penalty, 0.15)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _impact_adjustment(action_name: str) -> tuple[float, str | None]:
|
|
127
|
+
terms = _tokenize(action_name)
|
|
128
|
+
if terms & _HIGH_IMPACT_TERMS:
|
|
129
|
+
return 0.2, "high_impact_action"
|
|
130
|
+
if terms & _LOW_IMPACT_TERMS:
|
|
131
|
+
return -0.1, "low_impact_action"
|
|
132
|
+
return 0.0, None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _tokenize(value: str) -> set[str]:
|
|
136
|
+
normalized = value.replace("-", " ").replace("_", " ").lower()
|
|
137
|
+
return {token for token in normalized.split() if token}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _clamp(value: float) -> float:
|
|
141
|
+
return max(0.0, min(1.0, round(value, 4)))
|