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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -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"]