markov-protocols 0.1.0__tar.gz
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.
- markov_protocols-0.1.0/LICENSE +21 -0
- markov_protocols-0.1.0/PKG-INFO +76 -0
- markov_protocols-0.1.0/README.md +51 -0
- markov_protocols-0.1.0/pyproject.toml +69 -0
- markov_protocols-0.1.0/src/markov_protocols/__init__.py +109 -0
- markov_protocols-0.1.0/src/markov_protocols/base.py +23 -0
- markov_protocols-0.1.0/src/markov_protocols/conditions/__init__.py +44 -0
- markov_protocols-0.1.0/src/markov_protocols/conditions/evaluate.py +129 -0
- markov_protocols-0.1.0/src/markov_protocols/conditions/models.py +111 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/__init__.py +1 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/metadata.py +30 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/registry.py +54 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/state.py +113 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/states/__init__.py +9 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/states/action_execute.py +48 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/states/data_collection.py +27 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/states/human_handoff.py +46 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/transition.py +23 -0
- markov_protocols-0.1.0/src/markov_protocols/definition/workflow.py +101 -0
- markov_protocols-0.1.0/src/markov_protocols/py.typed +0 -0
- markov_protocols-0.1.0/src/markov_protocols/references.py +96 -0
- markov_protocols-0.1.0/src/markov_protocols/result.py +76 -0
- markov_protocols-0.1.0/src/markov_protocols/runtime/__init__.py +1 -0
- markov_protocols-0.1.0/src/markov_protocols/runtime/blackboard.py +55 -0
- markov_protocols-0.1.0/src/markov_protocols/runtime/events.py +73 -0
- markov_protocols-0.1.0/src/markov_protocols/runtime/session.py +325 -0
- markov_protocols-0.1.0/src/markov_protocols/serialization/__init__.py +83 -0
- markov_protocols-0.1.0/src/markov_protocols/serialization/converter.py +78 -0
- markov_protocols-0.1.0/src/markov_protocols/serialization/document.py +70 -0
- markov_protocols-0.1.0/src/markov_protocols/slug.py +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markov-protocols
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A deterministic finite state machine for modeling workflows that guide AI agents.
|
|
5
|
+
Keywords: finite-state-machine,fsm,workflow,state-machine,ai-agent,llm,deterministic
|
|
6
|
+
Author: geralm
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: pydantic>=2.13.4
|
|
18
|
+
Requires-Dist: pyyaml>=6.0 ; extra == 'yaml'
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Project-URL: Homepage, https://github.com/geralm/markov-protocols
|
|
21
|
+
Project-URL: Repository, https://github.com/geralm/markov-protocols
|
|
22
|
+
Project-URL: Issues, https://github.com/geralm/markov-protocols/issues
|
|
23
|
+
Provides-Extra: yaml
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# markov-protocols
|
|
27
|
+
|
|
28
|
+
A Finite State Machine capable of modeling workflows to provide guidance to an AI agent with determinism.
|
|
29
|
+
|
|
30
|
+
A standalone Python module providing the state-machine mechanism, meant to be reused across other Python projects.
|
|
31
|
+
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
Slices 1–3 complete — the engine is functionally whole:
|
|
35
|
+
|
|
36
|
+
- **Definition** — conditions, references, metadata, the `State` interface with three concrete state
|
|
37
|
+
types, transitions, the registry, and `Workflow.compile()`.
|
|
38
|
+
- **Runtime** — `Blackboard`, the `Event` history, `Directive` materialization, and
|
|
39
|
+
`Session.start/update` with the write + fast-forward passes.
|
|
40
|
+
- **Integrity** — a correction (`ValueChanged`) reopens the states that depend on it, strict-rewinds
|
|
41
|
+
to the earliest one, clears reopened actions' outputs so they re-run, and flags `had_executed`
|
|
42
|
+
for host compensation. All derived from `history` — no parallel bookkeeping.
|
|
43
|
+
|
|
44
|
+
See [`docs/DESIGN.md`](docs/DESIGN.md) for the full architecture and build plan.
|
|
45
|
+
|
|
46
|
+
## Development
|
|
47
|
+
|
|
48
|
+
Managed with [uv](https://docs.astral.sh/uv/).
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv sync # create the environment and install dev tools
|
|
52
|
+
uv run pytest # run the test suite
|
|
53
|
+
uv run ruff check # lint
|
|
54
|
+
uv run mypy # type-check
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
src/markov_protocols/
|
|
61
|
+
__init__.py # curated public API
|
|
62
|
+
result.py # Result[T, E] / ErrorType — the error-handling pattern
|
|
63
|
+
base.py # StrictModel / StrictOpenModel
|
|
64
|
+
slug.py # deterministic title -> id
|
|
65
|
+
conditions/ # the typed boolean vocabulary + pure evaluator
|
|
66
|
+
references.py # Ref + resolve + referenced_fields
|
|
67
|
+
definition/ # metadata, State interface + concrete states, Transition,
|
|
68
|
+
# registry, Workflow.compile()
|
|
69
|
+
runtime/ # Blackboard, Event history, Session + update() pipeline
|
|
70
|
+
tests/ # one property-focused module per unit
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Conventions
|
|
74
|
+
|
|
75
|
+
- Operations return a `Result[T, E]` instead of raising exceptions.
|
|
76
|
+
- Target Python 3.13+.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# markov-protocols
|
|
2
|
+
|
|
3
|
+
A Finite State Machine capable of modeling workflows to provide guidance to an AI agent with determinism.
|
|
4
|
+
|
|
5
|
+
A standalone Python module providing the state-machine mechanism, meant to be reused across other Python projects.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Slices 1–3 complete — the engine is functionally whole:
|
|
10
|
+
|
|
11
|
+
- **Definition** — conditions, references, metadata, the `State` interface with three concrete state
|
|
12
|
+
types, transitions, the registry, and `Workflow.compile()`.
|
|
13
|
+
- **Runtime** — `Blackboard`, the `Event` history, `Directive` materialization, and
|
|
14
|
+
`Session.start/update` with the write + fast-forward passes.
|
|
15
|
+
- **Integrity** — a correction (`ValueChanged`) reopens the states that depend on it, strict-rewinds
|
|
16
|
+
to the earliest one, clears reopened actions' outputs so they re-run, and flags `had_executed`
|
|
17
|
+
for host compensation. All derived from `history` — no parallel bookkeeping.
|
|
18
|
+
|
|
19
|
+
See [`docs/DESIGN.md`](docs/DESIGN.md) for the full architecture and build plan.
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
Managed with [uv](https://docs.astral.sh/uv/).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync # create the environment and install dev tools
|
|
27
|
+
uv run pytest # run the test suite
|
|
28
|
+
uv run ruff check # lint
|
|
29
|
+
uv run mypy # type-check
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Layout
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/markov_protocols/
|
|
36
|
+
__init__.py # curated public API
|
|
37
|
+
result.py # Result[T, E] / ErrorType — the error-handling pattern
|
|
38
|
+
base.py # StrictModel / StrictOpenModel
|
|
39
|
+
slug.py # deterministic title -> id
|
|
40
|
+
conditions/ # the typed boolean vocabulary + pure evaluator
|
|
41
|
+
references.py # Ref + resolve + referenced_fields
|
|
42
|
+
definition/ # metadata, State interface + concrete states, Transition,
|
|
43
|
+
# registry, Workflow.compile()
|
|
44
|
+
runtime/ # Blackboard, Event history, Session + update() pipeline
|
|
45
|
+
tests/ # one property-focused module per unit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Conventions
|
|
49
|
+
|
|
50
|
+
- Operations return a `Result[T, E]` instead of raising exceptions.
|
|
51
|
+
- Target Python 3.13+.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "markov-protocols"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A deterministic finite state machine for modeling workflows that guide AI agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "geralm" }
|
|
11
|
+
]
|
|
12
|
+
keywords = [
|
|
13
|
+
"finite-state-machine",
|
|
14
|
+
"fsm",
|
|
15
|
+
"workflow",
|
|
16
|
+
"state-machine",
|
|
17
|
+
"ai-agent",
|
|
18
|
+
"llm",
|
|
19
|
+
"deterministic",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"pydantic>=2.13.4",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
yaml = ["pyyaml>=6.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/geralm/markov-protocols"
|
|
40
|
+
Repository = "https://github.com/geralm/markov-protocols"
|
|
41
|
+
Issues = "https://github.com/geralm/markov-protocols/issues"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.0",
|
|
50
|
+
"ruff>=0.6",
|
|
51
|
+
"mypy>=1.11",
|
|
52
|
+
"pyyaml>=6.0",
|
|
53
|
+
"types-pyyaml>=6.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "-ra"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
src = ["src", "tests"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
strict = true
|
|
69
|
+
files = ["src"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""markov-protocols: a deterministic finite state machine for modeling
|
|
2
|
+
workflows that guide AI agents.
|
|
3
|
+
|
|
4
|
+
Author with ``Workflow`` + the concrete state types, express logic with
|
|
5
|
+
``Condition`` + ``Ref``, and handle outcomes with ``Result``. The runtime
|
|
6
|
+
(``Session``) arrives in a later slice.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .conditions import (
|
|
10
|
+
All,
|
|
11
|
+
Any,
|
|
12
|
+
Condition,
|
|
13
|
+
Eq,
|
|
14
|
+
Exists,
|
|
15
|
+
Gt,
|
|
16
|
+
Gte,
|
|
17
|
+
In,
|
|
18
|
+
Lt,
|
|
19
|
+
Lte,
|
|
20
|
+
Ne,
|
|
21
|
+
Not,
|
|
22
|
+
Regex,
|
|
23
|
+
condition_fields,
|
|
24
|
+
evaluate,
|
|
25
|
+
)
|
|
26
|
+
from .definition.metadata import StateMetadata, TransitionMetadata
|
|
27
|
+
from .definition.registry import state_registry
|
|
28
|
+
from .definition.state import Directive, Requirement, State, Status
|
|
29
|
+
from .definition.states import ActionExecuteState, DataCollectionState, HumanHandoffState
|
|
30
|
+
from .definition.transition import Transition
|
|
31
|
+
from .definition.workflow import Workflow
|
|
32
|
+
from .references import Ref, referenced_fields, resolve
|
|
33
|
+
from .result import ErrorType, Result
|
|
34
|
+
from .runtime.blackboard import Blackboard
|
|
35
|
+
from .runtime.events import (
|
|
36
|
+
ActionExecuted,
|
|
37
|
+
Event,
|
|
38
|
+
StateCompleted,
|
|
39
|
+
StateReopened,
|
|
40
|
+
TransitionTaken,
|
|
41
|
+
ValueChanged,
|
|
42
|
+
ValueSet,
|
|
43
|
+
)
|
|
44
|
+
from .runtime.session import Session, UpdateResult
|
|
45
|
+
from .serialization import (
|
|
46
|
+
export_to_dict,
|
|
47
|
+
export_to_file,
|
|
48
|
+
from_json,
|
|
49
|
+
from_yaml,
|
|
50
|
+
import_from_dict,
|
|
51
|
+
import_from_file,
|
|
52
|
+
to_json,
|
|
53
|
+
to_yaml,
|
|
54
|
+
)
|
|
55
|
+
from .slug import slugify
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"ActionExecuteState",
|
|
59
|
+
"ActionExecuted",
|
|
60
|
+
"All",
|
|
61
|
+
"Any",
|
|
62
|
+
"Blackboard",
|
|
63
|
+
"Condition",
|
|
64
|
+
"DataCollectionState",
|
|
65
|
+
"Directive",
|
|
66
|
+
"Eq",
|
|
67
|
+
"ErrorType",
|
|
68
|
+
"Event",
|
|
69
|
+
"Exists",
|
|
70
|
+
"Gt",
|
|
71
|
+
"Gte",
|
|
72
|
+
"HumanHandoffState",
|
|
73
|
+
"In",
|
|
74
|
+
"Lt",
|
|
75
|
+
"Lte",
|
|
76
|
+
"Ne",
|
|
77
|
+
"Not",
|
|
78
|
+
"Ref",
|
|
79
|
+
"Regex",
|
|
80
|
+
"Requirement",
|
|
81
|
+
"Result",
|
|
82
|
+
"Session",
|
|
83
|
+
"State",
|
|
84
|
+
"StateCompleted",
|
|
85
|
+
"StateMetadata",
|
|
86
|
+
"StateReopened",
|
|
87
|
+
"Status",
|
|
88
|
+
"Transition",
|
|
89
|
+
"TransitionMetadata",
|
|
90
|
+
"TransitionTaken",
|
|
91
|
+
"UpdateResult",
|
|
92
|
+
"ValueChanged",
|
|
93
|
+
"ValueSet",
|
|
94
|
+
"Workflow",
|
|
95
|
+
"condition_fields",
|
|
96
|
+
"evaluate",
|
|
97
|
+
"export_to_dict",
|
|
98
|
+
"export_to_file",
|
|
99
|
+
"from_json",
|
|
100
|
+
"from_yaml",
|
|
101
|
+
"import_from_dict",
|
|
102
|
+
"import_from_file",
|
|
103
|
+
"referenced_fields",
|
|
104
|
+
"resolve",
|
|
105
|
+
"slugify",
|
|
106
|
+
"state_registry",
|
|
107
|
+
"to_json",
|
|
108
|
+
"to_yaml",
|
|
109
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared Pydantic bases used across every layer.
|
|
2
|
+
|
|
3
|
+
``StrictModel`` is the default: it rejects unknown fields and revalidates on
|
|
4
|
+
assignment, so a malformed definition can never silently exist. ``StrictOpenModel``
|
|
5
|
+
relaxes only the "unknown fields" rule — used for metadata, where we validate the
|
|
6
|
+
*shape* we rely on but let business policy attach anything else.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StrictModel(BaseModel):
|
|
15
|
+
"""Reject unknown fields; validate on every assignment."""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StrictOpenModel(BaseModel):
|
|
21
|
+
"""Validate declared fields, but allow extra ones (extensible metadata)."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(extra="allow", validate_assignment=True)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Conditions: a small, closed, typed boolean vocabulary over the Blackboard.
|
|
2
|
+
|
|
3
|
+
A ``Condition`` reads collected data and returns a ``bool`` — the only way the
|
|
4
|
+
machine asks a yes/no question. The vocabulary is deliberately closed (not an open
|
|
5
|
+
expression language): it validates at authoring time, serializes to JSON, and is
|
|
6
|
+
safe to evaluate with no ``eval``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .evaluate import condition_fields, evaluate
|
|
12
|
+
from .models import (
|
|
13
|
+
All,
|
|
14
|
+
Any,
|
|
15
|
+
Condition,
|
|
16
|
+
Eq,
|
|
17
|
+
Exists,
|
|
18
|
+
Gt,
|
|
19
|
+
Gte,
|
|
20
|
+
In,
|
|
21
|
+
Lt,
|
|
22
|
+
Lte,
|
|
23
|
+
Ne,
|
|
24
|
+
Not,
|
|
25
|
+
Regex,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"All",
|
|
30
|
+
"Any",
|
|
31
|
+
"Condition",
|
|
32
|
+
"Eq",
|
|
33
|
+
"Exists",
|
|
34
|
+
"Gt",
|
|
35
|
+
"Gte",
|
|
36
|
+
"In",
|
|
37
|
+
"Lt",
|
|
38
|
+
"Lte",
|
|
39
|
+
"Ne",
|
|
40
|
+
"Not",
|
|
41
|
+
"Regex",
|
|
42
|
+
"condition_fields",
|
|
43
|
+
"evaluate",
|
|
44
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Pure evaluation of a ``Condition`` against collected data.
|
|
2
|
+
|
|
3
|
+
The evaluator is a dispatch table: ``op`` → a tiny pure function. Two invariants
|
|
4
|
+
hold for every leaf:
|
|
5
|
+
|
|
6
|
+
* A condition over a **missing** field is ``False`` — never an error. Absent data
|
|
7
|
+
can never assert anything, so a workflow can't be steered by data it doesn't have.
|
|
8
|
+
* Evaluation **never raises** on type mismatches (e.g. comparing a string to a
|
|
9
|
+
number) — it returns ``False``. Determinism must not depend on data being clean.
|
|
10
|
+
|
|
11
|
+
Only ``Not`` inverts, so ``Not(Exists("x"))`` is ``True`` when ``x`` is absent — the
|
|
12
|
+
one intentional way to assert on absence.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import Callable, Mapping
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .models import (
|
|
22
|
+
All,
|
|
23
|
+
Condition,
|
|
24
|
+
Eq,
|
|
25
|
+
Exists,
|
|
26
|
+
Gt,
|
|
27
|
+
Gte,
|
|
28
|
+
In,
|
|
29
|
+
Lt,
|
|
30
|
+
Lte,
|
|
31
|
+
Ne,
|
|
32
|
+
Not,
|
|
33
|
+
Regex,
|
|
34
|
+
)
|
|
35
|
+
from .models import (
|
|
36
|
+
Any as AnyCond,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_MISSING = object()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def evaluate(condition: Condition, data: Mapping[str, Any]) -> bool:
|
|
43
|
+
"""Return whether ``condition`` holds for ``data``."""
|
|
44
|
+
return _DISPATCH[type(condition)](condition, data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def condition_fields(condition: Condition) -> set[str]:
|
|
48
|
+
"""Every Blackboard field name a condition tree references.
|
|
49
|
+
|
|
50
|
+
Used to report which data a state is still waiting on: the absent members of
|
|
51
|
+
this set are its missing fields.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(condition, All | AnyCond):
|
|
54
|
+
if not condition.of:
|
|
55
|
+
return set()
|
|
56
|
+
return set().union(*(condition_fields(sub) for sub in condition.of))
|
|
57
|
+
if isinstance(condition, Not):
|
|
58
|
+
return condition_fields(condition.of)
|
|
59
|
+
return {condition.field}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get(data: Mapping[str, Any], field: str) -> Any:
|
|
63
|
+
return data.get(field, _MISSING)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _exists(c: Exists, data: Mapping[str, Any]) -> bool:
|
|
67
|
+
value = _get(data, c.field)
|
|
68
|
+
return value is not _MISSING and value is not None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _eq(c: Eq, data: Mapping[str, Any]) -> bool:
|
|
72
|
+
value = _get(data, c.field)
|
|
73
|
+
return value is not _MISSING and value == c.value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ne(c: Ne, data: Mapping[str, Any]) -> bool:
|
|
77
|
+
value = _get(data, c.field)
|
|
78
|
+
return value is not _MISSING and value != c.value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _in(c: In, data: Mapping[str, Any]) -> bool:
|
|
82
|
+
value = _get(data, c.field)
|
|
83
|
+
return value is not _MISSING and value in c.value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _regex(c: Regex, data: Mapping[str, Any]) -> bool:
|
|
87
|
+
value = _get(data, c.field)
|
|
88
|
+
return isinstance(value, str) and re.search(c.value, value) is not None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _compare(op: Callable[[Any, Any], bool]) -> Callable[[Any, Mapping[str, Any]], bool]:
|
|
92
|
+
def run(c: Any, data: Mapping[str, Any]) -> bool:
|
|
93
|
+
value = _get(data, c.field)
|
|
94
|
+
if value is _MISSING:
|
|
95
|
+
return False
|
|
96
|
+
try:
|
|
97
|
+
return op(value, c.value)
|
|
98
|
+
except TypeError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
return run
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _all(c: All, data: Mapping[str, Any]) -> bool:
|
|
105
|
+
return all(evaluate(sub, data) for sub in c.of)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _any(c: AnyCond, data: Mapping[str, Any]) -> bool:
|
|
109
|
+
return any(evaluate(sub, data) for sub in c.of)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _not(c: Not, data: Mapping[str, Any]) -> bool:
|
|
113
|
+
return not evaluate(c.of, data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_DISPATCH: dict[type, Callable[[Any, Mapping[str, Any]], bool]] = {
|
|
117
|
+
Exists: _exists,
|
|
118
|
+
Eq: _eq,
|
|
119
|
+
Ne: _ne,
|
|
120
|
+
In: _in,
|
|
121
|
+
Regex: _regex,
|
|
122
|
+
Gt: _compare(lambda a, b: a > b),
|
|
123
|
+
Gte: _compare(lambda a, b: a >= b),
|
|
124
|
+
Lt: _compare(lambda a, b: a < b),
|
|
125
|
+
Lte: _compare(lambda a, b: a <= b),
|
|
126
|
+
All: _all,
|
|
127
|
+
AnyCond: _any,
|
|
128
|
+
Not: _not,
|
|
129
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""The ``Condition`` discriminated union.
|
|
2
|
+
|
|
3
|
+
Each condition is a small immutable value object tagged by ``op``. Leaf conditions
|
|
4
|
+
test one field; combinators (``all``/``any``/``not``) nest other conditions. The
|
|
5
|
+
union is discriminated on ``op`` so it round-trips cleanly through JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Annotated, Literal
|
|
11
|
+
from typing import Any as AnyType
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
15
|
+
from ..base import StrictModel
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Exists(StrictModel):
|
|
19
|
+
"""True when ``field`` is present and not null."""
|
|
20
|
+
|
|
21
|
+
op: Literal["exists"] = "exists"
|
|
22
|
+
field: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Eq(StrictModel):
|
|
26
|
+
"""True when ``field`` exists and equals ``value``."""
|
|
27
|
+
|
|
28
|
+
op: Literal["eq"] = "eq"
|
|
29
|
+
field: str
|
|
30
|
+
value: AnyType
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Ne(StrictModel):
|
|
34
|
+
"""True when ``field`` exists and does not equal ``value`` (missing → False)."""
|
|
35
|
+
|
|
36
|
+
op: Literal["ne"] = "ne"
|
|
37
|
+
field: str
|
|
38
|
+
value: AnyType
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class In(StrictModel):
|
|
42
|
+
"""True when ``field`` exists and its value is one of ``value``."""
|
|
43
|
+
|
|
44
|
+
op: Literal["in"] = "in"
|
|
45
|
+
field: str
|
|
46
|
+
value: list[AnyType]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Regex(StrictModel):
|
|
50
|
+
"""True when ``field`` is a string that ``value`` (a regex) finds a match in."""
|
|
51
|
+
|
|
52
|
+
op: Literal["regex"] = "regex"
|
|
53
|
+
field: str
|
|
54
|
+
value: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Gt(StrictModel):
|
|
58
|
+
op: Literal["gt"] = "gt"
|
|
59
|
+
field: str
|
|
60
|
+
value: AnyType
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Gte(StrictModel):
|
|
64
|
+
op: Literal["gte"] = "gte"
|
|
65
|
+
field: str
|
|
66
|
+
value: AnyType
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Lt(StrictModel):
|
|
70
|
+
op: Literal["lt"] = "lt"
|
|
71
|
+
field: str
|
|
72
|
+
value: AnyType
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Lte(StrictModel):
|
|
76
|
+
op: Literal["lte"] = "lte"
|
|
77
|
+
field: str
|
|
78
|
+
value: AnyType
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class All(StrictModel):
|
|
82
|
+
"""True when every nested condition is true (vacuously true when empty)."""
|
|
83
|
+
|
|
84
|
+
op: Literal["all"] = "all"
|
|
85
|
+
of: list[Condition]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Any(StrictModel):
|
|
89
|
+
"""True when at least one nested condition is true (false when empty)."""
|
|
90
|
+
|
|
91
|
+
op: Literal["any"] = "any"
|
|
92
|
+
of: list[Condition]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Not(StrictModel):
|
|
96
|
+
"""The negation of a nested condition."""
|
|
97
|
+
|
|
98
|
+
op: Literal["not"] = "not"
|
|
99
|
+
of: Condition
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Condition = Annotated[
|
|
103
|
+
Exists | Eq | Ne | In | Regex | Gt | Gte | Lt | Lte | All | Any | Not,
|
|
104
|
+
Field(discriminator="op"),
|
|
105
|
+
]
|
|
106
|
+
"""Any boolean rule over the Blackboard."""
|
|
107
|
+
|
|
108
|
+
# Resolve the forward references inside the recursive combinators.
|
|
109
|
+
All.model_rebuild()
|
|
110
|
+
Any.model_rebuild()
|
|
111
|
+
Not.model_rebuild()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The definition layer: the immutable, serializable models a user authors."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""State and transition metadata: typed-but-open policy carriers.
|
|
2
|
+
|
|
3
|
+
The reserved sections are the shape the machine relies on being *present*; their
|
|
4
|
+
contents are opaque business policy the machine never interprets. ``extra="allow"``
|
|
5
|
+
(via ``StrictOpenModel``) lets authors attach anything beyond the reserved keys.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from ..base import StrictOpenModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StateMetadata(StrictOpenModel):
|
|
18
|
+
"""Policy attached to a state. Contents are opaque to the machine."""
|
|
19
|
+
|
|
20
|
+
tools: dict[str, Any] = Field(default_factory=dict)
|
|
21
|
+
prompts: dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
guardrails: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TransitionMetadata(StrictOpenModel):
|
|
26
|
+
"""Policy attached to a transition. Contents are opaque to the machine."""
|
|
27
|
+
|
|
28
|
+
events: dict[str, Any] = Field(default_factory=dict)
|
|
29
|
+
resources: dict[str, Any] = Field(default_factory=dict)
|
|
30
|
+
reward: float = Field(default=1.0, ge=0.0, description="Transition reward (experimental)")
|