crewcontext 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.
- crewcontext-0.1.0/LICENSE +21 -0
- crewcontext-0.1.0/PKG-INFO +135 -0
- crewcontext-0.1.0/README.md +102 -0
- crewcontext-0.1.0/crewcontext/__init__.py +40 -0
- crewcontext-0.1.0/crewcontext/cli.py +39 -0
- crewcontext-0.1.0/crewcontext/context.py +281 -0
- crewcontext-0.1.0/crewcontext/demos/__init__.py +1 -0
- crewcontext-0.1.0/crewcontext/demos/vendor_discrepancy.py +205 -0
- crewcontext-0.1.0/crewcontext/models.py +134 -0
- crewcontext-0.1.0/crewcontext/projection/__init__.py +5 -0
- crewcontext-0.1.0/crewcontext/projection/neo4j.py +228 -0
- crewcontext-0.1.0/crewcontext/projection/projector.py +130 -0
- crewcontext-0.1.0/crewcontext/router.py +237 -0
- crewcontext-0.1.0/crewcontext/store/__init__.py +5 -0
- crewcontext-0.1.0/crewcontext/store/base.py +86 -0
- crewcontext-0.1.0/crewcontext/store/postgres.py +356 -0
- crewcontext-0.1.0/crewcontext/utils.py +8 -0
- crewcontext-0.1.0/crewcontext.egg-info/PKG-INFO +135 -0
- crewcontext-0.1.0/crewcontext.egg-info/SOURCES.txt +29 -0
- crewcontext-0.1.0/crewcontext.egg-info/dependency_links.txt +1 -0
- crewcontext-0.1.0/crewcontext.egg-info/entry_points.txt +2 -0
- crewcontext-0.1.0/crewcontext.egg-info/requires.txt +9 -0
- crewcontext-0.1.0/crewcontext.egg-info/top_level.txt +4 -0
- crewcontext-0.1.0/pyproject.toml +55 -0
- crewcontext-0.1.0/setup.cfg +4 -0
- crewcontext-0.1.0/tests/__init__.py +1 -0
- crewcontext-0.1.0/tests/conftest.py +29 -0
- crewcontext-0.1.0/tests/test_models.py +102 -0
- crewcontext-0.1.0/tests/test_postgres_store.py +176 -0
- crewcontext-0.1.0/tests/test_router.py +165 -0
- crewcontext-0.1.0/tests/test_temporal_asof.py +6 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CrewContext
|
|
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,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crewcontext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Auditable shared memory for AI agent systems
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/crewcontext/crewcontext
|
|
7
|
+
Project-URL: Documentation, https://github.com/crewcontext/crewcontext/tree/main/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/crewcontext/crewcontext
|
|
9
|
+
Project-URL: Changelog, https://github.com/crewcontext/crewcontext/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/crewcontext/crewcontext/issues
|
|
11
|
+
Keywords: agents,multi-agent,context,event-sourcing,audit,orchestration
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
25
|
+
Requires-Dist: psycopg_pool>=3.1
|
|
26
|
+
Requires-Dist: neo4j>=5.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0
|
|
28
|
+
Requires-Dist: click>=8.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img width="128" height="128" alt="crewcontext_logo_128" src="https://github.com/user-attachments/assets/db17d668-adca-401d-ab4e-901c05f60af4" />
|
|
36
|
+
</p>
|
|
37
|
+
<p align="center">
|
|
38
|
+
<h1 align="center">CrewContext</h1>
|
|
39
|
+
<p align="center"><strong>Auditable shared memory for AI agent systems.</strong></p>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## The Problem
|
|
44
|
+
|
|
45
|
+
Multi-agent AI systems break at handoffs. Agent 1 processes an invoice. Agent 2 validates it. Agent 3 reconciles discrepancies. But Agent 3 has no idea what Agent 1 found. Context is lost. Decisions are invisible. Nothing is auditable.
|
|
46
|
+
|
|
47
|
+
Existing agent frameworks give you orchestration — **but not memory**. Chat history is personal. RAG is read-only. Neither gives you a shared, structured, temporal record of what happened, who did it, and why.
|
|
48
|
+
|
|
49
|
+
In regulated industries — finance, insurance, compliance — this isn't just inconvenient. It's a liability.
|
|
50
|
+
|
|
51
|
+
## What CrewContext Does
|
|
52
|
+
|
|
53
|
+
CrewContext is a **context coordination layer** that sits underneath your agent framework and provides:
|
|
54
|
+
|
|
55
|
+
- **Shared event store** — Every agent action is recorded. Nothing is lost at handoffs.
|
|
56
|
+
- **Causal DAG** — Every event tracks what caused it. You can answer "why did this happen?" by walking the chain backwards.
|
|
57
|
+
- **Temporal queries** — Reconstruct the exact state of any entity at any point in time. "What did we know at 2pm yesterday?"
|
|
58
|
+
- **Versioned entities** — Business objects (invoices, customers, claims) are snapshotted at each stage, never overwritten.
|
|
59
|
+
- **Policy router** — Deterministic, auditable routing rules with composable conditions. No black boxes.
|
|
60
|
+
- **Provenance tracking** — Every event records which agent, what scope, and when. Built for auditors.
|
|
61
|
+
|
|
62
|
+
## Architecture
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
┌──────────────────────────────────┐
|
|
66
|
+
│ ProcessContext API │
|
|
67
|
+
│ emit · query · timeline · causal│
|
|
68
|
+
└──────────┬───────────────────────┘
|
|
69
|
+
│
|
|
70
|
+
┌────────────────┼────────────────┐
|
|
71
|
+
│ │ │
|
|
72
|
+
┌─────────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
|
|
73
|
+
│ PostgreSQL │ │ Neo4j │ │ Policy │
|
|
74
|
+
│ Event Store │ │ Graph │ │ Router │
|
|
75
|
+
│ │ │ │ │ │
|
|
76
|
+
│ Append-only │ │ Lineage │ │ Rules │
|
|
77
|
+
│ Temporal │ │ Causal │ │ Pub/Sub │
|
|
78
|
+
│ Causal links │ │ DAG │ │ Routing │
|
|
79
|
+
│ Versioned │ │ Typed │ │ decisions │
|
|
80
|
+
│ entities │ │ relations │ │ │
|
|
81
|
+
└────────────────┘ └────────────┘ └─────────────┘
|
|
82
|
+
(truth) (optional) (in-process)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**PostgreSQL** is the source of truth — append-only event log, versioned entity snapshots, causal link table. **Neo4j** is an optional projection for graph queries and lineage visualization. **Policy Router** evaluates events against composable rules in-process.
|
|
86
|
+
|
|
87
|
+
## Who It's For
|
|
88
|
+
|
|
89
|
+
CrewContext is for teams building multi-agent systems where **trust, auditability, and context preservation** matter:
|
|
90
|
+
|
|
91
|
+
- **Financial operations** — Payment processing, reconciliation, dispute resolution
|
|
92
|
+
- **KYC/AML compliance** — Auditable decision trails for regulators
|
|
93
|
+
- **Insurance claims** — Multi-stage pipelines where context loss means money lost
|
|
94
|
+
- **Supply chain** — Order-to-delivery orchestration across multiple agents
|
|
95
|
+
- **Any regulated workflow** where "the AI decided" isn't a good enough answer
|
|
96
|
+
|
|
97
|
+
## Framework-Agnostic
|
|
98
|
+
|
|
99
|
+
CrewContext is not a replacement for your agent framework. It's the **memory layer underneath it**. It works with:
|
|
100
|
+
|
|
101
|
+
- [CrewAI](https://github.com/joaomdmoura/crewAI)
|
|
102
|
+
- [LangGraph](https://github.com/langchain-ai/langgraph)
|
|
103
|
+
- [AutoGen](https://github.com/microsoft/autogen)
|
|
104
|
+
- [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)
|
|
105
|
+
- Custom agent systems
|
|
106
|
+
|
|
107
|
+
## Getting Started
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install crewcontext
|
|
111
|
+
docker compose up -d
|
|
112
|
+
crewcontext init-db
|
|
113
|
+
crewcontext demo vendor-discrepancy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Full API documentation and examples are available in the [docs](docs/) directory.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
| Environment Variable | Default | Description |
|
|
121
|
+
|---------------------|---------|-------------|
|
|
122
|
+
| `CREWCONTEXT_DB_URL` | `postgresql://crew:crew@localhost:5432/crewcontext` | PostgreSQL connection |
|
|
123
|
+
| `CREWCONTEXT_NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt endpoint |
|
|
124
|
+
| `CREWCONTEXT_NEO4J_USER` | `neo4j` | Neo4j username |
|
|
125
|
+
| `CREWCONTEXT_NEO4J_PASSWORD` | `crewcontext123` | Neo4j password |
|
|
126
|
+
|
|
127
|
+
Neo4j is optional. Pass `enable_neo4j=False` for Postgres-only mode.
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img width="128" height="128" alt="crewcontext_logo_128" src="https://github.com/user-attachments/assets/db17d668-adca-401d-ab4e-901c05f60af4" />
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<h1 align="center">CrewContext</h1>
|
|
6
|
+
<p align="center"><strong>Auditable shared memory for AI agent systems.</strong></p>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## The Problem
|
|
11
|
+
|
|
12
|
+
Multi-agent AI systems break at handoffs. Agent 1 processes an invoice. Agent 2 validates it. Agent 3 reconciles discrepancies. But Agent 3 has no idea what Agent 1 found. Context is lost. Decisions are invisible. Nothing is auditable.
|
|
13
|
+
|
|
14
|
+
Existing agent frameworks give you orchestration — **but not memory**. Chat history is personal. RAG is read-only. Neither gives you a shared, structured, temporal record of what happened, who did it, and why.
|
|
15
|
+
|
|
16
|
+
In regulated industries — finance, insurance, compliance — this isn't just inconvenient. It's a liability.
|
|
17
|
+
|
|
18
|
+
## What CrewContext Does
|
|
19
|
+
|
|
20
|
+
CrewContext is a **context coordination layer** that sits underneath your agent framework and provides:
|
|
21
|
+
|
|
22
|
+
- **Shared event store** — Every agent action is recorded. Nothing is lost at handoffs.
|
|
23
|
+
- **Causal DAG** — Every event tracks what caused it. You can answer "why did this happen?" by walking the chain backwards.
|
|
24
|
+
- **Temporal queries** — Reconstruct the exact state of any entity at any point in time. "What did we know at 2pm yesterday?"
|
|
25
|
+
- **Versioned entities** — Business objects (invoices, customers, claims) are snapshotted at each stage, never overwritten.
|
|
26
|
+
- **Policy router** — Deterministic, auditable routing rules with composable conditions. No black boxes.
|
|
27
|
+
- **Provenance tracking** — Every event records which agent, what scope, and when. Built for auditors.
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌──────────────────────────────────┐
|
|
33
|
+
│ ProcessContext API │
|
|
34
|
+
│ emit · query · timeline · causal│
|
|
35
|
+
└──────────┬───────────────────────┘
|
|
36
|
+
│
|
|
37
|
+
┌────────────────┼────────────────┐
|
|
38
|
+
│ │ │
|
|
39
|
+
┌─────────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
|
|
40
|
+
│ PostgreSQL │ │ Neo4j │ │ Policy │
|
|
41
|
+
│ Event Store │ │ Graph │ │ Router │
|
|
42
|
+
│ │ │ │ │ │
|
|
43
|
+
│ Append-only │ │ Lineage │ │ Rules │
|
|
44
|
+
│ Temporal │ │ Causal │ │ Pub/Sub │
|
|
45
|
+
│ Causal links │ │ DAG │ │ Routing │
|
|
46
|
+
│ Versioned │ │ Typed │ │ decisions │
|
|
47
|
+
│ entities │ │ relations │ │ │
|
|
48
|
+
└────────────────┘ └────────────┘ └─────────────┘
|
|
49
|
+
(truth) (optional) (in-process)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**PostgreSQL** is the source of truth — append-only event log, versioned entity snapshots, causal link table. **Neo4j** is an optional projection for graph queries and lineage visualization. **Policy Router** evaluates events against composable rules in-process.
|
|
53
|
+
|
|
54
|
+
## Who It's For
|
|
55
|
+
|
|
56
|
+
CrewContext is for teams building multi-agent systems where **trust, auditability, and context preservation** matter:
|
|
57
|
+
|
|
58
|
+
- **Financial operations** — Payment processing, reconciliation, dispute resolution
|
|
59
|
+
- **KYC/AML compliance** — Auditable decision trails for regulators
|
|
60
|
+
- **Insurance claims** — Multi-stage pipelines where context loss means money lost
|
|
61
|
+
- **Supply chain** — Order-to-delivery orchestration across multiple agents
|
|
62
|
+
- **Any regulated workflow** where "the AI decided" isn't a good enough answer
|
|
63
|
+
|
|
64
|
+
## Framework-Agnostic
|
|
65
|
+
|
|
66
|
+
CrewContext is not a replacement for your agent framework. It's the **memory layer underneath it**. It works with:
|
|
67
|
+
|
|
68
|
+
- [CrewAI](https://github.com/joaomdmoura/crewAI)
|
|
69
|
+
- [LangGraph](https://github.com/langchain-ai/langgraph)
|
|
70
|
+
- [AutoGen](https://github.com/microsoft/autogen)
|
|
71
|
+
- [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)
|
|
72
|
+
- Custom agent systems
|
|
73
|
+
|
|
74
|
+
## Getting Started
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install crewcontext
|
|
78
|
+
docker compose up -d
|
|
79
|
+
crewcontext init-db
|
|
80
|
+
crewcontext demo vendor-discrepancy
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Full API documentation and examples are available in the [docs](docs/) directory.
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
| Environment Variable | Default | Description |
|
|
88
|
+
|---------------------|---------|-------------|
|
|
89
|
+
| `CREWCONTEXT_DB_URL` | `postgresql://crew:crew@localhost:5432/crewcontext` | PostgreSQL connection |
|
|
90
|
+
| `CREWCONTEXT_NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt endpoint |
|
|
91
|
+
| `CREWCONTEXT_NEO4J_USER` | `neo4j` | Neo4j username |
|
|
92
|
+
| `CREWCONTEXT_NEO4J_PASSWORD` | `crewcontext123` | Neo4j password |
|
|
93
|
+
|
|
94
|
+
Neo4j is optional. Pass `enable_neo4j=False` for Postgres-only mode.
|
|
95
|
+
|
|
96
|
+
## Contributing
|
|
97
|
+
|
|
98
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""CrewContext — Context coordination layer for multi-agent workflows.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from crewcontext import ProcessContext, Entity, Event, Relation
|
|
5
|
+
|
|
6
|
+
with ProcessContext(process_id="p1", agent_id="agent-1") as ctx:
|
|
7
|
+
event = ctx.emit("invoice.received", {"amount": 5000}, entity_id="inv-1")
|
|
8
|
+
"""
|
|
9
|
+
from .context import ProcessContext
|
|
10
|
+
from .models import Entity, Event, Relation, RoutingDecision, generate_id
|
|
11
|
+
from .router import (
|
|
12
|
+
PolicyRouter,
|
|
13
|
+
all_of,
|
|
14
|
+
any_of,
|
|
15
|
+
none_of,
|
|
16
|
+
data_field_eq,
|
|
17
|
+
data_field_gt,
|
|
18
|
+
data_field_ne,
|
|
19
|
+
data_fields_differ,
|
|
20
|
+
event_type_is,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ProcessContext",
|
|
26
|
+
"Entity",
|
|
27
|
+
"Event",
|
|
28
|
+
"Relation",
|
|
29
|
+
"RoutingDecision",
|
|
30
|
+
"generate_id",
|
|
31
|
+
"PolicyRouter",
|
|
32
|
+
"all_of",
|
|
33
|
+
"any_of",
|
|
34
|
+
"none_of",
|
|
35
|
+
"data_field_eq",
|
|
36
|
+
"data_field_gt",
|
|
37
|
+
"data_field_ne",
|
|
38
|
+
"data_fields_differ",
|
|
39
|
+
"event_type_is",
|
|
40
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""CLI for CrewContext."""
|
|
2
|
+
import click
|
|
3
|
+
from .utils import load_env
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
@click.version_option(version="0.1.0")
|
|
8
|
+
def main():
|
|
9
|
+
"""CrewContext - Context coordination for multi-agent workflows."""
|
|
10
|
+
load_env()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@main.group()
|
|
14
|
+
def demo():
|
|
15
|
+
"""Run demo scenarios."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@demo.command("vendor-discrepancy")
|
|
20
|
+
def vendor_discrepancy():
|
|
21
|
+
"""Run the vendor discrepancy demo."""
|
|
22
|
+
from .demos.vendor_discrepancy import run_demo
|
|
23
|
+
run_demo()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@main.command("init-db")
|
|
27
|
+
@click.option("--db-url", envvar="CREWCONTEXT_DB_URL", default=None)
|
|
28
|
+
def init_db(db_url):
|
|
29
|
+
"""Initialise the database schema."""
|
|
30
|
+
from .store.postgres import PostgresStore
|
|
31
|
+
store = PostgresStore(db_url)
|
|
32
|
+
store.connect()
|
|
33
|
+
store.init_schema()
|
|
34
|
+
store.close()
|
|
35
|
+
click.echo("Database schema initialised.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""ProcessContext — the main API agents interact with.
|
|
2
|
+
|
|
3
|
+
This is the public interface. Agents emit events, query history,
|
|
4
|
+
build entity snapshots, and trace causal chains through this class.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
with ProcessContext(process_id="proc-1", agent_id="agent-a") as ctx:
|
|
8
|
+
e1 = ctx.emit("invoice.received", {"amount": 5000}, entity_id="inv-1")
|
|
9
|
+
e2 = ctx.emit("invoice.validated", {"ok": True}, entity_id="inv-1", caused_by=[e1])
|
|
10
|
+
history = ctx.timeline("inv-1")
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
from .models import Entity, Event, Relation, RoutingDecision, generate_id
|
|
19
|
+
from .projection.projector import Neo4jProjector
|
|
20
|
+
from .router import PolicyRouter
|
|
21
|
+
from .store.postgres import PostgresStore
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessContext:
|
|
27
|
+
"""Scoped, temporal, causal context for a business process.
|
|
28
|
+
|
|
29
|
+
Features:
|
|
30
|
+
- **emit()**: Record events with optional causal parents.
|
|
31
|
+
- **query()**: Retrieve events with temporal/scope/type filtering.
|
|
32
|
+
- **timeline()**: Ordered event history for an entity.
|
|
33
|
+
- **snapshot()**: Save/retrieve versioned entity state.
|
|
34
|
+
- **causal_chain()**: Walk the causal DAG.
|
|
35
|
+
- **subscribe()**: React to event types in real-time.
|
|
36
|
+
- **batch_emit()**: Atomic multi-event writes.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
process_id: str,
|
|
42
|
+
agent_id: str,
|
|
43
|
+
scope: str = "default",
|
|
44
|
+
db_url: Optional[str] = None,
|
|
45
|
+
enable_neo4j: bool = True,
|
|
46
|
+
):
|
|
47
|
+
self.process_id = process_id
|
|
48
|
+
self.agent_id = agent_id
|
|
49
|
+
self.scope = scope
|
|
50
|
+
|
|
51
|
+
self._store = PostgresStore(db_url)
|
|
52
|
+
self._projector = Neo4jProjector() if enable_neo4j else None
|
|
53
|
+
self._router = PolicyRouter()
|
|
54
|
+
self._connected = False
|
|
55
|
+
|
|
56
|
+
# -- context manager ----------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> ProcessContext:
|
|
59
|
+
self.connect()
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
63
|
+
self.close()
|
|
64
|
+
|
|
65
|
+
def connect(self) -> None:
|
|
66
|
+
self._store.connect()
|
|
67
|
+
self._store.init_schema()
|
|
68
|
+
self._connected = True
|
|
69
|
+
if self._projector:
|
|
70
|
+
self._projector.connect() # best-effort, logs on failure
|
|
71
|
+
log.info(
|
|
72
|
+
"ProcessContext ready: process=%s agent=%s scope=%s",
|
|
73
|
+
self.process_id, self.agent_id, self.scope,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
self._store.close()
|
|
78
|
+
if self._projector:
|
|
79
|
+
self._projector.close()
|
|
80
|
+
self._connected = False
|
|
81
|
+
|
|
82
|
+
# -- router access ------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def router(self) -> PolicyRouter:
|
|
86
|
+
return self._router
|
|
87
|
+
|
|
88
|
+
# -- emit events --------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def emit(
|
|
91
|
+
self,
|
|
92
|
+
event_type: str,
|
|
93
|
+
data: Dict[str, Any],
|
|
94
|
+
entity_id: Optional[str] = None,
|
|
95
|
+
relation_id: Optional[str] = None,
|
|
96
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
97
|
+
caused_by: Optional[Sequence[Event]] = None,
|
|
98
|
+
) -> Event:
|
|
99
|
+
"""Emit an event into the process context.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
event_type: Dotted event name (e.g. "invoice.received").
|
|
103
|
+
data: Event payload.
|
|
104
|
+
entity_id: Optional entity this event affects.
|
|
105
|
+
relation_id: Optional relation this event affects.
|
|
106
|
+
metadata: Arbitrary metadata.
|
|
107
|
+
caused_by: Parent events that caused this one (builds the DAG).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The persisted Event object.
|
|
111
|
+
"""
|
|
112
|
+
parent_ids = tuple(e.id for e in caused_by) if caused_by else ()
|
|
113
|
+
|
|
114
|
+
event = Event(
|
|
115
|
+
id=generate_id(),
|
|
116
|
+
type=event_type,
|
|
117
|
+
process_id=self.process_id,
|
|
118
|
+
data=data,
|
|
119
|
+
agent_id=self.agent_id,
|
|
120
|
+
entity_id=entity_id,
|
|
121
|
+
relation_id=relation_id,
|
|
122
|
+
scope=self.scope,
|
|
123
|
+
metadata=metadata or {},
|
|
124
|
+
parent_ids=parent_ids,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 1. Persist to source of truth
|
|
128
|
+
self._store.save_event(event)
|
|
129
|
+
|
|
130
|
+
# 2. Project to graph (best-effort)
|
|
131
|
+
if self._projector:
|
|
132
|
+
self._projector.project_event(event)
|
|
133
|
+
|
|
134
|
+
# 3. Notify subscribers
|
|
135
|
+
self._router.notify_subscribers(event)
|
|
136
|
+
|
|
137
|
+
# 4. Evaluate routing rules
|
|
138
|
+
decision = self._router.evaluate(event)
|
|
139
|
+
if decision:
|
|
140
|
+
self._persist_routing_decision(decision, parent_event=event)
|
|
141
|
+
|
|
142
|
+
log.debug("Emitted: %s (type=%s, entity=%s)", event.id[:8], event_type, entity_id)
|
|
143
|
+
return event
|
|
144
|
+
|
|
145
|
+
def batch_emit(
|
|
146
|
+
self, events_spec: List[Dict[str, Any]]
|
|
147
|
+
) -> List[Event]:
|
|
148
|
+
"""Atomically emit multiple events in a single transaction.
|
|
149
|
+
|
|
150
|
+
Each spec is a dict with keys: event_type, data, and optionally
|
|
151
|
+
entity_id, relation_id, metadata.
|
|
152
|
+
"""
|
|
153
|
+
events = []
|
|
154
|
+
for spec in events_spec:
|
|
155
|
+
event = Event(
|
|
156
|
+
id=generate_id(),
|
|
157
|
+
type=spec["event_type"],
|
|
158
|
+
process_id=self.process_id,
|
|
159
|
+
data=spec["data"],
|
|
160
|
+
agent_id=self.agent_id,
|
|
161
|
+
entity_id=spec.get("entity_id"),
|
|
162
|
+
relation_id=spec.get("relation_id"),
|
|
163
|
+
scope=self.scope,
|
|
164
|
+
metadata=spec.get("metadata", {}),
|
|
165
|
+
)
|
|
166
|
+
events.append(event)
|
|
167
|
+
|
|
168
|
+
self._store.save_events(events)
|
|
169
|
+
|
|
170
|
+
# Best-effort graph projection
|
|
171
|
+
if self._projector:
|
|
172
|
+
for ev in events:
|
|
173
|
+
self._projector.project_event(ev)
|
|
174
|
+
|
|
175
|
+
return events
|
|
176
|
+
|
|
177
|
+
def _persist_routing_decision(
|
|
178
|
+
self, decision: RoutingDecision, parent_event: Event
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Save a routing decision as a child event (no re-evaluation)."""
|
|
181
|
+
decision_event = Event(
|
|
182
|
+
id=generate_id(),
|
|
183
|
+
type="routing.decision",
|
|
184
|
+
process_id=self.process_id,
|
|
185
|
+
data=decision.to_dict(),
|
|
186
|
+
agent_id="system.router",
|
|
187
|
+
entity_id=parent_event.entity_id,
|
|
188
|
+
scope=self.scope,
|
|
189
|
+
parent_ids=(parent_event.id,),
|
|
190
|
+
)
|
|
191
|
+
self._store.save_event(decision_event)
|
|
192
|
+
if self._projector:
|
|
193
|
+
self._projector.project_event(decision_event)
|
|
194
|
+
|
|
195
|
+
# -- query events -------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def query(
|
|
198
|
+
self,
|
|
199
|
+
entity_id: Optional[str] = None,
|
|
200
|
+
event_type: Optional[str] = None,
|
|
201
|
+
scope: Optional[str] = None,
|
|
202
|
+
as_of: Optional[datetime] = None,
|
|
203
|
+
limit: int = 1000,
|
|
204
|
+
offset: int = 0,
|
|
205
|
+
) -> List[Dict[str, Any]]:
|
|
206
|
+
"""Query events in this process with optional filters."""
|
|
207
|
+
return self._store.query_events(
|
|
208
|
+
self.process_id,
|
|
209
|
+
entity_id=entity_id,
|
|
210
|
+
event_type=event_type,
|
|
211
|
+
scope=scope or self.scope,
|
|
212
|
+
as_of=as_of,
|
|
213
|
+
limit=limit,
|
|
214
|
+
offset=offset,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def timeline(self, entity_id: str, as_of: Optional[datetime] = None) -> List[Dict[str, Any]]:
|
|
218
|
+
"""Get the full ordered event history for an entity."""
|
|
219
|
+
return self._store.query_events(
|
|
220
|
+
self.process_id,
|
|
221
|
+
entity_id=entity_id,
|
|
222
|
+
as_of=as_of,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# -- entity snapshots ---------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def save_entity(self, entity: Entity) -> None:
|
|
228
|
+
"""Save a versioned entity snapshot."""
|
|
229
|
+
self._store.save_entity(entity)
|
|
230
|
+
if self._projector:
|
|
231
|
+
self._projector.project_entity(entity)
|
|
232
|
+
|
|
233
|
+
def get_entity(
|
|
234
|
+
self, entity_id: str, as_of: Optional[datetime] = None
|
|
235
|
+
) -> Optional[Dict[str, Any]]:
|
|
236
|
+
"""Retrieve the latest entity state (or state at a point in time)."""
|
|
237
|
+
return self._store.get_entity(entity_id, as_of=as_of)
|
|
238
|
+
|
|
239
|
+
# -- relations ----------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def save_relation(self, relation: Relation) -> None:
|
|
242
|
+
"""Persist a typed relation between entities."""
|
|
243
|
+
self._store.save_relation(relation)
|
|
244
|
+
if self._projector:
|
|
245
|
+
self._projector.project_relation(relation)
|
|
246
|
+
|
|
247
|
+
# -- causal DAG ---------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def causal_parents(self, event_id: str) -> List[str]:
|
|
250
|
+
"""Get the events that caused this event."""
|
|
251
|
+
return self._store.get_causal_parents(event_id)
|
|
252
|
+
|
|
253
|
+
def causal_children(self, event_id: str) -> List[str]:
|
|
254
|
+
"""Get the events caused by this event."""
|
|
255
|
+
return self._store.get_causal_children(event_id)
|
|
256
|
+
|
|
257
|
+
def causal_chain(self, event_id: str, max_depth: int = 10) -> List[Dict[str, Any]]:
|
|
258
|
+
"""Walk the full causal DAG via Neo4j (if available)."""
|
|
259
|
+
if self._projector:
|
|
260
|
+
return self._projector.get_causal_chain(event_id, max_depth=max_depth)
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
# -- graph queries ------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def lineage(self, entity_id: str, max_depth: int = 20) -> List[Dict[str, Any]]:
|
|
266
|
+
"""Get Neo4j lineage for an entity (if available)."""
|
|
267
|
+
if self._projector:
|
|
268
|
+
return self._projector.get_lineage(entity_id, max_depth=max_depth)
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
def cypher(self, query: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
272
|
+
"""Execute a raw Cypher query against Neo4j."""
|
|
273
|
+
if self._projector:
|
|
274
|
+
return self._projector.run_cypher(query, params)
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
# -- pub/sub convenience ------------------------------------------------
|
|
278
|
+
|
|
279
|
+
def subscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
|
|
280
|
+
"""Subscribe to events of a given type."""
|
|
281
|
+
self._router.subscribe(event_type, callback)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Demo scenarios."""
|