reflexr 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.
Files changed (81) hide show
  1. reflexr-0.1.0/LICENSE +21 -0
  2. reflexr-0.1.0/PKG-INFO +196 -0
  3. reflexr-0.1.0/README.md +135 -0
  4. reflexr-0.1.0/pyproject.toml +177 -0
  5. reflexr-0.1.0/pyproject.toml.orig +144 -0
  6. reflexr-0.1.0/src/reflexr/__init__.py +78 -0
  7. reflexr-0.1.0/src/reflexr/agent/__init__.py +24 -0
  8. reflexr-0.1.0/src/reflexr/agent/actions.py +63 -0
  9. reflexr-0.1.0/src/reflexr/agent/context.py +90 -0
  10. reflexr-0.1.0/src/reflexr/agent/graphs.py +411 -0
  11. reflexr-0.1.0/src/reflexr/agent/models.py +69 -0
  12. reflexr-0.1.0/src/reflexr/agent/render.py +33 -0
  13. reflexr-0.1.0/src/reflexr/agent/tools.py +102 -0
  14. reflexr-0.1.0/src/reflexr/core/__init__.py +379 -0
  15. reflexr-0.1.0/src/reflexr/core/actors.py +148 -0
  16. reflexr-0.1.0/src/reflexr/core/conditions.py +463 -0
  17. reflexr-0.1.0/src/reflexr/core/errors.py +107 -0
  18. reflexr-0.1.0/src/reflexr/core/evaluation.py +488 -0
  19. reflexr-0.1.0/src/reflexr/core/events.py +656 -0
  20. reflexr-0.1.0/src/reflexr/core/feedback.py +164 -0
  21. reflexr-0.1.0/src/reflexr/core/ids.py +81 -0
  22. reflexr-0.1.0/src/reflexr/core/protocol.py +306 -0
  23. reflexr-0.1.0/src/reflexr/core/rules.py +290 -0
  24. reflexr-0.1.0/src/reflexr/core/runs.py +264 -0
  25. reflexr-0.1.0/src/reflexr/core/schema.py +66 -0
  26. reflexr-0.1.0/src/reflexr/core/state.py +164 -0
  27. reflexr-0.1.0/src/reflexr/core/stored.py +214 -0
  28. reflexr-0.1.0/src/reflexr/core/tagged.py +13 -0
  29. reflexr-0.1.0/src/reflexr/evals/__init__.py +38 -0
  30. reflexr-0.1.0/src/reflexr/evals/action.py +61 -0
  31. reflexr-0.1.0/src/reflexr/evals/context.py +59 -0
  32. reflexr-0.1.0/src/reflexr/evals/experiments.py +81 -0
  33. reflexr-0.1.0/src/reflexr/evals/measures.py +131 -0
  34. reflexr-0.1.0/src/reflexr/evals/source.py +142 -0
  35. reflexr-0.1.0/src/reflexr/fastapi/__init__.py +30 -0
  36. reflexr-0.1.0/src/reflexr/fastapi/openapi.py +42 -0
  37. reflexr-0.1.0/src/reflexr/fastapi/router.py +280 -0
  38. reflexr-0.1.0/src/reflexr/fastapi/stream.py +211 -0
  39. reflexr-0.1.0/src/reflexr/langfuse/__init__.py +35 -0
  40. reflexr-0.1.0/src/reflexr/langfuse/client.py +26 -0
  41. reflexr-0.1.0/src/reflexr/langfuse/tracing.py +88 -0
  42. reflexr-0.1.0/src/reflexr/litellm/__init__.py +38 -0
  43. reflexr-0.1.0/src/reflexr/litellm/gateway.py +198 -0
  44. reflexr-0.1.0/src/reflexr/mcp/__init__.py +10 -0
  45. reflexr-0.1.0/src/reflexr/mcp/server.py +573 -0
  46. reflexr-0.1.0/src/reflexr/otel/__init__.py +52 -0
  47. reflexr-0.1.0/src/reflexr/otel/configure.py +142 -0
  48. reflexr-0.1.0/src/reflexr/py.typed +0 -0
  49. reflexr-0.1.0/src/reflexr/scores/__init__.py +25 -0
  50. reflexr-0.1.0/src/reflexr/scores/mapping.py +31 -0
  51. reflexr-0.1.0/src/reflexr/scores/mirror.py +179 -0
  52. reflexr-0.1.0/src/reflexr/sql/__init__.py +18 -0
  53. reflexr-0.1.0/src/reflexr/sql/migrations/env.py +17 -0
  54. reflexr-0.1.0/src/reflexr/sql/migrations/script.py.mako +26 -0
  55. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0001_initial.py +141 -0
  56. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0002_scope_heads.py +29 -0
  57. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0003_event_types.py +43 -0
  58. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0004_cursors.py +35 -0
  59. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0005_qualified_facts.py +101 -0
  60. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0006_stored_rules.py +44 -0
  61. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0007_row_level_security.py +57 -0
  62. reflexr-0.1.0/src/reflexr/sql/migrations/versions/0008_rule_history.py +56 -0
  63. reflexr-0.1.0/src/reflexr/sql/schema.py +66 -0
  64. reflexr-0.1.0/src/reflexr/sql/sqlite.py +18 -0
  65. reflexr-0.1.0/src/reflexr/sql/storage.py +716 -0
  66. reflexr-0.1.0/src/reflexr/sql/tables.py +236 -0
  67. reflexr-0.1.0/src/reflexr/sql/tenancy.py +18 -0
  68. reflexr-0.1.0/src/reflexr/telemetry/__init__.py +51 -0
  69. reflexr-0.1.0/src/reflexr/telemetry/attributes.py +63 -0
  70. reflexr-0.1.0/src/reflexr/telemetry/metrics.py +231 -0
  71. reflexr-0.1.0/src/reflexr/telemetry/telemetry.py +141 -0
  72. reflexr-0.1.0/src/reflexr/telemetry/traces.py +68 -0
  73. reflexr-0.1.0/src/reflexr/workspace/__init__.py +80 -0
  74. reflexr-0.1.0/src/reflexr/workspace/actions.py +199 -0
  75. reflexr-0.1.0/src/reflexr/workspace/executor.py +469 -0
  76. reflexr-0.1.0/src/reflexr/workspace/memory.py +370 -0
  77. reflexr-0.1.0/src/reflexr/workspace/reactor.py +573 -0
  78. reflexr-0.1.0/src/reflexr/workspace/results.py +69 -0
  79. reflexr-0.1.0/src/reflexr/workspace/schedules.py +102 -0
  80. reflexr-0.1.0/src/reflexr/workspace/storage.py +353 -0
  81. reflexr-0.1.0/src/reflexr/workspace/workspace.py +1622 -0
reflexr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Nodeland
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.
reflexr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflexr
3
+ Version: 0.1.0
4
+ Summary: Rules over event streams that run LLM workflows: pydantic-ai agents, pydantic-graph graphs and functions.
5
+ Keywords: agents,llm,events,rules,workflows,pydantic-ai,pydantic-graph,mcp
6
+ Author: Alex Nodeland
7
+ Author-email: Alex Nodeland <alex@ournature.studio>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Framework :: Pydantic :: 2
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: cronsim>=2.7
21
+ Requires-Dist: lattice-core-ai>=0.1.0
22
+ Requires-Dist: opentelemetry-api>=1.45
23
+ Requires-Dist: pydantic>=2.13
24
+ Requires-Dist: pydantic-ai-slim>=2.51
25
+ Requires-Dist: pydantic-core>=2.46.0
26
+ Requires-Dist: pydantic-graph>=2.51
27
+ Requires-Dist: evalr>=0.1.0.dev0 ; extra == 'evals'
28
+ Requires-Dist: fastapi>=0.141 ; extra == 'fastapi'
29
+ Requires-Dist: starlette>=0.46.0 ; extra == 'fastapi'
30
+ Requires-Dist: lattice-core-ai[langfuse]>=0.1.0 ; extra == 'langfuse'
31
+ Requires-Dist: langfuse>=4.15,<5 ; extra == 'langfuse'
32
+ Requires-Dist: evalr[langfuse]>=0.1.0.dev0 ; extra == 'langfuse'
33
+ Requires-Dist: lattice-core-ai[litellm]>=0.1.0 ; extra == 'litellm'
34
+ Requires-Dist: mcp>=2.2 ; extra == 'mcp'
35
+ Requires-Dist: starlette>=0.27 ; extra == 'mcp'
36
+ Requires-Dist: lattice-core-ai[otel]>=0.1.0 ; extra == 'otel'
37
+ Requires-Dist: opentelemetry-sdk>=1.45 ; extra == 'otel'
38
+ Requires-Dist: reflexr[sql] ; extra == 'postgres'
39
+ Requires-Dist: asyncpg>=0.31 ; extra == 'postgres'
40
+ Requires-Dist: lattice-core-ai[sql]>=0.1.0 ; extra == 'sql'
41
+ Requires-Dist: sqlalchemy[asyncio]>=2.1 ; extra == 'sql'
42
+ Requires-Dist: alembic>=1.20 ; extra == 'sql'
43
+ Requires-Dist: reflexr[sql] ; extra == 'sqlite'
44
+ Requires-Dist: aiosqlite>=0.22 ; extra == 'sqlite'
45
+ Requires-Python: >=3.12
46
+ Project-URL: Homepage, https://lattice.alexnodeland.com/reflexr/
47
+ Project-URL: Documentation, https://lattice.alexnodeland.com/reflexr/
48
+ Project-URL: Repository, https://github.com/alexnodeland/lattice
49
+ Project-URL: Issues, https://github.com/alexnodeland/lattice/issues
50
+ Project-URL: Changelog, https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/CHANGELOG.md
51
+ Provides-Extra: evals
52
+ Provides-Extra: fastapi
53
+ Provides-Extra: langfuse
54
+ Provides-Extra: litellm
55
+ Provides-Extra: mcp
56
+ Provides-Extra: otel
57
+ Provides-Extra: postgres
58
+ Provides-Extra: sql
59
+ Provides-Extra: sqlite
60
+ Description-Content-Type: text/markdown
61
+
62
+ <picture>
63
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alexnodeland/lattice/main/docs/reflexr/assets/brand/banner-dark.svg">
64
+ <img alt="reflexr: rules that watch streams of events, and run agents, graphs and functions when they fire." src="https://raw.githubusercontent.com/alexnodeland/lattice/main/docs/reflexr/assets/brand/banner-light.svg" width="100%">
65
+ </picture>
66
+
67
+ <p>
68
+ <a href="https://lattice.alexnodeland.com/reflexr/"><img alt="Docs" src="https://img.shields.io/badge/docs-lattice.alexnodeland.com%2Freflexr-B3122E"></a>
69
+ <a href="https://github.com/alexnodeland/lattice/actions/workflows/nightly.yml"><img alt="Nightly" src="https://github.com/alexnodeland/lattice/actions/workflows/nightly.yml/badge.svg?branch=main"></a>
70
+ <img alt="Python 3.12, 3.13 and 3.14" src="https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-B3122E">
71
+ <img alt="Coverage: 100%" src="https://img.shields.io/badge/coverage-100%25-B3122E">
72
+ <img alt="Typed: pyright strict" src="https://img.shields.io/badge/typed-pyright%20strict-B3122E">
73
+ <a href="https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-B3122E"></a>
74
+ </p>
75
+
76
+ **reflexr** is a Python library for **reactive agent workflows**: rules watch the event log of each workspace, and when a rule's condition holds (three errors from one service within a minute, a deploy followed by a spike, a heartbeat that stops), it runs a workflow: a [pydantic-ai](https://ai.pydantic.dev) agent, a pydantic-graph graph, or a plain async function.
77
+
78
+ It is the sibling of [artifactr](https://lattice.alexnodeland.com/artifactr/): artifactr is for live chats in which people and agents edit shared artifacts, and reflexr is for workflows that events start. The two share their conventions (tenants and workspaces, envelopes, actors, storage, the protocol's shape), and neither imports the other.
79
+
80
+ > **Status:** pre-release. v0.1 is built, as planned in [RFC-0001](https://lattice.alexnodeland.com/reflexr/rfcs/0001-v0.1-implementation-plan/), and not yet released; the API may change before the first release. Work from before the rebuild is on the [`archive/pre-rebuild`](https://github.com/alexnodeland/reflexr/tree/archive/pre-rebuild) branch of reflexr's old repository.
81
+
82
+ ## Why
83
+
84
+ Most LLM applications start with a person typing. Many useful workflows start with something happening instead, and need something to watch for it, decide exactly when a pattern holds, and run a workflow in response, without running it twice, losing it in a crash, or letting it trigger itself forever. In reflexr, rules are typed data evaluated as pure reducers over the log, so the same log always makes the same decisions; runs execute at least once under leases, with retries, dead letters and an idempotency key; and depth limits, throttles and allowlists keep workflows in bounds.
85
+
86
+ ## Install
87
+
88
+ Python 3.12 or newer. Until reflexr is on PyPI, install it from its directory in [lattice](https://github.com/alexnodeland/lattice), the repository every package lives in:
89
+
90
+ ```bash
91
+ uv add "reflexr[fastapi] @ git+https://github.com/alexnodeland/lattice#subdirectory=packages/reflexr"
92
+ ```
93
+
94
+ Extras: `fastapi` (publishing, REST and the WebSocket stream), `mcp` (external agents), `postgres` or `sqlite` (SQL storage with a driver), `sql` (SQL storage without one), `otel` (OpenTelemetry in one call), `langfuse` (traces and scores in Langfuse), `litellm` (the LLM gateway) and `evals` (evaluation with [evalr](https://lattice.alexnodeland.com/evalr/)). The `evals` and `langfuse` extras need evalr, which is not on PyPI either: uv takes it from the same commit of lattice, and with pip you install it from lattice too.
95
+
96
+ ## Example
97
+
98
+ A monitoring system publishes errors. When one service reports three severe errors within a minute, a function pages someone:
99
+
100
+ ```python
101
+ import asyncio
102
+ from datetime import timedelta
103
+
104
+ from reflexr import Event, F, Rule, SourceActor, by, on, run
105
+ from reflexr.workspace import InMemoryStorage, Reaction, Reactor, Workspaces
106
+
107
+
108
+ class OpsEvent(Event, abstract=True, event_namespace="ops"): # the application's namespace
109
+ pass
110
+
111
+
112
+ class ServiceError(OpsEvent, name="service.error"): # an event type, ops:service.error
113
+ service: str
114
+ severity: int
115
+
116
+
117
+ error_spike = Rule( # a rule: typed, serializable data
118
+ name="ops:error-spike",
119
+ when=on(ServiceError).where(F.severity >= 7).count(at_least=3, within=timedelta(minutes=1)),
120
+ scope=by(F.service),
121
+ then=run("page"),
122
+ )
123
+
124
+
125
+ async def page(reaction: Reaction[None]) -> str: # an action: a function, an agent or a graph
126
+ return f"paged about {reaction.scope['service']}"
127
+
128
+
129
+ workspaces = Workspaces(InMemoryStorage(), events=[ServiceError], rules=[error_spike])
130
+ reactor = Reactor(workspaces, actions={"page": page})
131
+
132
+
133
+ async def main() -> None:
134
+ workspace = await workspaces.open("acme", "prod", actor=SourceActor(name="monitor"))
135
+ for severity in (8, 9, 3, 8):
136
+ await workspace.publish(ServiceError(service="auth", severity=severity))
137
+ await reactor.settle() # evaluate the rules, then run what fired
138
+
139
+ for envelope in await workspace.read():
140
+ print(envelope.seq, envelope.actor.kind, envelope.event_type)
141
+
142
+
143
+ asyncio.run(main())
144
+ ```
145
+
146
+ The log it prints shows the four errors, the rule firing on the three severe ones, and the run of its action, in order and attributed. Swap `page` for an `AgentAction` and the same rule runs a pydantic-ai agent that is told what fired, can read the log, and can publish the events you allow ([Getting started](https://lattice.alexnodeland.com/reflexr/getting-started/)).
147
+
148
+ ## What you get
149
+
150
+ - **Events as Pydantic models**, published idempotently by id into tenant-scoped workspaces with one ordered log each.
151
+ - **Rules as data**: filters, deduplication, counts in a window, sequences, absences and throttles, scoped per field, with a JSON Schema.
152
+ - **Exact decisions**: a pure, synchronous core with per-rule cursors, where the log is the clock, pinned by language-neutral conformance fixtures.
153
+ - **Durable actions**: at-least-once runs under leases, retries, dead letters, ordering per scope, typed failures, replays, and graphs that resume after their last completed step.
154
+ - **Agents, graphs and functions** over one `Reaction`, with the `EventContext` capability for pydantic-ai agents.
155
+ - **Schedules** on intervals or cron expressions.
156
+ - **Storage in memory, SQLite or PostgreSQL**, behind one protocol.
157
+ - **Surfaces that behave the same**: publishing, REST and a resumable WebSocket stream, and MCP for external agents, through one command handler.
158
+ - **Observability and evaluation**: OpenTelemetry with every run in its causal chain's session, Langfuse, the LiteLLM gateway, typed feedback as scores, and evaluators as rules.
159
+ - **Safety by default**: no self-reaction, causation depth limits, throttles, emit allowlists, timeouts and usage limits.
160
+
161
+ ## Try it
162
+
163
+ [`examples/oncall`](https://github.com/alexnodeland/lattice/blob/main/examples/oncall/README.md) is a complete application built on the library, for incident response: severe alerts run a triage agent that opens an incident, the incident runs a runbook graph that rolls back a recent deploy or pages a person, and a service that stops sending heartbeats is paged. It serves REST, the WebSocket stream and MCP, with a terminal client that watches the log live. From the root of a clone of lattice:
164
+
165
+ ```sh
166
+ make install
167
+ export ANTHROPIC_API_KEY=...
168
+ uv run oncall-serve # in one terminal
169
+ uv run oncall watch # in another
170
+ uv run oncall alert api --severity 8 --message "5xx rate above 5%" # three times, in a third
171
+ ```
172
+
173
+ Set `ONCALL_DATABASE_URL` (for example `sqlite+aiosqlite:///oncall.db`) to keep its workspaces in a database instead of memory.
174
+
175
+ ## Documentation
176
+
177
+ The documentation is reflexr's section of lattice's site, at **<https://lattice.alexnodeland.com/reflexr/>**. It is built from [`docs/reflexr/`](https://github.com/alexnodeland/lattice/tree/main/docs/reflexr) and published from `main` on every push; run `moon run lattice:docs-serve` to read it locally at <http://localhost:8000>.
178
+
179
+ - [Getting started](https://lattice.alexnodeland.com/reflexr/getting-started/) and the [guides](https://lattice.alexnodeland.com/reflexr/guides/events/): events, rules, workspaces, the reactor, actions, schedules, storage, serving, MCP, security, observability, the LLM gateway, evaluation, safety and testing.
180
+ - [Architecture](https://lattice.alexnodeland.com/reflexr/architecture/): concepts, layers, rules, deciding and acting, actions, safety, tenancy and storage.
181
+ - [Stream protocol v1](https://lattice.alexnodeland.com/reflexr/protocol/): the REST, WebSocket and MCP contracts, with generated JSON Schemas for [the protocol](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/schemas/reflexr.v1.json) and [rules](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/schemas/reflexr.rules.v1.json).
182
+ - [Architecture decision records](https://lattice.alexnodeland.com/reflexr/adr/): why each part is the way it is.
183
+ - [RFCs](https://lattice.alexnodeland.com/reflexr/rfcs/): proposals and the v0.1 build plan.
184
+ - [Brand](https://lattice.alexnodeland.com/reflexr/assets/brand/): the mark, colours and type, and how reflexr sits in lattice's brand system.
185
+
186
+ ## Built on
187
+
188
+ [Pydantic](https://docs.pydantic.dev), [pydantic-ai](https://ai.pydantic.dev), [pydantic-graph](https://ai.pydantic.dev/graph/), [cronsim](https://github.com/cuu508/cronsim), [SQLAlchemy](https://www.sqlalchemy.org), [FastAPI](https://fastapi.tiangolo.com), the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) and [OpenTelemetry](https://opentelemetry.io).
189
+
190
+ ## Contributing
191
+
192
+ See [CONTRIBUTING.md](https://github.com/alexnodeland/lattice/blob/main/CONTRIBUTING.md) for setup, the trunk-based workflow, and the RFC and ADR process.
193
+
194
+ ## License
195
+
196
+ [MIT](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/LICENSE)
@@ -0,0 +1,135 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alexnodeland/lattice/main/docs/reflexr/assets/brand/banner-dark.svg">
3
+ <img alt="reflexr: rules that watch streams of events, and run agents, graphs and functions when they fire." src="https://raw.githubusercontent.com/alexnodeland/lattice/main/docs/reflexr/assets/brand/banner-light.svg" width="100%">
4
+ </picture>
5
+
6
+ <p>
7
+ <a href="https://lattice.alexnodeland.com/reflexr/"><img alt="Docs" src="https://img.shields.io/badge/docs-lattice.alexnodeland.com%2Freflexr-B3122E"></a>
8
+ <a href="https://github.com/alexnodeland/lattice/actions/workflows/nightly.yml"><img alt="Nightly" src="https://github.com/alexnodeland/lattice/actions/workflows/nightly.yml/badge.svg?branch=main"></a>
9
+ <img alt="Python 3.12, 3.13 and 3.14" src="https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-B3122E">
10
+ <img alt="Coverage: 100%" src="https://img.shields.io/badge/coverage-100%25-B3122E">
11
+ <img alt="Typed: pyright strict" src="https://img.shields.io/badge/typed-pyright%20strict-B3122E">
12
+ <a href="https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-B3122E"></a>
13
+ </p>
14
+
15
+ **reflexr** is a Python library for **reactive agent workflows**: rules watch the event log of each workspace, and when a rule's condition holds (three errors from one service within a minute, a deploy followed by a spike, a heartbeat that stops), it runs a workflow: a [pydantic-ai](https://ai.pydantic.dev) agent, a pydantic-graph graph, or a plain async function.
16
+
17
+ It is the sibling of [artifactr](https://lattice.alexnodeland.com/artifactr/): artifactr is for live chats in which people and agents edit shared artifacts, and reflexr is for workflows that events start. The two share their conventions (tenants and workspaces, envelopes, actors, storage, the protocol's shape), and neither imports the other.
18
+
19
+ > **Status:** pre-release. v0.1 is built, as planned in [RFC-0001](https://lattice.alexnodeland.com/reflexr/rfcs/0001-v0.1-implementation-plan/), and not yet released; the API may change before the first release. Work from before the rebuild is on the [`archive/pre-rebuild`](https://github.com/alexnodeland/reflexr/tree/archive/pre-rebuild) branch of reflexr's old repository.
20
+
21
+ ## Why
22
+
23
+ Most LLM applications start with a person typing. Many useful workflows start with something happening instead, and need something to watch for it, decide exactly when a pattern holds, and run a workflow in response, without running it twice, losing it in a crash, or letting it trigger itself forever. In reflexr, rules are typed data evaluated as pure reducers over the log, so the same log always makes the same decisions; runs execute at least once under leases, with retries, dead letters and an idempotency key; and depth limits, throttles and allowlists keep workflows in bounds.
24
+
25
+ ## Install
26
+
27
+ Python 3.12 or newer. Until reflexr is on PyPI, install it from its directory in [lattice](https://github.com/alexnodeland/lattice), the repository every package lives in:
28
+
29
+ ```bash
30
+ uv add "reflexr[fastapi] @ git+https://github.com/alexnodeland/lattice#subdirectory=packages/reflexr"
31
+ ```
32
+
33
+ Extras: `fastapi` (publishing, REST and the WebSocket stream), `mcp` (external agents), `postgres` or `sqlite` (SQL storage with a driver), `sql` (SQL storage without one), `otel` (OpenTelemetry in one call), `langfuse` (traces and scores in Langfuse), `litellm` (the LLM gateway) and `evals` (evaluation with [evalr](https://lattice.alexnodeland.com/evalr/)). The `evals` and `langfuse` extras need evalr, which is not on PyPI either: uv takes it from the same commit of lattice, and with pip you install it from lattice too.
34
+
35
+ ## Example
36
+
37
+ A monitoring system publishes errors. When one service reports three severe errors within a minute, a function pages someone:
38
+
39
+ ```python
40
+ import asyncio
41
+ from datetime import timedelta
42
+
43
+ from reflexr import Event, F, Rule, SourceActor, by, on, run
44
+ from reflexr.workspace import InMemoryStorage, Reaction, Reactor, Workspaces
45
+
46
+
47
+ class OpsEvent(Event, abstract=True, event_namespace="ops"): # the application's namespace
48
+ pass
49
+
50
+
51
+ class ServiceError(OpsEvent, name="service.error"): # an event type, ops:service.error
52
+ service: str
53
+ severity: int
54
+
55
+
56
+ error_spike = Rule( # a rule: typed, serializable data
57
+ name="ops:error-spike",
58
+ when=on(ServiceError).where(F.severity >= 7).count(at_least=3, within=timedelta(minutes=1)),
59
+ scope=by(F.service),
60
+ then=run("page"),
61
+ )
62
+
63
+
64
+ async def page(reaction: Reaction[None]) -> str: # an action: a function, an agent or a graph
65
+ return f"paged about {reaction.scope['service']}"
66
+
67
+
68
+ workspaces = Workspaces(InMemoryStorage(), events=[ServiceError], rules=[error_spike])
69
+ reactor = Reactor(workspaces, actions={"page": page})
70
+
71
+
72
+ async def main() -> None:
73
+ workspace = await workspaces.open("acme", "prod", actor=SourceActor(name="monitor"))
74
+ for severity in (8, 9, 3, 8):
75
+ await workspace.publish(ServiceError(service="auth", severity=severity))
76
+ await reactor.settle() # evaluate the rules, then run what fired
77
+
78
+ for envelope in await workspace.read():
79
+ print(envelope.seq, envelope.actor.kind, envelope.event_type)
80
+
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ The log it prints shows the four errors, the rule firing on the three severe ones, and the run of its action, in order and attributed. Swap `page` for an `AgentAction` and the same rule runs a pydantic-ai agent that is told what fired, can read the log, and can publish the events you allow ([Getting started](https://lattice.alexnodeland.com/reflexr/getting-started/)).
86
+
87
+ ## What you get
88
+
89
+ - **Events as Pydantic models**, published idempotently by id into tenant-scoped workspaces with one ordered log each.
90
+ - **Rules as data**: filters, deduplication, counts in a window, sequences, absences and throttles, scoped per field, with a JSON Schema.
91
+ - **Exact decisions**: a pure, synchronous core with per-rule cursors, where the log is the clock, pinned by language-neutral conformance fixtures.
92
+ - **Durable actions**: at-least-once runs under leases, retries, dead letters, ordering per scope, typed failures, replays, and graphs that resume after their last completed step.
93
+ - **Agents, graphs and functions** over one `Reaction`, with the `EventContext` capability for pydantic-ai agents.
94
+ - **Schedules** on intervals or cron expressions.
95
+ - **Storage in memory, SQLite or PostgreSQL**, behind one protocol.
96
+ - **Surfaces that behave the same**: publishing, REST and a resumable WebSocket stream, and MCP for external agents, through one command handler.
97
+ - **Observability and evaluation**: OpenTelemetry with every run in its causal chain's session, Langfuse, the LiteLLM gateway, typed feedback as scores, and evaluators as rules.
98
+ - **Safety by default**: no self-reaction, causation depth limits, throttles, emit allowlists, timeouts and usage limits.
99
+
100
+ ## Try it
101
+
102
+ [`examples/oncall`](https://github.com/alexnodeland/lattice/blob/main/examples/oncall/README.md) is a complete application built on the library, for incident response: severe alerts run a triage agent that opens an incident, the incident runs a runbook graph that rolls back a recent deploy or pages a person, and a service that stops sending heartbeats is paged. It serves REST, the WebSocket stream and MCP, with a terminal client that watches the log live. From the root of a clone of lattice:
103
+
104
+ ```sh
105
+ make install
106
+ export ANTHROPIC_API_KEY=...
107
+ uv run oncall-serve # in one terminal
108
+ uv run oncall watch # in another
109
+ uv run oncall alert api --severity 8 --message "5xx rate above 5%" # three times, in a third
110
+ ```
111
+
112
+ Set `ONCALL_DATABASE_URL` (for example `sqlite+aiosqlite:///oncall.db`) to keep its workspaces in a database instead of memory.
113
+
114
+ ## Documentation
115
+
116
+ The documentation is reflexr's section of lattice's site, at **<https://lattice.alexnodeland.com/reflexr/>**. It is built from [`docs/reflexr/`](https://github.com/alexnodeland/lattice/tree/main/docs/reflexr) and published from `main` on every push; run `moon run lattice:docs-serve` to read it locally at <http://localhost:8000>.
117
+
118
+ - [Getting started](https://lattice.alexnodeland.com/reflexr/getting-started/) and the [guides](https://lattice.alexnodeland.com/reflexr/guides/events/): events, rules, workspaces, the reactor, actions, schedules, storage, serving, MCP, security, observability, the LLM gateway, evaluation, safety and testing.
119
+ - [Architecture](https://lattice.alexnodeland.com/reflexr/architecture/): concepts, layers, rules, deciding and acting, actions, safety, tenancy and storage.
120
+ - [Stream protocol v1](https://lattice.alexnodeland.com/reflexr/protocol/): the REST, WebSocket and MCP contracts, with generated JSON Schemas for [the protocol](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/schemas/reflexr.v1.json) and [rules](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/schemas/reflexr.rules.v1.json).
121
+ - [Architecture decision records](https://lattice.alexnodeland.com/reflexr/adr/): why each part is the way it is.
122
+ - [RFCs](https://lattice.alexnodeland.com/reflexr/rfcs/): proposals and the v0.1 build plan.
123
+ - [Brand](https://lattice.alexnodeland.com/reflexr/assets/brand/): the mark, colours and type, and how reflexr sits in lattice's brand system.
124
+
125
+ ## Built on
126
+
127
+ [Pydantic](https://docs.pydantic.dev), [pydantic-ai](https://ai.pydantic.dev), [pydantic-graph](https://ai.pydantic.dev/graph/), [cronsim](https://github.com/cuu508/cronsim), [SQLAlchemy](https://www.sqlalchemy.org), [FastAPI](https://fastapi.tiangolo.com), the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) and [OpenTelemetry](https://opentelemetry.io).
128
+
129
+ ## Contributing
130
+
131
+ See [CONTRIBUTING.md](https://github.com/alexnodeland/lattice/blob/main/CONTRIBUTING.md) for setup, the trunk-based workflow, and the RFC and ADR process.
132
+
133
+ ## License
134
+
135
+ [MIT](https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/LICENSE)
@@ -0,0 +1,177 @@
1
+ [project]
2
+ name = "reflexr"
3
+ version = "0.1.0"
4
+ description = "Rules over event streams that run LLM workflows: pydantic-ai agents, pydantic-graph graphs and functions."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = [
10
+ "agents",
11
+ "llm",
12
+ "events",
13
+ "rules",
14
+ "workflows",
15
+ "pydantic-ai",
16
+ "pydantic-graph",
17
+ "mcp",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 2 - Pre-Alpha",
21
+ "Framework :: Pydantic :: 2",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = [
32
+ "cronsim>=2.7",
33
+ "lattice-core-ai>=0.1.0",
34
+ "opentelemetry-api>=1.45",
35
+ "pydantic>=2.13",
36
+ "pydantic-ai-slim>=2.51",
37
+ "pydantic-core>=2.46.0",
38
+ "pydantic-graph>=2.51",
39
+ ]
40
+
41
+ [[project.authors]]
42
+ name = "Alex Nodeland"
43
+ email = "alex@ournature.studio"
44
+
45
+ [project.optional-dependencies]
46
+ sql = [
47
+ "lattice-core-ai[sql]>=0.1.0",
48
+ "sqlalchemy[asyncio]>=2.1",
49
+ "alembic>=1.20",
50
+ ]
51
+ sqlite = [
52
+ "reflexr[sql]",
53
+ "aiosqlite>=0.22",
54
+ ]
55
+ postgres = [
56
+ "reflexr[sql]",
57
+ "asyncpg>=0.31",
58
+ ]
59
+ fastapi = [
60
+ "fastapi>=0.141",
61
+ "starlette>=0.46.0",
62
+ ]
63
+ mcp = [
64
+ "mcp>=2.2",
65
+ "starlette>=0.27",
66
+ ]
67
+ evals = ["evalr>=0.1.0.dev0"]
68
+ langfuse = [
69
+ "lattice-core-ai[langfuse]>=0.1.0",
70
+ "langfuse>=4.15,<5",
71
+ "evalr[langfuse]>=0.1.0.dev0",
72
+ ]
73
+ litellm = ["lattice-core-ai[litellm]>=0.1.0"]
74
+ otel = [
75
+ "lattice-core-ai[otel]>=0.1.0",
76
+ "opentelemetry-sdk>=1.45",
77
+ ]
78
+
79
+ [project.urls]
80
+ Homepage = "https://lattice.alexnodeland.com/reflexr/"
81
+ Documentation = "https://lattice.alexnodeland.com/reflexr/"
82
+ Repository = "https://github.com/alexnodeland/lattice"
83
+ Issues = "https://github.com/alexnodeland/lattice/issues"
84
+ Changelog = "https://github.com/alexnodeland/lattice/blob/main/packages/reflexr/CHANGELOG.md"
85
+
86
+ [dependency-groups]
87
+ test = [
88
+ "pytest>=9.1",
89
+ "pytest-asyncio>=1.4",
90
+ "pytest-cov>=7.1",
91
+ "pytest-timeout>=2.4",
92
+ "hypothesis>=6.168",
93
+ "aiosqlite>=0.22",
94
+ "asyncpg>=0.31",
95
+ "opentelemetry-sdk>=1.45",
96
+ "httpx>=0.28",
97
+ "fastapi>=0.141",
98
+ "mcp>=2.2",
99
+ "evalr>=0.1.0.dev0",
100
+ "jsonschema>=4.26",
101
+ ]
102
+ lint = [
103
+ "ruff>=0.16",
104
+ "pyright>=1.1.414",
105
+ ]
106
+
107
+ [[dependency-groups.dev]]
108
+ include-group = "test"
109
+
110
+ [[dependency-groups.dev]]
111
+ include-group = "lint"
112
+
113
+ [tool.uv.sources.evalr]
114
+ workspace = true
115
+
116
+ [tool.uv.sources.lattice-core-ai]
117
+ workspace = true
118
+
119
+ [tool.deptry.package_module_name_map]
120
+ lattice-core-ai = "lattice_core"
121
+
122
+ [tool.pyright]
123
+ include = [
124
+ "src",
125
+ "tests",
126
+ "scripts",
127
+ ]
128
+ strict = [
129
+ "src",
130
+ "scripts",
131
+ ]
132
+ typeCheckingMode = "standard"
133
+ pythonVersion = "3.12"
134
+ venvPath = "../.."
135
+ venv = ".venv"
136
+
137
+ [tool.pytest.ini_options]
138
+ testpaths = ["tests"]
139
+ addopts = [
140
+ "--strict-markers",
141
+ "--strict-config",
142
+ "-ra",
143
+ ]
144
+ timeout = 60
145
+ timeout_method = "thread"
146
+ asyncio_mode = "auto"
147
+ asyncio_default_fixture_loop_scope = "function"
148
+ filterwarnings = [
149
+ "error",
150
+ "ignore:'asyncio.iscoroutinefunction' is deprecated:DeprecationWarning:backoff._decorator",
151
+ ]
152
+ markers = ["postgres: needs a PostgreSQL database (set REFLEXR_TEST_POSTGRES_URL)"]
153
+
154
+ [tool.coverage.run]
155
+ branch = true
156
+ source = ["src/reflexr"]
157
+ concurrency = [
158
+ "greenlet",
159
+ "thread",
160
+ ]
161
+
162
+ [tool.coverage.report]
163
+ fail_under = 100
164
+ show_missing = true
165
+ skip_covered = true
166
+ exclude_also = [
167
+ "if TYPE_CHECKING:",
168
+ "@overload",
169
+ 'assert_never\(',
170
+ "raise NotImplementedError",
171
+ '^\s*\.\.\.$',
172
+ 'class .*\(Protocol.*\):',
173
+ ]
174
+
175
+ [build-system]
176
+ requires = ["uv_build>=0.12,<0.13"]
177
+ build-backend = "uv_build"