ellements 0.2.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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- ellements-0.2.0.dist-info/top_level.txt +1 -0
ellements/fslm/models.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"""Canonical Pydantic models for `ellements.fslm`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal, Self
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
12
|
+
|
|
13
|
+
from .errors import SpecValidationError
|
|
14
|
+
|
|
15
|
+
GuardKind = Literal["deterministic", "nl"]
|
|
16
|
+
InvariantKind = Literal["deterministic", "nl"]
|
|
17
|
+
ActionKind = Literal["deterministic", "nl", "tool"]
|
|
18
|
+
OutputKind = Literal["deterministic", "nl", "static"]
|
|
19
|
+
TransitionKind = Literal["normal", "recovery"]
|
|
20
|
+
ExecutionMode = Literal["execute", "dry_run"]
|
|
21
|
+
TraceMode = Literal["disabled", "jsonl"]
|
|
22
|
+
TerminalSafetyMode = Literal["nl", "disabled"]
|
|
23
|
+
StepStatus = Literal["transitioned", "no_transition", "blocked"]
|
|
24
|
+
ActionStatus = Literal["planned", "executed", "blocked", "failed"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _new_id() -> str:
|
|
28
|
+
return uuid.uuid4().hex
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _utcnow() -> datetime:
|
|
32
|
+
return datetime.now(UTC)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BudgetSpec(BaseModel):
|
|
36
|
+
"""Execution budgets for one machine or state."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(extra="forbid")
|
|
39
|
+
|
|
40
|
+
max_steps: int | None = Field(default=None, ge=1)
|
|
41
|
+
max_nl_calls: int | None = Field(default=None, ge=1)
|
|
42
|
+
max_tool_calls: int | None = Field(default=None, ge=1)
|
|
43
|
+
max_terminal_actions: int | None = Field(default=None, ge=1)
|
|
44
|
+
max_wall_seconds: float | None = Field(default=None, gt=0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ConfidencePolicy(BaseModel):
|
|
48
|
+
"""How structured natural-language confidence gates decisions."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(extra="forbid")
|
|
51
|
+
|
|
52
|
+
min_guard_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
53
|
+
min_invariant_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
54
|
+
low_confidence_behavior: Literal[
|
|
55
|
+
"block", "no_transition", "request_approval"
|
|
56
|
+
] = "block"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExecutionPolicy(BaseModel):
|
|
60
|
+
"""Machine-wide runtime policy."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
execution: ExecutionMode = "execute"
|
|
65
|
+
persistence: str | None = None
|
|
66
|
+
traces: TraceMode = "disabled"
|
|
67
|
+
verbose: bool = False
|
|
68
|
+
terminal_safety: TerminalSafetyMode = "nl"
|
|
69
|
+
confidence: ConfidencePolicy = Field(default_factory=ConfidencePolicy)
|
|
70
|
+
budgets: BudgetSpec = Field(default_factory=BudgetSpec)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ToolRef(BaseModel):
|
|
74
|
+
"""Serializable reference to an ellements tool."""
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(extra="forbid")
|
|
77
|
+
|
|
78
|
+
name: str
|
|
79
|
+
description: str = ""
|
|
80
|
+
params_json_schema: dict[str, Any] = Field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AffordanceSpec(BaseModel):
|
|
84
|
+
"""A state-local capability description."""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(extra="forbid")
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
description: str = ""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EventPatternSpec(BaseModel):
|
|
93
|
+
"""Declared event pattern that can trigger a transition."""
|
|
94
|
+
|
|
95
|
+
model_config = ConfigDict(extra="forbid")
|
|
96
|
+
|
|
97
|
+
type: str
|
|
98
|
+
description: str = ""
|
|
99
|
+
source: str | None = None
|
|
100
|
+
filters: dict[str, Any] = Field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
@model_validator(mode="before")
|
|
103
|
+
@classmethod
|
|
104
|
+
def _from_string(cls, value: Any) -> Any:
|
|
105
|
+
if isinstance(value, str):
|
|
106
|
+
return {"type": value}
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class GuardSpec(BaseModel):
|
|
111
|
+
"""A transition guard."""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(extra="forbid")
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
kind: GuardKind = "deterministic"
|
|
117
|
+
text: str = ""
|
|
118
|
+
ref: str | None = None
|
|
119
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
120
|
+
min_confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
121
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
122
|
+
|
|
123
|
+
@model_validator(mode="before")
|
|
124
|
+
@classmethod
|
|
125
|
+
def _from_string(cls, value: Any) -> Any:
|
|
126
|
+
if isinstance(value, str):
|
|
127
|
+
return {"id": value}
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class InvariantSpec(BaseModel):
|
|
132
|
+
"""A state invariant."""
|
|
133
|
+
|
|
134
|
+
model_config = ConfigDict(extra="forbid")
|
|
135
|
+
|
|
136
|
+
id: str
|
|
137
|
+
kind: InvariantKind = "deterministic"
|
|
138
|
+
text: str = ""
|
|
139
|
+
ref: str | None = None
|
|
140
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
141
|
+
severity: Literal["error", "warning"] = "error"
|
|
142
|
+
min_confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
143
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="before")
|
|
146
|
+
@classmethod
|
|
147
|
+
def _from_string(cls, value: Any) -> Any:
|
|
148
|
+
if isinstance(value, str):
|
|
149
|
+
return {"id": value}
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ActionSpec(BaseModel):
|
|
154
|
+
"""A declared tool/action invocation affordance."""
|
|
155
|
+
|
|
156
|
+
model_config = ConfigDict(extra="forbid")
|
|
157
|
+
|
|
158
|
+
name: str | None = None
|
|
159
|
+
kind: ActionKind = "tool"
|
|
160
|
+
tool: str | None = None
|
|
161
|
+
ref: str | None = None
|
|
162
|
+
instruction: str = ""
|
|
163
|
+
text: str = ""
|
|
164
|
+
arguments: dict[str, Any] = Field(default_factory=dict)
|
|
165
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
166
|
+
requires_approval: bool = False
|
|
167
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
168
|
+
|
|
169
|
+
@model_validator(mode="before")
|
|
170
|
+
@classmethod
|
|
171
|
+
def _from_string(cls, value: Any) -> Any:
|
|
172
|
+
if isinstance(value, str):
|
|
173
|
+
return {"tool": value}
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
@model_validator(mode="after")
|
|
177
|
+
def _default_name(self) -> Self:
|
|
178
|
+
if self.name is None:
|
|
179
|
+
self.name = self.tool or self.ref or "action"
|
|
180
|
+
if self.kind == "tool" and self.tool is None:
|
|
181
|
+
raise ValueError("tool actions require `tool`")
|
|
182
|
+
if self.kind == "deterministic" and self.ref is None:
|
|
183
|
+
raise ValueError("deterministic actions require `ref`")
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class OutputSpec(BaseModel):
|
|
188
|
+
"""A typed output the FSLM may emit."""
|
|
189
|
+
|
|
190
|
+
model_config = ConfigDict(extra="forbid")
|
|
191
|
+
|
|
192
|
+
type: str
|
|
193
|
+
kind: OutputKind = "static"
|
|
194
|
+
description: str = ""
|
|
195
|
+
text: str = ""
|
|
196
|
+
ref: str | None = None
|
|
197
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
198
|
+
payload_schema: dict[str, Any] = Field(default_factory=dict)
|
|
199
|
+
destination: str | None = None
|
|
200
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
201
|
+
|
|
202
|
+
@model_validator(mode="before")
|
|
203
|
+
@classmethod
|
|
204
|
+
def _from_string(cls, value: Any) -> Any:
|
|
205
|
+
if isinstance(value, str):
|
|
206
|
+
return {"type": value}
|
|
207
|
+
return value
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class EffectSpec(BaseModel):
|
|
211
|
+
"""A pure variable/snapshot patch."""
|
|
212
|
+
|
|
213
|
+
model_config = ConfigDict(extra="forbid")
|
|
214
|
+
|
|
215
|
+
id: str | None = None
|
|
216
|
+
ref: str
|
|
217
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TransitionSpec(BaseModel):
|
|
221
|
+
"""One legal transition in a machine."""
|
|
222
|
+
|
|
223
|
+
model_config = ConfigDict(extra="forbid")
|
|
224
|
+
|
|
225
|
+
name: str
|
|
226
|
+
source: str
|
|
227
|
+
target: str
|
|
228
|
+
trigger: EventPatternSpec = Field(alias="event")
|
|
229
|
+
description: str = ""
|
|
230
|
+
guards: list[GuardSpec] = Field(default_factory=list)
|
|
231
|
+
actions: list[ActionSpec] = Field(default_factory=list)
|
|
232
|
+
emits: list[OutputSpec] = Field(default_factory=list)
|
|
233
|
+
effects: dict[str, Any] = Field(default_factory=dict)
|
|
234
|
+
effect_calls: list[EffectSpec] = Field(default_factory=list)
|
|
235
|
+
kind: TransitionKind = "normal"
|
|
236
|
+
weight: float | None = Field(default=None, gt=0)
|
|
237
|
+
random_group: str | None = None
|
|
238
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
239
|
+
|
|
240
|
+
@field_validator("guards", mode="before")
|
|
241
|
+
@classmethod
|
|
242
|
+
def _coerce_guards(cls, value: Any) -> Any:
|
|
243
|
+
return _coerce_list(value)
|
|
244
|
+
|
|
245
|
+
@field_validator("actions", mode="before")
|
|
246
|
+
@classmethod
|
|
247
|
+
def _coerce_actions(cls, value: Any) -> Any:
|
|
248
|
+
return _coerce_list(value)
|
|
249
|
+
|
|
250
|
+
@field_validator("emits", mode="before")
|
|
251
|
+
@classmethod
|
|
252
|
+
def _coerce_outputs(cls, value: Any) -> Any:
|
|
253
|
+
return _coerce_list(value)
|
|
254
|
+
|
|
255
|
+
@field_validator("effect_calls", mode="before")
|
|
256
|
+
@classmethod
|
|
257
|
+
def _coerce_effect_calls(cls, value: Any) -> Any:
|
|
258
|
+
return _coerce_list(value)
|
|
259
|
+
|
|
260
|
+
class StateSpec(BaseModel):
|
|
261
|
+
"""One state and its local locus of control."""
|
|
262
|
+
|
|
263
|
+
model_config = ConfigDict(extra="forbid")
|
|
264
|
+
|
|
265
|
+
name: str
|
|
266
|
+
objective: str = ""
|
|
267
|
+
description: str = ""
|
|
268
|
+
affordances: list[AffordanceSpec] = Field(default_factory=list)
|
|
269
|
+
tools: list[str] = Field(default_factory=list)
|
|
270
|
+
emits: list[OutputSpec] = Field(default_factory=list)
|
|
271
|
+
allowed_event_types: list[str] = Field(default_factory=list)
|
|
272
|
+
invariants: list[InvariantSpec] = Field(default_factory=list)
|
|
273
|
+
transitions: list[str] = Field(default_factory=list)
|
|
274
|
+
recovery_transitions: list[str] = Field(default_factory=list)
|
|
275
|
+
budgets: BudgetSpec | None = None
|
|
276
|
+
terminal: bool = False
|
|
277
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
278
|
+
|
|
279
|
+
@model_validator(mode="before")
|
|
280
|
+
@classmethod
|
|
281
|
+
def _from_string(cls, value: Any) -> Any:
|
|
282
|
+
if isinstance(value, str):
|
|
283
|
+
return {"objective": value}
|
|
284
|
+
return value
|
|
285
|
+
|
|
286
|
+
@field_validator("affordances", mode="before")
|
|
287
|
+
@classmethod
|
|
288
|
+
def _coerce_affordances(cls, value: Any) -> Any:
|
|
289
|
+
items = _coerce_list(value)
|
|
290
|
+
result: list[Any] = []
|
|
291
|
+
for item in items:
|
|
292
|
+
if isinstance(item, str):
|
|
293
|
+
result.append({"name": item})
|
|
294
|
+
else:
|
|
295
|
+
result.append(item)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
@field_validator("emits", mode="before")
|
|
299
|
+
@classmethod
|
|
300
|
+
def _coerce_emits(cls, value: Any) -> Any:
|
|
301
|
+
return _coerce_list(value)
|
|
302
|
+
|
|
303
|
+
@field_validator("invariants", mode="before")
|
|
304
|
+
@classmethod
|
|
305
|
+
def _coerce_invariants(cls, value: Any) -> Any:
|
|
306
|
+
return _coerce_list(value)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class MachineSpec(BaseModel):
|
|
310
|
+
"""Canonical serializable FSLM specification."""
|
|
311
|
+
|
|
312
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
313
|
+
|
|
314
|
+
name: str
|
|
315
|
+
description: str = ""
|
|
316
|
+
version: str = "0.1"
|
|
317
|
+
initial: str
|
|
318
|
+
states: dict[str, StateSpec]
|
|
319
|
+
transitions: dict[str, TransitionSpec] = Field(default_factory=dict)
|
|
320
|
+
tools: dict[str, ToolRef] = Field(default_factory=dict)
|
|
321
|
+
outputs: dict[str, OutputSpec] = Field(default_factory=dict)
|
|
322
|
+
policy: ExecutionPolicy = Field(default_factory=ExecutionPolicy)
|
|
323
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
324
|
+
|
|
325
|
+
@model_validator(mode="before")
|
|
326
|
+
@classmethod
|
|
327
|
+
def _normalize(cls, value: Any) -> Any:
|
|
328
|
+
if not isinstance(value, dict):
|
|
329
|
+
return value
|
|
330
|
+
data = dict(value)
|
|
331
|
+
data["states"] = _normalize_states(data.get("states", {}))
|
|
332
|
+
data["transitions"] = _normalize_transitions(
|
|
333
|
+
data.get("transitions", {}),
|
|
334
|
+
data["states"],
|
|
335
|
+
)
|
|
336
|
+
return data
|
|
337
|
+
|
|
338
|
+
@model_validator(mode="after")
|
|
339
|
+
def _validate_graph(self) -> Self:
|
|
340
|
+
if self.initial not in self.states:
|
|
341
|
+
raise SpecValidationError(
|
|
342
|
+
f"initial state {self.initial!r} is not declared"
|
|
343
|
+
)
|
|
344
|
+
for state_name, state in self.states.items():
|
|
345
|
+
if state.name != state_name:
|
|
346
|
+
raise SpecValidationError(
|
|
347
|
+
f"state key {state_name!r} does not match name {state.name!r}"
|
|
348
|
+
)
|
|
349
|
+
for transition in self.transitions.values():
|
|
350
|
+
if transition.source not in self.states:
|
|
351
|
+
raise SpecValidationError(
|
|
352
|
+
f"transition {transition.name!r} source "
|
|
353
|
+
f"{transition.source!r} is not declared"
|
|
354
|
+
)
|
|
355
|
+
if transition.target not in self.states:
|
|
356
|
+
raise SpecValidationError(
|
|
357
|
+
f"transition {transition.name!r} target "
|
|
358
|
+
f"{transition.target!r} is not declared"
|
|
359
|
+
)
|
|
360
|
+
state = self.states[transition.source]
|
|
361
|
+
bucket = (
|
|
362
|
+
state.recovery_transitions
|
|
363
|
+
if transition.kind == "recovery"
|
|
364
|
+
else state.transitions
|
|
365
|
+
)
|
|
366
|
+
if transition.name not in bucket:
|
|
367
|
+
bucket.append(transition.name)
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
def from_json(cls, path: str | Path) -> MachineSpec:
|
|
372
|
+
"""Load a machine specification from JSON."""
|
|
373
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
374
|
+
return cls.model_validate(data)
|
|
375
|
+
|
|
376
|
+
def to_json_file(self, path: str | Path) -> None:
|
|
377
|
+
"""Write this specification as JSON for interchange or debugging."""
|
|
378
|
+
Path(path).write_text(
|
|
379
|
+
json.dumps(self.model_dump(mode="json", by_alias=True), indent=2),
|
|
380
|
+
encoding="utf-8",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def initial_snapshot(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
machine_id: str | None = None,
|
|
387
|
+
variables: dict[str, Any] | None = None,
|
|
388
|
+
random_seed: int | None = None,
|
|
389
|
+
) -> MachineSnapshot:
|
|
390
|
+
"""Create a fresh snapshot at the initial state."""
|
|
391
|
+
return MachineSnapshot(
|
|
392
|
+
machine_id=machine_id or self.name,
|
|
393
|
+
machine_name=self.name,
|
|
394
|
+
current_state=self.initial,
|
|
395
|
+
variables=variables or {},
|
|
396
|
+
random_seed=random_seed,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class FSLMEvent(BaseModel):
|
|
401
|
+
"""One typed event fed into a machine step."""
|
|
402
|
+
|
|
403
|
+
model_config = ConfigDict(extra="forbid")
|
|
404
|
+
|
|
405
|
+
id: str = Field(default_factory=_new_id)
|
|
406
|
+
type: str
|
|
407
|
+
source: str | None = None
|
|
408
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
409
|
+
occurred_at: datetime = Field(default_factory=_utcnow)
|
|
410
|
+
parent_ids: list[str] = Field(default_factory=list)
|
|
411
|
+
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
412
|
+
evidence: list[str] = Field(default_factory=list)
|
|
413
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class MachineSnapshot(BaseModel):
|
|
417
|
+
"""Durable runtime state for one machine instance."""
|
|
418
|
+
|
|
419
|
+
model_config = ConfigDict(extra="forbid")
|
|
420
|
+
|
|
421
|
+
id: str = Field(default_factory=_new_id)
|
|
422
|
+
machine_id: str
|
|
423
|
+
machine_name: str
|
|
424
|
+
current_state: str
|
|
425
|
+
variables: dict[str, Any] = Field(default_factory=dict)
|
|
426
|
+
step_index: int = Field(default=0, ge=0)
|
|
427
|
+
random_seed: int | None = None
|
|
428
|
+
random_draws: int = Field(default=0, ge=0)
|
|
429
|
+
pending_actions: list[dict[str, Any]] = Field(default_factory=list)
|
|
430
|
+
pending_approvals: list[dict[str, Any]] = Field(default_factory=list)
|
|
431
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
432
|
+
updated_at: datetime = Field(default_factory=_utcnow)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class DecisionResult(BaseModel):
|
|
436
|
+
"""Structured decision returned by deterministic or NL evaluators."""
|
|
437
|
+
|
|
438
|
+
model_config = ConfigDict(extra="forbid")
|
|
439
|
+
|
|
440
|
+
id: str
|
|
441
|
+
allowed: bool
|
|
442
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
443
|
+
evidence: list[str] = Field(default_factory=list)
|
|
444
|
+
uncertainties: list[str] = Field(default_factory=list)
|
|
445
|
+
alternatives: list[str] = Field(default_factory=list)
|
|
446
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class OutputRecord(BaseModel):
|
|
450
|
+
"""Concrete output emitted by a step."""
|
|
451
|
+
|
|
452
|
+
model_config = ConfigDict(extra="forbid")
|
|
453
|
+
|
|
454
|
+
type: str
|
|
455
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
456
|
+
description: str = ""
|
|
457
|
+
destination: str | None = None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class ActionResult(BaseModel):
|
|
461
|
+
"""Concrete result of planning or executing one action."""
|
|
462
|
+
|
|
463
|
+
model_config = ConfigDict(extra="forbid")
|
|
464
|
+
|
|
465
|
+
action_name: str
|
|
466
|
+
tool: str
|
|
467
|
+
status: ActionStatus
|
|
468
|
+
output: dict[str, Any] = Field(default_factory=dict)
|
|
469
|
+
message: str = ""
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class StepResult(BaseModel):
|
|
473
|
+
"""Result of one machine step."""
|
|
474
|
+
|
|
475
|
+
model_config = ConfigDict(extra="forbid")
|
|
476
|
+
|
|
477
|
+
step_id: str = Field(default_factory=_new_id)
|
|
478
|
+
event_id: str
|
|
479
|
+
source_state: str
|
|
480
|
+
target_state: str
|
|
481
|
+
status: StepStatus
|
|
482
|
+
selected_transition: str | None = None
|
|
483
|
+
guard_results: list[DecisionResult] = Field(default_factory=list)
|
|
484
|
+
invariant_results: list[DecisionResult] = Field(default_factory=list)
|
|
485
|
+
outputs: list[OutputRecord] = Field(default_factory=list)
|
|
486
|
+
actions: list[ActionResult] = Field(default_factory=list)
|
|
487
|
+
violations: list[str] = Field(default_factory=list)
|
|
488
|
+
random: dict[str, Any] = Field(default_factory=dict)
|
|
489
|
+
trace: dict[str, Any] = Field(default_factory=dict)
|
|
490
|
+
new_snapshot: MachineSnapshot
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _coerce_list(value: Any) -> list[Any]:
|
|
494
|
+
if value is None:
|
|
495
|
+
return []
|
|
496
|
+
if isinstance(value, list):
|
|
497
|
+
return value
|
|
498
|
+
return [value]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _normalize_states(raw_states: Any) -> dict[str, Any]:
|
|
502
|
+
if not isinstance(raw_states, dict):
|
|
503
|
+
raise SpecValidationError("states must be a mapping")
|
|
504
|
+
normalized: dict[str, Any] = {}
|
|
505
|
+
for state_name, state_data in raw_states.items():
|
|
506
|
+
if isinstance(state_data, BaseModel):
|
|
507
|
+
state_data = state_data.model_dump(mode="python", exclude_none=True)
|
|
508
|
+
if state_data is None:
|
|
509
|
+
state_data = {}
|
|
510
|
+
if isinstance(state_data, str):
|
|
511
|
+
state_data = {"objective": state_data}
|
|
512
|
+
if not isinstance(state_data, dict):
|
|
513
|
+
raise SpecValidationError(f"state {state_name!r} must be a mapping")
|
|
514
|
+
data = dict(state_data)
|
|
515
|
+
data.setdefault("name", state_name)
|
|
516
|
+
normalized[state_name] = data
|
|
517
|
+
return normalized
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _normalize_transitions(
|
|
521
|
+
raw_transitions: Any,
|
|
522
|
+
states: dict[str, Any],
|
|
523
|
+
) -> dict[str, Any]:
|
|
524
|
+
transitions: dict[str, Any] = {}
|
|
525
|
+
if isinstance(raw_transitions, dict):
|
|
526
|
+
for name, transition in raw_transitions.items():
|
|
527
|
+
data = _normalize_transition_data(transition)
|
|
528
|
+
data.setdefault("name", name)
|
|
529
|
+
transitions[data["name"]] = data
|
|
530
|
+
elif isinstance(raw_transitions, list):
|
|
531
|
+
for index, transition in enumerate(raw_transitions):
|
|
532
|
+
data = _normalize_transition_data(transition)
|
|
533
|
+
data.setdefault("name", f"transition_{index + 1}")
|
|
534
|
+
transitions[data["name"]] = data
|
|
535
|
+
elif raw_transitions not in (None, {}):
|
|
536
|
+
raise SpecValidationError("transitions must be a mapping or list")
|
|
537
|
+
|
|
538
|
+
for state_name, state_data in states.items():
|
|
539
|
+
local_specs = list(state_data.get("transitions", []) or [])
|
|
540
|
+
recovery_specs = list(state_data.get("recovery_transitions", []) or [])
|
|
541
|
+
state_transition_names: list[str] = []
|
|
542
|
+
state_recovery_names: list[str] = []
|
|
543
|
+
for index, item in enumerate(local_specs):
|
|
544
|
+
if isinstance(item, str):
|
|
545
|
+
state_transition_names.append(item)
|
|
546
|
+
continue
|
|
547
|
+
data = _normalize_transition_data(item)
|
|
548
|
+
data.setdefault("source", state_name)
|
|
549
|
+
data.setdefault("kind", "normal")
|
|
550
|
+
data.setdefault("name", f"{state_name}_{data['event']['type']}_{index + 1}")
|
|
551
|
+
transitions[data["name"]] = data
|
|
552
|
+
state_transition_names.append(data["name"])
|
|
553
|
+
for index, item in enumerate(recovery_specs):
|
|
554
|
+
if isinstance(item, str):
|
|
555
|
+
state_recovery_names.append(item)
|
|
556
|
+
continue
|
|
557
|
+
data = _normalize_transition_data(item)
|
|
558
|
+
data.setdefault("source", state_name)
|
|
559
|
+
data["kind"] = "recovery"
|
|
560
|
+
data.setdefault(
|
|
561
|
+
"name", f"{state_name}_recover_{data['event']['type']}_{index + 1}"
|
|
562
|
+
)
|
|
563
|
+
transitions[data["name"]] = data
|
|
564
|
+
state_recovery_names.append(data["name"])
|
|
565
|
+
state_data["transitions"] = state_transition_names
|
|
566
|
+
state_data["recovery_transitions"] = state_recovery_names
|
|
567
|
+
return transitions
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _normalize_transition_data(value: Any) -> dict[str, Any]:
|
|
571
|
+
if isinstance(value, BaseModel):
|
|
572
|
+
value = value.model_dump(mode="python", by_alias=True, exclude_none=True)
|
|
573
|
+
if not isinstance(value, dict):
|
|
574
|
+
raise SpecValidationError("transition entries must be mappings")
|
|
575
|
+
data = dict(value)
|
|
576
|
+
if "target" not in data and "to" in data:
|
|
577
|
+
data["target"] = data.pop("to")
|
|
578
|
+
if "event" not in data:
|
|
579
|
+
trigger = data.pop("trigger", None)
|
|
580
|
+
if trigger is not None:
|
|
581
|
+
data["event"] = trigger
|
|
582
|
+
if "event" not in data:
|
|
583
|
+
raise SpecValidationError("transition is missing event")
|
|
584
|
+
if isinstance(data["event"], str):
|
|
585
|
+
data["event"] = {"type": data["event"]}
|
|
586
|
+
if "target" not in data:
|
|
587
|
+
raise SpecValidationError("transition is missing target")
|
|
588
|
+
return data
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
__all__ = [
|
|
592
|
+
"ActionResult",
|
|
593
|
+
"ActionSpec",
|
|
594
|
+
"ActionStatus",
|
|
595
|
+
"ActionKind",
|
|
596
|
+
"AffordanceSpec",
|
|
597
|
+
"BudgetSpec",
|
|
598
|
+
"ConfidencePolicy",
|
|
599
|
+
"DecisionResult",
|
|
600
|
+
"EventPatternSpec",
|
|
601
|
+
"ExecutionMode",
|
|
602
|
+
"ExecutionPolicy",
|
|
603
|
+
"EffectSpec",
|
|
604
|
+
"FSLMEvent",
|
|
605
|
+
"GuardKind",
|
|
606
|
+
"GuardSpec",
|
|
607
|
+
"InvariantKind",
|
|
608
|
+
"InvariantSpec",
|
|
609
|
+
"MachineSnapshot",
|
|
610
|
+
"MachineSpec",
|
|
611
|
+
"OutputRecord",
|
|
612
|
+
"OutputSpec",
|
|
613
|
+
"OutputKind",
|
|
614
|
+
"OutputSpec",
|
|
615
|
+
"StateSpec",
|
|
616
|
+
"StepResult",
|
|
617
|
+
"StepStatus",
|
|
618
|
+
"TerminalSafetyMode",
|
|
619
|
+
"ToolRef",
|
|
620
|
+
"TraceMode",
|
|
621
|
+
"TransitionKind",
|
|
622
|
+
"TransitionSpec",
|
|
623
|
+
]
|
ellements/fslm/nl.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Natural-language spec helpers.
|
|
2
|
+
|
|
3
|
+
The helpers intentionally use the neutral name ``nl`` rather than ``llm``:
|
|
4
|
+
LLMs are one implementation strategy, but human judges, rules plus retrieval,
|
|
5
|
+
or other semantic evaluators can satisfy the same contracts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .models import ActionSpec, GuardSpec, InvariantSpec, OutputSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def guard(
|
|
16
|
+
id: str,
|
|
17
|
+
text: str,
|
|
18
|
+
*,
|
|
19
|
+
min_confidence: float | None = None,
|
|
20
|
+
metadata: dict[str, Any] | None = None,
|
|
21
|
+
) -> GuardSpec:
|
|
22
|
+
"""Declare a natural-language transition guard."""
|
|
23
|
+
return GuardSpec(
|
|
24
|
+
id=id,
|
|
25
|
+
kind="nl",
|
|
26
|
+
text=text,
|
|
27
|
+
min_confidence=min_confidence,
|
|
28
|
+
metadata=metadata or {},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def invariant(
|
|
33
|
+
id: str,
|
|
34
|
+
text: str,
|
|
35
|
+
*,
|
|
36
|
+
severity: str = "error",
|
|
37
|
+
min_confidence: float | None = None,
|
|
38
|
+
metadata: dict[str, Any] | None = None,
|
|
39
|
+
) -> InvariantSpec:
|
|
40
|
+
"""Declare a natural-language state invariant."""
|
|
41
|
+
return InvariantSpec(
|
|
42
|
+
id=id,
|
|
43
|
+
kind="nl",
|
|
44
|
+
text=text,
|
|
45
|
+
severity=severity, # type: ignore[arg-type]
|
|
46
|
+
min_confidence=min_confidence,
|
|
47
|
+
metadata=metadata or {},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def action(
|
|
52
|
+
tool: str,
|
|
53
|
+
text: str,
|
|
54
|
+
*,
|
|
55
|
+
name: str | None = None,
|
|
56
|
+
requires_safety_check: bool = False,
|
|
57
|
+
metadata: dict[str, Any] | None = None,
|
|
58
|
+
) -> ActionSpec:
|
|
59
|
+
"""Declare a natural-language-planned tool action."""
|
|
60
|
+
return ActionSpec(
|
|
61
|
+
name=name or tool,
|
|
62
|
+
kind="nl",
|
|
63
|
+
tool=tool,
|
|
64
|
+
text=text,
|
|
65
|
+
requires_approval=requires_safety_check,
|
|
66
|
+
metadata=metadata or {},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def output(
|
|
71
|
+
type: str,
|
|
72
|
+
text: str,
|
|
73
|
+
*,
|
|
74
|
+
description: str = "",
|
|
75
|
+
metadata: dict[str, Any] | None = None,
|
|
76
|
+
) -> OutputSpec:
|
|
77
|
+
"""Declare a natural-language-produced output."""
|
|
78
|
+
return OutputSpec(
|
|
79
|
+
type=type,
|
|
80
|
+
kind="nl",
|
|
81
|
+
text=text,
|
|
82
|
+
description=description,
|
|
83
|
+
metadata=metadata or {},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["action", "guard", "invariant", "output"]
|