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.
- reflexr-0.1.0/LICENSE +21 -0
- reflexr-0.1.0/PKG-INFO +196 -0
- reflexr-0.1.0/README.md +135 -0
- reflexr-0.1.0/pyproject.toml +177 -0
- reflexr-0.1.0/pyproject.toml.orig +144 -0
- reflexr-0.1.0/src/reflexr/__init__.py +78 -0
- reflexr-0.1.0/src/reflexr/agent/__init__.py +24 -0
- reflexr-0.1.0/src/reflexr/agent/actions.py +63 -0
- reflexr-0.1.0/src/reflexr/agent/context.py +90 -0
- reflexr-0.1.0/src/reflexr/agent/graphs.py +411 -0
- reflexr-0.1.0/src/reflexr/agent/models.py +69 -0
- reflexr-0.1.0/src/reflexr/agent/render.py +33 -0
- reflexr-0.1.0/src/reflexr/agent/tools.py +102 -0
- reflexr-0.1.0/src/reflexr/core/__init__.py +379 -0
- reflexr-0.1.0/src/reflexr/core/actors.py +148 -0
- reflexr-0.1.0/src/reflexr/core/conditions.py +463 -0
- reflexr-0.1.0/src/reflexr/core/errors.py +107 -0
- reflexr-0.1.0/src/reflexr/core/evaluation.py +488 -0
- reflexr-0.1.0/src/reflexr/core/events.py +656 -0
- reflexr-0.1.0/src/reflexr/core/feedback.py +164 -0
- reflexr-0.1.0/src/reflexr/core/ids.py +81 -0
- reflexr-0.1.0/src/reflexr/core/protocol.py +306 -0
- reflexr-0.1.0/src/reflexr/core/rules.py +290 -0
- reflexr-0.1.0/src/reflexr/core/runs.py +264 -0
- reflexr-0.1.0/src/reflexr/core/schema.py +66 -0
- reflexr-0.1.0/src/reflexr/core/state.py +164 -0
- reflexr-0.1.0/src/reflexr/core/stored.py +214 -0
- reflexr-0.1.0/src/reflexr/core/tagged.py +13 -0
- reflexr-0.1.0/src/reflexr/evals/__init__.py +38 -0
- reflexr-0.1.0/src/reflexr/evals/action.py +61 -0
- reflexr-0.1.0/src/reflexr/evals/context.py +59 -0
- reflexr-0.1.0/src/reflexr/evals/experiments.py +81 -0
- reflexr-0.1.0/src/reflexr/evals/measures.py +131 -0
- reflexr-0.1.0/src/reflexr/evals/source.py +142 -0
- reflexr-0.1.0/src/reflexr/fastapi/__init__.py +30 -0
- reflexr-0.1.0/src/reflexr/fastapi/openapi.py +42 -0
- reflexr-0.1.0/src/reflexr/fastapi/router.py +280 -0
- reflexr-0.1.0/src/reflexr/fastapi/stream.py +211 -0
- reflexr-0.1.0/src/reflexr/langfuse/__init__.py +35 -0
- reflexr-0.1.0/src/reflexr/langfuse/client.py +26 -0
- reflexr-0.1.0/src/reflexr/langfuse/tracing.py +88 -0
- reflexr-0.1.0/src/reflexr/litellm/__init__.py +38 -0
- reflexr-0.1.0/src/reflexr/litellm/gateway.py +198 -0
- reflexr-0.1.0/src/reflexr/mcp/__init__.py +10 -0
- reflexr-0.1.0/src/reflexr/mcp/server.py +573 -0
- reflexr-0.1.0/src/reflexr/otel/__init__.py +52 -0
- reflexr-0.1.0/src/reflexr/otel/configure.py +142 -0
- reflexr-0.1.0/src/reflexr/py.typed +0 -0
- reflexr-0.1.0/src/reflexr/scores/__init__.py +25 -0
- reflexr-0.1.0/src/reflexr/scores/mapping.py +31 -0
- reflexr-0.1.0/src/reflexr/scores/mirror.py +179 -0
- reflexr-0.1.0/src/reflexr/sql/__init__.py +18 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/env.py +17 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/script.py.mako +26 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0001_initial.py +141 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0002_scope_heads.py +29 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0003_event_types.py +43 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0004_cursors.py +35 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0005_qualified_facts.py +101 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0006_stored_rules.py +44 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0007_row_level_security.py +57 -0
- reflexr-0.1.0/src/reflexr/sql/migrations/versions/0008_rule_history.py +56 -0
- reflexr-0.1.0/src/reflexr/sql/schema.py +66 -0
- reflexr-0.1.0/src/reflexr/sql/sqlite.py +18 -0
- reflexr-0.1.0/src/reflexr/sql/storage.py +716 -0
- reflexr-0.1.0/src/reflexr/sql/tables.py +236 -0
- reflexr-0.1.0/src/reflexr/sql/tenancy.py +18 -0
- reflexr-0.1.0/src/reflexr/telemetry/__init__.py +51 -0
- reflexr-0.1.0/src/reflexr/telemetry/attributes.py +63 -0
- reflexr-0.1.0/src/reflexr/telemetry/metrics.py +231 -0
- reflexr-0.1.0/src/reflexr/telemetry/telemetry.py +141 -0
- reflexr-0.1.0/src/reflexr/telemetry/traces.py +68 -0
- reflexr-0.1.0/src/reflexr/workspace/__init__.py +80 -0
- reflexr-0.1.0/src/reflexr/workspace/actions.py +199 -0
- reflexr-0.1.0/src/reflexr/workspace/executor.py +469 -0
- reflexr-0.1.0/src/reflexr/workspace/memory.py +370 -0
- reflexr-0.1.0/src/reflexr/workspace/reactor.py +573 -0
- reflexr-0.1.0/src/reflexr/workspace/results.py +69 -0
- reflexr-0.1.0/src/reflexr/workspace/schedules.py +102 -0
- reflexr-0.1.0/src/reflexr/workspace/storage.py +353 -0
- 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)
|
reflexr-0.1.0/README.md
ADDED
|
@@ -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"
|