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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Small built-in binding functions for YAML-authored machines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .context import FSLMContext
|
|
8
|
+
|
|
9
|
+
_MISSING = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def payload_exists(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
13
|
+
"""Return whether a JSON path exists in the current event payload."""
|
|
14
|
+
return _get_path(ctx, args["path"], missing=_MISSING) is not _MISSING
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def payload_equals(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
18
|
+
"""Return whether a JSON path equals a value."""
|
|
19
|
+
return bool(_get_path(ctx, args["path"]) == args.get("value"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def payload_not_equals(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
23
|
+
"""Return whether a JSON path does not equal a value."""
|
|
24
|
+
return bool(_get_path(ctx, args["path"]) != args.get("value"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def var_exists(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
28
|
+
"""Return whether a snapshot variable exists."""
|
|
29
|
+
return _get_path(ctx, args["path"], missing=_MISSING) is not _MISSING
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def var_equals(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
33
|
+
"""Return whether a snapshot variable path equals a value."""
|
|
34
|
+
return bool(_get_path(ctx, args["path"]) == args.get("value"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def var_not_equals(ctx: FSLMContext, args: dict[str, Any]) -> bool:
|
|
38
|
+
"""Return whether a snapshot variable path does not equal a value."""
|
|
39
|
+
return bool(_get_path(ctx, args["path"]) != args.get("value"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assign(_ctx: FSLMContext, args: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
"""Return a variable assignment patch."""
|
|
44
|
+
return {str(args["path"]).removeprefix("$.snapshot.variables."): args.get("value")}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def increment(ctx: FSLMContext, args: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
"""Return a variable increment patch."""
|
|
49
|
+
key = str(args["path"]).removeprefix("$.snapshot.variables.")
|
|
50
|
+
current = _get_path(ctx, args["path"], missing=0)
|
|
51
|
+
return {key: current + args.get("by", 1)}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def append(ctx: FSLMContext, args: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
"""Return a variable list-append patch."""
|
|
56
|
+
key = str(args["path"]).removeprefix("$.snapshot.variables.")
|
|
57
|
+
current = _get_path(ctx, args["path"], missing=[])
|
|
58
|
+
if not isinstance(current, list):
|
|
59
|
+
current = [current]
|
|
60
|
+
return {key: [*current, args.get("value")]}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def copy_payload_to_var(ctx: FSLMContext, args: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
"""Copy an event payload value to a snapshot variable."""
|
|
65
|
+
value = _get_path(ctx, args["payload_path"])
|
|
66
|
+
key = str(args["var_path"]).removeprefix("$.snapshot.variables.")
|
|
67
|
+
return {key: value}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_path(ctx: FSLMContext, path: str, *, missing: Any = None) -> Any:
|
|
71
|
+
root: Any
|
|
72
|
+
if path.startswith("$.event.payload"):
|
|
73
|
+
root = ctx.event.payload
|
|
74
|
+
parts = path.removeprefix("$.event.payload").strip(".").split(".")
|
|
75
|
+
elif path.startswith("$.snapshot.variables"):
|
|
76
|
+
root = ctx.snapshot.variables
|
|
77
|
+
parts = path.removeprefix("$.snapshot.variables").strip(".").split(".")
|
|
78
|
+
else:
|
|
79
|
+
root = {"event": {"payload": ctx.event.payload}, "snapshot": {"variables": ctx.snapshot.variables}}
|
|
80
|
+
parts = path.removeprefix("$.").split(".")
|
|
81
|
+
if parts == [""]:
|
|
82
|
+
return root
|
|
83
|
+
current = root
|
|
84
|
+
for part in parts:
|
|
85
|
+
if isinstance(current, dict) and part in current:
|
|
86
|
+
current = current[part]
|
|
87
|
+
else:
|
|
88
|
+
return missing
|
|
89
|
+
return current
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"append",
|
|
94
|
+
"assign",
|
|
95
|
+
"copy_payload_to_var",
|
|
96
|
+
"increment",
|
|
97
|
+
"payload_equals",
|
|
98
|
+
"payload_exists",
|
|
99
|
+
"payload_not_equals",
|
|
100
|
+
"var_equals",
|
|
101
|
+
"var_exists",
|
|
102
|
+
"var_not_equals",
|
|
103
|
+
]
|
ellements/fslm/cli.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""`fslm` command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal, cast
|
|
12
|
+
|
|
13
|
+
from .definition import MachineDefinition
|
|
14
|
+
from .kernel import FSLMKernel
|
|
15
|
+
from .loading import load_machine_definition
|
|
16
|
+
from .models import FSLMEvent, MachineSnapshot
|
|
17
|
+
from .rendering import ColorMode, render_snapshot, render_step, render_validation
|
|
18
|
+
from .visualization import to_mermaid
|
|
19
|
+
|
|
20
|
+
OutputFormat = Literal["json", "rich"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="fslm",
|
|
26
|
+
description="Run and inspect ellements finite-state linguistic machines.",
|
|
27
|
+
)
|
|
28
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
validate = sub.add_parser("validate", help="Validate a Python-defined machine.")
|
|
31
|
+
validate.add_argument("spec")
|
|
32
|
+
validate.add_argument("--bindings", action="append", default=None)
|
|
33
|
+
_add_human_output_options(validate)
|
|
34
|
+
validate.set_defaults(handler=_validate)
|
|
35
|
+
|
|
36
|
+
mermaid = sub.add_parser("mermaid", help="Render a spec as Mermaid.")
|
|
37
|
+
mermaid.add_argument("spec")
|
|
38
|
+
mermaid.add_argument("--bindings", action="append", default=None)
|
|
39
|
+
mermaid.set_defaults(handler=_mermaid)
|
|
40
|
+
|
|
41
|
+
init_state = sub.add_parser("init-state", help="Create an initial snapshot.")
|
|
42
|
+
init_state.add_argument("spec")
|
|
43
|
+
init_state.add_argument("--bindings", action="append", default=None)
|
|
44
|
+
init_state.add_argument("--machine-id", default=None)
|
|
45
|
+
init_state.add_argument("--seed", type=int, default=None)
|
|
46
|
+
init_state.add_argument("--state-dir", type=Path, default=None)
|
|
47
|
+
_add_human_output_options(init_state)
|
|
48
|
+
init_state.set_defaults(handler=_init_state)
|
|
49
|
+
|
|
50
|
+
inspect = sub.add_parser("inspect", help="Print a snapshot JSON file.")
|
|
51
|
+
inspect.add_argument("snapshot", type=Path)
|
|
52
|
+
inspect.add_argument("--spec", default=None, help="Optional machine spec for state objectives.")
|
|
53
|
+
_add_human_output_options(inspect)
|
|
54
|
+
inspect.set_defaults(handler=_inspect)
|
|
55
|
+
|
|
56
|
+
step = sub.add_parser("step", help="Run one machine step.")
|
|
57
|
+
step.add_argument("spec")
|
|
58
|
+
step.add_argument("--bindings", action="append", default=None)
|
|
59
|
+
step.add_argument("--snapshot", type=Path)
|
|
60
|
+
step.add_argument("--state-dir", type=Path, default=None)
|
|
61
|
+
step.add_argument("--event", type=Path, required=True)
|
|
62
|
+
_add_human_output_options(step)
|
|
63
|
+
step.set_defaults(handler=_step)
|
|
64
|
+
return parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv: list[str] | None = None) -> int:
|
|
68
|
+
parser = build_parser()
|
|
69
|
+
args = parser.parse_args(argv)
|
|
70
|
+
try:
|
|
71
|
+
handler: Callable[[argparse.Namespace], int] = args.handler
|
|
72
|
+
return handler(args)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate(args: argparse.Namespace) -> int:
|
|
79
|
+
spec = _load_definition(args).spec
|
|
80
|
+
if _output_format(args) == "rich":
|
|
81
|
+
render_validation(spec, color=_color_mode(args))
|
|
82
|
+
else:
|
|
83
|
+
print(json.dumps({"ok": True, "machine": spec.name}, sort_keys=True))
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _mermaid(args: argparse.Namespace) -> int:
|
|
88
|
+
print(to_mermaid(_load_definition(args).spec), end="")
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _init_state(args: argparse.Namespace) -> int:
|
|
93
|
+
spec = _load_definition(args).spec
|
|
94
|
+
snapshot = spec.initial_snapshot(
|
|
95
|
+
machine_id=args.machine_id,
|
|
96
|
+
random_seed=args.seed,
|
|
97
|
+
)
|
|
98
|
+
if args.state_dir is not None:
|
|
99
|
+
args.state_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
(args.state_dir / "snapshot.json").write_text(
|
|
101
|
+
json.dumps(snapshot.model_dump(mode="json"), indent=2, sort_keys=True),
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
)
|
|
104
|
+
if _output_format(args) == "rich":
|
|
105
|
+
render_snapshot(
|
|
106
|
+
snapshot,
|
|
107
|
+
spec=spec,
|
|
108
|
+
title="FSLM Initial State",
|
|
109
|
+
color=_color_mode(args),
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
print(json.dumps(snapshot.model_dump(mode="json"), indent=2, sort_keys=True))
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _inspect(args: argparse.Namespace) -> int:
|
|
117
|
+
snapshot = MachineSnapshot.model_validate(
|
|
118
|
+
json.loads(args.snapshot.read_text(encoding="utf-8"))
|
|
119
|
+
)
|
|
120
|
+
if _output_format(args) == "rich":
|
|
121
|
+
spec = _load_definition(args).spec if args.spec is not None else None
|
|
122
|
+
render_snapshot(snapshot, spec=spec, color=_color_mode(args))
|
|
123
|
+
else:
|
|
124
|
+
print(json.dumps(snapshot.model_dump(mode="json"), indent=2, sort_keys=True))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _step(args: argparse.Namespace) -> int:
|
|
129
|
+
definition = _load_definition(args)
|
|
130
|
+
snapshot_path = args.snapshot
|
|
131
|
+
if snapshot_path is None and args.state_dir is not None:
|
|
132
|
+
snapshot_path = args.state_dir / "snapshot.json"
|
|
133
|
+
if snapshot_path is None:
|
|
134
|
+
raise ValueError("--snapshot or --state-dir is required")
|
|
135
|
+
snapshot = MachineSnapshot.model_validate(json.loads(snapshot_path.read_text("utf-8")))
|
|
136
|
+
event = FSLMEvent.model_validate(json.loads(args.event.read_text(encoding="utf-8")))
|
|
137
|
+
result = asyncio.run(FSLMKernel(definition).step(snapshot, event))
|
|
138
|
+
if args.state_dir is not None:
|
|
139
|
+
args.state_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
(args.state_dir / "snapshot.json").write_text(
|
|
141
|
+
json.dumps(
|
|
142
|
+
result.new_snapshot.model_dump(mode="json"),
|
|
143
|
+
indent=2,
|
|
144
|
+
sort_keys=True,
|
|
145
|
+
),
|
|
146
|
+
encoding="utf-8",
|
|
147
|
+
)
|
|
148
|
+
if _output_format(args) == "rich":
|
|
149
|
+
render_step(
|
|
150
|
+
result,
|
|
151
|
+
definition=definition,
|
|
152
|
+
event=event,
|
|
153
|
+
state_dir=args.state_dir,
|
|
154
|
+
color=_color_mode(args),
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
print(json.dumps(result.model_dump(mode="json"), indent=2, sort_keys=True))
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _add_human_output_options(parser: argparse.ArgumentParser) -> None:
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--format",
|
|
164
|
+
choices=("json", "rich"),
|
|
165
|
+
default="json",
|
|
166
|
+
help="Output format. JSON is stable for automation; rich is designed for humans.",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--color",
|
|
170
|
+
choices=("auto", "always", "never"),
|
|
171
|
+
default="auto",
|
|
172
|
+
help="Color handling for rich output.",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _output_format(args: argparse.Namespace) -> OutputFormat:
|
|
177
|
+
value = args.format
|
|
178
|
+
if value not in {"json", "rich"}:
|
|
179
|
+
raise ValueError(f"unsupported output format: {value}")
|
|
180
|
+
return cast(OutputFormat, value)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _color_mode(args: argparse.Namespace) -> ColorMode:
|
|
184
|
+
value = args.color
|
|
185
|
+
if value not in {"auto", "always", "never"}:
|
|
186
|
+
raise ValueError(f"unsupported color mode: {value}")
|
|
187
|
+
return cast(ColorMode, value)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _load_definition(args: argparse.Namespace) -> MachineDefinition:
|
|
191
|
+
bindings = getattr(args, "bindings", None)
|
|
192
|
+
return load_machine_definition(args.spec, binding_modules=list(bindings or []))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
raise SystemExit(main())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = ["build_parser", "main"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Runtime context passed to FSLM callables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ellements.core import ToolRegistry
|
|
10
|
+
|
|
11
|
+
from .definition import MachineDefinition
|
|
12
|
+
from .models import (
|
|
13
|
+
FSLMEvent,
|
|
14
|
+
MachineSnapshot,
|
|
15
|
+
MachineSpec,
|
|
16
|
+
StateSpec,
|
|
17
|
+
TransitionSpec,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class FSLMContext:
|
|
23
|
+
"""Context object passed to deterministic and NL hooks."""
|
|
24
|
+
|
|
25
|
+
spec: MachineSpec
|
|
26
|
+
definition: MachineDefinition
|
|
27
|
+
state: StateSpec
|
|
28
|
+
transition: TransitionSpec | None
|
|
29
|
+
snapshot: MachineSnapshot
|
|
30
|
+
event: FSLMEvent
|
|
31
|
+
vars: Mapping[str, Any]
|
|
32
|
+
step_id: str
|
|
33
|
+
run_id: str | None = None
|
|
34
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def tools(self) -> ToolRegistry | None:
|
|
38
|
+
"""Tool registry available to this machine, if any."""
|
|
39
|
+
return self.definition.bindings.tools
|
|
40
|
+
|
|
41
|
+
async def call_tool(self, name: str, **arguments: Any) -> Any:
|
|
42
|
+
"""Invoke a registered ellements tool by name."""
|
|
43
|
+
if self.tools is None:
|
|
44
|
+
raise KeyError(f"no tool registry configured for {name!r}")
|
|
45
|
+
if name not in self.tools:
|
|
46
|
+
raise KeyError(f"tool {name!r} is not registered")
|
|
47
|
+
return await self.tools[name].invoke(**arguments)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["FSLMContext"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Executable FSLM definitions and runtime bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from ellements.core import ToolRegistry
|
|
12
|
+
|
|
13
|
+
from .models import MachineSpec
|
|
14
|
+
|
|
15
|
+
BindingCallable = Callable[..., Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RuntimeBindings:
|
|
20
|
+
"""Non-serializable runtime objects referenced by a `MachineSpec`."""
|
|
21
|
+
|
|
22
|
+
guards: dict[str, BindingCallable] = field(default_factory=dict)
|
|
23
|
+
invariants: dict[str, BindingCallable] = field(default_factory=dict)
|
|
24
|
+
effects: dict[str, BindingCallable] = field(default_factory=dict)
|
|
25
|
+
actions: dict[str, BindingCallable] = field(default_factory=dict)
|
|
26
|
+
outputs: dict[str, BindingCallable] = field(default_factory=dict)
|
|
27
|
+
evaluators: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
modules: dict[str, ModuleType] = field(default_factory=dict)
|
|
29
|
+
tools: ToolRegistry | None = None
|
|
30
|
+
|
|
31
|
+
def merge(self, other: RuntimeBindings) -> RuntimeBindings:
|
|
32
|
+
"""Return a binding set with `other` taking precedence."""
|
|
33
|
+
return RuntimeBindings(
|
|
34
|
+
guards={**self.guards, **other.guards},
|
|
35
|
+
invariants={**self.invariants, **other.invariants},
|
|
36
|
+
effects={**self.effects, **other.effects},
|
|
37
|
+
actions={**self.actions, **other.actions},
|
|
38
|
+
outputs={**self.outputs, **other.outputs},
|
|
39
|
+
evaluators={**self.evaluators, **other.evaluators},
|
|
40
|
+
modules={**self.modules, **other.modules},
|
|
41
|
+
tools=other.tools or self.tools,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def resolve(self, ref: str, category: str) -> BindingCallable:
|
|
45
|
+
"""Resolve a binding reference for one callable category."""
|
|
46
|
+
registry = self._registry(category)
|
|
47
|
+
if ref in registry:
|
|
48
|
+
return registry[ref]
|
|
49
|
+
if ref.startswith("builtin."):
|
|
50
|
+
return _builtin(ref)
|
|
51
|
+
if "." in ref:
|
|
52
|
+
alias, _, name = ref.partition(".")
|
|
53
|
+
module = self.modules.get(alias)
|
|
54
|
+
if module is not None and hasattr(module, name):
|
|
55
|
+
value = getattr(module, name)
|
|
56
|
+
if callable(value):
|
|
57
|
+
return cast(BindingCallable, value)
|
|
58
|
+
raise TypeError(f"binding {ref!r} is not callable")
|
|
59
|
+
imported = _import_ref(ref)
|
|
60
|
+
if imported is not None:
|
|
61
|
+
return imported
|
|
62
|
+
raise KeyError(f"unresolved {category} binding reference {ref!r}")
|
|
63
|
+
|
|
64
|
+
def _registry(self, category: str) -> dict[str, BindingCallable]:
|
|
65
|
+
registries = {
|
|
66
|
+
"guard": self.guards,
|
|
67
|
+
"invariant": self.invariants,
|
|
68
|
+
"effect": self.effects,
|
|
69
|
+
"action": self.actions,
|
|
70
|
+
"output": self.outputs,
|
|
71
|
+
}
|
|
72
|
+
return registries.get(category, {})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class MachineDefinition:
|
|
77
|
+
"""Executable machine definition: serializable spec plus bindings."""
|
|
78
|
+
|
|
79
|
+
spec: MachineSpec
|
|
80
|
+
bindings: RuntimeBindings = field(default_factory=RuntimeBindings)
|
|
81
|
+
|
|
82
|
+
def __getattr__(self, name: str) -> Any:
|
|
83
|
+
return getattr(self.spec, name)
|
|
84
|
+
|
|
85
|
+
def initial_snapshot(self, **kwargs: Any) -> Any:
|
|
86
|
+
"""Delegate initial snapshot creation to the spec."""
|
|
87
|
+
return self.spec.initial_snapshot(**kwargs)
|
|
88
|
+
|
|
89
|
+
def to_json_file(self, path: str) -> None:
|
|
90
|
+
"""Write the serializable spec as JSON."""
|
|
91
|
+
self.spec.to_json_file(path)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def coerce_definition(
|
|
95
|
+
value: MachineDefinition | MachineSpec | Any,
|
|
96
|
+
*,
|
|
97
|
+
bindings: RuntimeBindings | None = None,
|
|
98
|
+
) -> MachineDefinition:
|
|
99
|
+
"""Normalize specs/builders/callables into a `MachineDefinition`."""
|
|
100
|
+
if isinstance(value, MachineDefinition):
|
|
101
|
+
if bindings is None:
|
|
102
|
+
return value
|
|
103
|
+
return MachineDefinition(value.spec, value.bindings.merge(bindings))
|
|
104
|
+
if isinstance(value, MachineSpec):
|
|
105
|
+
return MachineDefinition(value, bindings or RuntimeBindings())
|
|
106
|
+
build = getattr(value, "build", None)
|
|
107
|
+
if callable(build):
|
|
108
|
+
return coerce_definition(build(), bindings=bindings)
|
|
109
|
+
if callable(value):
|
|
110
|
+
return coerce_definition(value(), bindings=bindings)
|
|
111
|
+
raise TypeError(
|
|
112
|
+
"expected MachineDefinition, MachineSpec, builder with build(), "
|
|
113
|
+
"or callable returning one"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bindings_from_mapping(mapping: Mapping[str, Mapping[str, BindingCallable]]) -> RuntimeBindings:
|
|
118
|
+
"""Build bindings from a nested mapping keyed by category."""
|
|
119
|
+
return RuntimeBindings(
|
|
120
|
+
guards=dict(mapping.get("guards", {})),
|
|
121
|
+
invariants=dict(mapping.get("invariants", {})),
|
|
122
|
+
effects=dict(mapping.get("effects", {})),
|
|
123
|
+
actions=dict(mapping.get("actions", {})),
|
|
124
|
+
outputs=dict(mapping.get("outputs", {})),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _builtin(ref: str) -> BindingCallable:
|
|
129
|
+
from . import builtins
|
|
130
|
+
|
|
131
|
+
name = ref.removeprefix("builtin.")
|
|
132
|
+
value = getattr(builtins, name, None)
|
|
133
|
+
if callable(value):
|
|
134
|
+
return cast(BindingCallable, value)
|
|
135
|
+
raise KeyError(f"unknown built-in binding {ref!r}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _import_ref(ref: str) -> BindingCallable | None:
|
|
139
|
+
module_name: str
|
|
140
|
+
attr_name: str
|
|
141
|
+
if ":" in ref:
|
|
142
|
+
module_name, attr_name = ref.rsplit(":", 1)
|
|
143
|
+
elif "." in ref:
|
|
144
|
+
module_name, attr_name = ref.rsplit(".", 1)
|
|
145
|
+
else:
|
|
146
|
+
return None
|
|
147
|
+
try:
|
|
148
|
+
module = importlib.import_module(module_name)
|
|
149
|
+
except ImportError:
|
|
150
|
+
return None
|
|
151
|
+
value = getattr(module, attr_name)
|
|
152
|
+
if not callable(value):
|
|
153
|
+
raise TypeError(f"binding {ref!r} is not callable")
|
|
154
|
+
return cast(BindingCallable, value)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"BindingCallable",
|
|
159
|
+
"MachineDefinition",
|
|
160
|
+
"RuntimeBindings",
|
|
161
|
+
"bindings_from_mapping",
|
|
162
|
+
"coerce_definition",
|
|
163
|
+
]
|
ellements/fslm/det.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Deterministic helper namespace for FSLM definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .models import ActionSpec, EffectSpec, GuardSpec, InvariantSpec, OutputSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class BoundGuard:
|
|
14
|
+
spec: GuardSpec
|
|
15
|
+
fn: Callable[..., Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class BoundInvariant:
|
|
20
|
+
spec: InvariantSpec
|
|
21
|
+
fn: Callable[..., Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class BoundOutput:
|
|
26
|
+
spec: OutputSpec
|
|
27
|
+
fn: Callable[..., Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BoundAction:
|
|
32
|
+
spec: ActionSpec
|
|
33
|
+
fn: Callable[..., Any] | None = None
|
|
34
|
+
argument_factory: Callable[..., Any] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class BoundEffect:
|
|
39
|
+
spec: EffectSpec
|
|
40
|
+
fn: Callable[..., Any]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Increment:
|
|
45
|
+
path: str
|
|
46
|
+
by: int | float = 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def guard(
|
|
50
|
+
id: str,
|
|
51
|
+
fn: Callable[..., Any] | None = None,
|
|
52
|
+
*,
|
|
53
|
+
ref: str | None = None,
|
|
54
|
+
args: Mapping[str, Any] | None = None,
|
|
55
|
+
min_confidence: float | None = None,
|
|
56
|
+
) -> GuardSpec | BoundGuard:
|
|
57
|
+
"""Declare a deterministic guard."""
|
|
58
|
+
spec = GuardSpec(
|
|
59
|
+
id=id,
|
|
60
|
+
kind="deterministic",
|
|
61
|
+
ref=ref or id,
|
|
62
|
+
args=dict(args or {}),
|
|
63
|
+
min_confidence=min_confidence,
|
|
64
|
+
)
|
|
65
|
+
return BoundGuard(spec, fn) if fn is not None else spec
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def invariant(
|
|
69
|
+
id: str,
|
|
70
|
+
fn: Callable[..., Any] | None = None,
|
|
71
|
+
*,
|
|
72
|
+
ref: str | None = None,
|
|
73
|
+
args: Mapping[str, Any] | None = None,
|
|
74
|
+
severity: str = "error",
|
|
75
|
+
) -> InvariantSpec | BoundInvariant:
|
|
76
|
+
"""Declare a deterministic invariant."""
|
|
77
|
+
spec = InvariantSpec(
|
|
78
|
+
id=id,
|
|
79
|
+
kind="deterministic",
|
|
80
|
+
ref=ref or id,
|
|
81
|
+
args=dict(args or {}),
|
|
82
|
+
severity=severity, # type: ignore[arg-type]
|
|
83
|
+
)
|
|
84
|
+
return BoundInvariant(spec, fn) if fn is not None else spec
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def output(
|
|
88
|
+
type: str,
|
|
89
|
+
fn: Callable[..., Any] | None = None,
|
|
90
|
+
*,
|
|
91
|
+
ref: str | None = None,
|
|
92
|
+
args: Mapping[str, Any] | None = None,
|
|
93
|
+
description: str = "",
|
|
94
|
+
) -> OutputSpec | BoundOutput:
|
|
95
|
+
"""Declare a deterministic output."""
|
|
96
|
+
spec = OutputSpec(
|
|
97
|
+
type=type,
|
|
98
|
+
kind="deterministic",
|
|
99
|
+
ref=ref or type,
|
|
100
|
+
args=dict(args or {}),
|
|
101
|
+
description=description,
|
|
102
|
+
)
|
|
103
|
+
return BoundOutput(spec, fn) if fn is not None else spec
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def action(
|
|
107
|
+
tool: str,
|
|
108
|
+
*,
|
|
109
|
+
name: str | None = None,
|
|
110
|
+
arguments: Mapping[str, Any] | Callable[..., Any] | None = None,
|
|
111
|
+
requires_safety_check: bool = False,
|
|
112
|
+
args: Mapping[str, Any] | None = None,
|
|
113
|
+
) -> BoundAction | ActionSpec:
|
|
114
|
+
"""Declare a deterministic tool action."""
|
|
115
|
+
spec = ActionSpec(
|
|
116
|
+
name=name or tool,
|
|
117
|
+
kind="tool",
|
|
118
|
+
tool=tool,
|
|
119
|
+
arguments=dict(arguments or {}) if isinstance(arguments, Mapping) else {},
|
|
120
|
+
args=dict(args or {}),
|
|
121
|
+
requires_approval=requires_safety_check,
|
|
122
|
+
)
|
|
123
|
+
if callable(arguments):
|
|
124
|
+
return BoundAction(spec, argument_factory=arguments)
|
|
125
|
+
return spec
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def call(
|
|
129
|
+
name: str,
|
|
130
|
+
fn: Callable[..., Any],
|
|
131
|
+
*,
|
|
132
|
+
args: Mapping[str, Any] | None = None,
|
|
133
|
+
) -> BoundAction:
|
|
134
|
+
"""Declare a deterministic side-effectful Python call."""
|
|
135
|
+
spec = ActionSpec(
|
|
136
|
+
name=name,
|
|
137
|
+
kind="deterministic",
|
|
138
|
+
ref=name,
|
|
139
|
+
args=dict(args or {}),
|
|
140
|
+
)
|
|
141
|
+
return BoundAction(spec, fn=fn)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def effect(
|
|
145
|
+
id: str,
|
|
146
|
+
fn: Callable[..., Any],
|
|
147
|
+
*,
|
|
148
|
+
args: Mapping[str, Any] | None = None,
|
|
149
|
+
) -> BoundEffect:
|
|
150
|
+
"""Declare a pure deterministic effect function."""
|
|
151
|
+
return BoundEffect(EffectSpec(id=id, ref=id, args=dict(args or {})), fn)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def increment(path: str, by: int | float = 1) -> Increment:
|
|
155
|
+
"""Declare an increment effect shorthand for `.effects(...)`."""
|
|
156
|
+
return Increment(path=path, by=by)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"BoundAction",
|
|
161
|
+
"BoundEffect",
|
|
162
|
+
"BoundGuard",
|
|
163
|
+
"BoundInvariant",
|
|
164
|
+
"BoundOutput",
|
|
165
|
+
"Increment",
|
|
166
|
+
"action",
|
|
167
|
+
"call",
|
|
168
|
+
"effect",
|
|
169
|
+
"guard",
|
|
170
|
+
"increment",
|
|
171
|
+
"invariant",
|
|
172
|
+
"output",
|
|
173
|
+
]
|