rrcp 0.1.0a0__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.
- rrcp-0.1.0a0/.gitignore +10 -0
- rrcp-0.1.0a0/CHANGELOG.md +5 -0
- rrcp-0.1.0a0/CLAUDE.md +142 -0
- rrcp-0.1.0a0/PKG-INFO +35 -0
- rrcp-0.1.0a0/README.md +9 -0
- rrcp-0.1.0a0/docker-compose.test.yml +14 -0
- rrcp-0.1.0a0/pyproject.toml +80 -0
- rrcp-0.1.0a0/src/rrcp/__init__.py +129 -0
- rrcp-0.1.0a0/src/rrcp/analytics/__init__.py +3 -0
- rrcp-0.1.0a0/src/rrcp/analytics/collector.py +56 -0
- rrcp-0.1.0a0/src/rrcp/broadcast/__init__.py +5 -0
- rrcp-0.1.0a0/src/rrcp/broadcast/protocol.py +21 -0
- rrcp-0.1.0a0/src/rrcp/broadcast/recording.py +40 -0
- rrcp-0.1.0a0/src/rrcp/broadcast/socketio.py +58 -0
- rrcp-0.1.0a0/src/rrcp/handler/__init__.py +6 -0
- rrcp-0.1.0a0/src/rrcp/handler/context.py +43 -0
- rrcp-0.1.0a0/src/rrcp/handler/executor.py +245 -0
- rrcp-0.1.0a0/src/rrcp/handler/send.py +100 -0
- rrcp-0.1.0a0/src/rrcp/handler/types.py +14 -0
- rrcp-0.1.0a0/src/rrcp/protocol/__init__.py +0 -0
- rrcp-0.1.0a0/src/rrcp/protocol/content.py +66 -0
- rrcp-0.1.0a0/src/rrcp/protocol/event.py +142 -0
- rrcp-0.1.0a0/src/rrcp/protocol/identity.py +36 -0
- rrcp-0.1.0a0/src/rrcp/protocol/run.py +32 -0
- rrcp-0.1.0a0/src/rrcp/protocol/tenant.py +10 -0
- rrcp-0.1.0a0/src/rrcp/protocol/thread.py +37 -0
- rrcp-0.1.0a0/src/rrcp/server/__init__.py +3 -0
- rrcp-0.1.0a0/src/rrcp/server/auth.py +19 -0
- rrcp-0.1.0a0/src/rrcp/server/namespace.py +59 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/__init__.py +0 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/deps.py +45 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/invocations.py +78 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/members.py +73 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/messages.py +69 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/runs.py +53 -0
- rrcp-0.1.0a0/src/rrcp/server/rest/threads.py +141 -0
- rrcp-0.1.0a0/src/rrcp/server/thread_server.py +167 -0
- rrcp-0.1.0a0/src/rrcp/socketio/__init__.py +3 -0
- rrcp-0.1.0a0/src/rrcp/socketio/server.py +411 -0
- rrcp-0.1.0a0/src/rrcp/store/__init__.py +4 -0
- rrcp-0.1.0a0/src/rrcp/store/postgres/__init__.py +0 -0
- rrcp-0.1.0a0/src/rrcp/store/postgres/schema.sql +55 -0
- rrcp-0.1.0a0/src/rrcp/store/postgres/store.py +378 -0
- rrcp-0.1.0a0/src/rrcp/store/protocol.py +56 -0
- rrcp-0.1.0a0/src/rrcp/store/types.py +29 -0
- rrcp-0.1.0a0/tests/__init__.py +0 -0
- rrcp-0.1.0a0/tests/broadcast/__init__.py +0 -0
- rrcp-0.1.0a0/tests/broadcast/test_recording.py +145 -0
- rrcp-0.1.0a0/tests/conftest.py +37 -0
- rrcp-0.1.0a0/tests/handler/__init__.py +0 -0
- rrcp-0.1.0a0/tests/handler/test_analytics.py +33 -0
- rrcp-0.1.0a0/tests/handler/test_ctx_invoke.py +81 -0
- rrcp-0.1.0a0/tests/handler/test_executor.py +157 -0
- rrcp-0.1.0a0/tests/handler/test_send.py +39 -0
- rrcp-0.1.0a0/tests/protocol/__init__.py +0 -0
- rrcp-0.1.0a0/tests/protocol/test_content.py +63 -0
- rrcp-0.1.0a0/tests/protocol/test_event.py +124 -0
- rrcp-0.1.0a0/tests/protocol/test_identity.py +44 -0
- rrcp-0.1.0a0/tests/protocol/test_run.py +24 -0
- rrcp-0.1.0a0/tests/protocol/test_tenant.py +22 -0
- rrcp-0.1.0a0/tests/protocol/test_thread.py +35 -0
- rrcp-0.1.0a0/tests/server/__init__.py +0 -0
- rrcp-0.1.0a0/tests/server/test_acp_namespace.py +139 -0
- rrcp-0.1.0a0/tests/server/test_authorize.py +80 -0
- rrcp-0.1.0a0/tests/server/test_e2e_chat.py +80 -0
- rrcp-0.1.0a0/tests/server/test_e2e_chat_with_assistant.py +86 -0
- rrcp-0.1.0a0/tests/server/test_namespace.py +112 -0
- rrcp-0.1.0a0/tests/server/test_rest_invoke.py +163 -0
- rrcp-0.1.0a0/tests/server/test_rest_members.py +79 -0
- rrcp-0.1.0a0/tests/server/test_rest_messages.py +99 -0
- rrcp-0.1.0a0/tests/server/test_rest_threads.py +163 -0
- rrcp-0.1.0a0/tests/socketio/__init__.py +0 -0
- rrcp-0.1.0a0/tests/socketio/test_live_integration.py +252 -0
- rrcp-0.1.0a0/tests/socketio/test_namespace_integration.py +337 -0
- rrcp-0.1.0a0/tests/socketio/test_thread_namespace_dispatch.py +197 -0
- rrcp-0.1.0a0/tests/store/__init__.py +0 -0
- rrcp-0.1.0a0/tests/store/test_postgres_events.py +70 -0
- rrcp-0.1.0a0/tests/store/test_postgres_list.py +50 -0
- rrcp-0.1.0a0/tests/store/test_postgres_members.py +54 -0
- rrcp-0.1.0a0/tests/store/test_postgres_runs.py +101 -0
- rrcp-0.1.0a0/tests/store/test_postgres_threads.py +59 -0
- rrcp-0.1.0a0/tests/test_setup.py +7 -0
- rrcp-0.1.0a0/uv.lock +1514 -0
rrcp-0.1.0a0/.gitignore
ADDED
rrcp-0.1.0a0/CLAUDE.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Philosophy (READ FIRST)
|
|
6
|
+
|
|
7
|
+
rrcp is a **Communication Protocol** for assistant-driven threads where multiple
|
|
8
|
+
users and multiple assistants can interact. It is NOT an AI framework, NOT an LLM
|
|
9
|
+
wrapper, and NOT a chat application — it is the wire and the storage underneath them.
|
|
10
|
+
|
|
11
|
+
### Core principles
|
|
12
|
+
|
|
13
|
+
1. **Business agnostic.** The SDK never inspects credentials, never decides who can
|
|
14
|
+
do what, and never owns user/assistant/tool data shapes beyond `{ id, name }`.
|
|
15
|
+
Authentication, authorization, and tenancy semantics are consumer territory,
|
|
16
|
+
exposed through callbacks (`authenticate`, `authorize`) and opaque scope keys.
|
|
17
|
+
|
|
18
|
+
2. **Communication-first.** The SDK ships exactly four primitives: identities,
|
|
19
|
+
threads, events, runs. Everything else is composition by the consumer.
|
|
20
|
+
|
|
21
|
+
3. **REST for state, WebSocket for delivery.** Anything that can be a request/response
|
|
22
|
+
IS a request/response. Socket.IO carries subscriptions and live event push.
|
|
23
|
+
Action operations exist in both transports as thin shells over one handler.
|
|
24
|
+
|
|
25
|
+
4. **Plug and play.** A consumer should be able to mount the SDK on their existing
|
|
26
|
+
FastAPI/Hono app in <30 lines and have a working multi-assistant chat backend.
|
|
27
|
+
No forced microservices, no forced migrations of existing user/auth tables.
|
|
28
|
+
|
|
29
|
+
5. **Drop everything else.** No streaming, no MCP client, no A2A, no settings
|
|
30
|
+
broadcaster, no knowledge sources, no built-in form rendering, no built-in
|
|
31
|
+
blob storage. If a consumer needs these, they bring them.
|
|
32
|
+
|
|
33
|
+
### What goes where
|
|
34
|
+
|
|
35
|
+
- **Wire/storage primitive?** → in the SDK
|
|
36
|
+
- **Real-time push to subscribers?** → Socket.IO event
|
|
37
|
+
- **Request/response, no live push?** → REST endpoint
|
|
38
|
+
- **Decision the SDK shouldn't be making?** → consumer callback
|
|
39
|
+
- **Reasonable in 80% of cases?** → SDK default with opt-out
|
|
40
|
+
- **UI/render concern?** → docs example, not SDK code
|
|
41
|
+
|
|
42
|
+
### When in doubt, follow
|
|
43
|
+
|
|
44
|
+
> "Could the consumer build this themselves with the primitives we already ship?"
|
|
45
|
+
> If yes, do not add it to the SDK.
|
|
46
|
+
|
|
47
|
+
### CHANGELOG discipline
|
|
48
|
+
|
|
49
|
+
- Every PR touching `src/` updates `CHANGELOG.md` under `## [Unreleased]`.
|
|
50
|
+
- Breaking changes get a `BREAKING:` prefix and bump major (in lockstep with the
|
|
51
|
+
other two repos: `rrcp-client-ts` and `rrcp-ts`).
|
|
52
|
+
- Breaking changes ship side-by-side migration notes in the changelog entry.
|
|
53
|
+
- The `changelog.yml` GitHub Action blocks PRs that touch `src/` without
|
|
54
|
+
updating `CHANGELOG.md`.
|
|
55
|
+
|
|
56
|
+
## Repo role
|
|
57
|
+
|
|
58
|
+
`rrcp` (PyPI name) is the **Python server SDK** for the rrcp protocol.
|
|
59
|
+
It is the consumer-facing Python library that ships REST + Socket.IO endpoints
|
|
60
|
+
mountable onto FastAPI (or any ASGI app), backed by Postgres.
|
|
61
|
+
|
|
62
|
+
Built on:
|
|
63
|
+
|
|
64
|
+
- **Python 3.11+**
|
|
65
|
+
- **Pydantic** for protocol models
|
|
66
|
+
- **python-socketio (ASGI)** for the live transport
|
|
67
|
+
- **asyncpg** for the Postgres reference `ThreadStore`
|
|
68
|
+
- **FastAPI compatible** — mounts as a router on existing FastAPI apps
|
|
69
|
+
|
|
70
|
+
It is part of a three-repo family. Major versions are in lockstep:
|
|
71
|
+
|
|
72
|
+
| Repo | Publishes to | Package name |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `rrcp-client-ts` | npm | `@0x0064/rrcp-react` |
|
|
75
|
+
| `rrcp-py` (this repo) | PyPI | `rrcp` |
|
|
76
|
+
| `rrcp-ts` | npm | `@0x0064/rrcp` |
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv sync --all-extras # Install all dependencies
|
|
83
|
+
uv run poe dev # Full sanity check: lint + typecheck + test
|
|
84
|
+
uv run poe build # Build wheel + sdist (python -m build)
|
|
85
|
+
uv run poe format # Format code (ruff format)
|
|
86
|
+
uv run poe format:imports # Sort imports
|
|
87
|
+
uv run poe check # Lint only (ruff check)
|
|
88
|
+
uv run poe check:fix # Auto-fix lint issues
|
|
89
|
+
uv run poe typecheck # Type check only (mypy src/)
|
|
90
|
+
uv run poe test # Run tests
|
|
91
|
+
uv run poe test:cov # Tests with coverage
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Run a single test: `uv run pytest tests/path/to/test.py::test_name -v`
|
|
95
|
+
|
|
96
|
+
### Tests require a Postgres connection
|
|
97
|
+
|
|
98
|
+
Two options. **Either** spin up the bundled docker-compose (zero config):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
docker compose -f docker-compose.test.yml up -d
|
|
102
|
+
uv run poe test
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Or** point at your own Postgres via `DATABASE_URL` (use a dedicated test database
|
|
106
|
+
— tests truncate tables between runs, never point at dev or prod):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
createdb rrcp_test
|
|
110
|
+
DATABASE_URL=postgresql://localhost/rrcp_test uv run poe test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The schema in `src/rrcp/store/postgres/schema.sql` is auto-applied on the first
|
|
114
|
+
test run via `CREATE TABLE IF NOT EXISTS`. No migrations to manage manually.
|
|
115
|
+
|
|
116
|
+
Default `DATABASE_URL` (when unset) points at the docker-compose service:
|
|
117
|
+
`postgresql://rrcp:rrcp@localhost:55432/rrcp_test`
|
|
118
|
+
|
|
119
|
+
## Architecture
|
|
120
|
+
|
|
121
|
+
See `docs/plans/2026-04-09-rrcp-v2-design.md` for the full architecture.
|
|
122
|
+
|
|
123
|
+
Source layout (target):
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
src/rrcp/
|
|
127
|
+
server/ # ThreadServer entrypoint, lifecycle
|
|
128
|
+
protocol/ # Pydantic frame types, parser, mappers
|
|
129
|
+
store/ # ThreadStore protocol + PostgresThreadStore
|
|
130
|
+
handler/ # HandlerContext, HandlerSend, run executor
|
|
131
|
+
socketio/ # Socket.IO event handlers
|
|
132
|
+
rest/ # FastAPI router (/threads, /events, /runs, ...)
|
|
133
|
+
analytics/ # AnalyticsCollector (consumer-callback sink)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Code Style
|
|
137
|
+
|
|
138
|
+
- Python 3.11+, ruff for linting/formatting, line length 120
|
|
139
|
+
- Quote style: double quotes
|
|
140
|
+
- Ruff rules: E, F, I (isort), UP (pyupgrade)
|
|
141
|
+
- `pytest-asyncio` with `asyncio_mode = "auto"` (no need for `@pytest.mark.asyncio`)
|
|
142
|
+
- Pydantic for all models; dataclasses for config objects
|
rrcp-0.1.0a0/PKG-INFO
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rrcp
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: rrcp - agent communication protocol
|
|
5
|
+
Project-URL: Repository, https://github.com/0x0064/rrcp
|
|
6
|
+
Author-email: 0x0064 <user.frndvrgs@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: acp,agent,ai,assistant,python,rrcp,sdk
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: asyncpg>=0.31.0
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Requires-Dist: python-socketio[asgi]<6,>=5.13.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: aiohttp>=3.11.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: build>=1.2.2; extra == 'dev'
|
|
16
|
+
Requires-Dist: fastapi>=0.117.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: httpx>=0.28.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: mypy>=1.19.1; extra == 'dev'
|
|
19
|
+
Requires-Dist: poethepoet>=0.42.1; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.15.7; extra == 'dev'
|
|
24
|
+
Requires-Dist: uvicorn>=0.32.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# rrcp
|
|
28
|
+
|
|
29
|
+
rrcp - agent communication protocol
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT — see [`LICENSE`](../../LICENSE).
|
rrcp-0.1.0a0/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_USER: rrcp
|
|
6
|
+
POSTGRES_PASSWORD: rrcp
|
|
7
|
+
POSTGRES_DB: rrcp_test
|
|
8
|
+
ports:
|
|
9
|
+
- "55432:5432"
|
|
10
|
+
healthcheck:
|
|
11
|
+
test: ["CMD-SHELL", "pg_isready -U rrcp -d rrcp_test"]
|
|
12
|
+
interval: 1s
|
|
13
|
+
timeout: 5s
|
|
14
|
+
retries: 10
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rrcp"
|
|
3
|
+
version = "0.1.0a0"
|
|
4
|
+
description = "rrcp - agent communication protocol"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "0x0064", email = "user.frndvrgs@gmail.com"},
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"rrcp",
|
|
13
|
+
"acp",
|
|
14
|
+
"assistant",
|
|
15
|
+
"agent",
|
|
16
|
+
"sdk",
|
|
17
|
+
"ai",
|
|
18
|
+
"python"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pydantic>=2.12.5",
|
|
23
|
+
"asyncpg>=0.31.0",
|
|
24
|
+
"python-socketio[asgi]>=5.13.0,<6",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/0x0064/rrcp"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/rrcp"]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"mypy>=1.19.1",
|
|
40
|
+
"poethepoet>=0.42.1",
|
|
41
|
+
"pytest>=9.0.2",
|
|
42
|
+
"pytest-asyncio>=1.3.0",
|
|
43
|
+
"pytest-cov>=6.0.0",
|
|
44
|
+
"ruff>=0.15.7",
|
|
45
|
+
"fastapi>=0.117.0",
|
|
46
|
+
"httpx>=0.28.1",
|
|
47
|
+
"uvicorn>=0.32.0",
|
|
48
|
+
"aiohttp>=3.11.0",
|
|
49
|
+
"build>=1.2.2",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 120
|
|
54
|
+
target-version = "py311"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.format]
|
|
60
|
+
quote-style = "double"
|
|
61
|
+
|
|
62
|
+
[[tool.mypy.overrides]]
|
|
63
|
+
module = ["asyncpg", "asyncpg.*", "socketio", "socketio.*"]
|
|
64
|
+
ignore_missing_imports = true
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
asyncio_mode = "auto"
|
|
68
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
69
|
+
asyncio_default_test_loop_scope = "session"
|
|
70
|
+
|
|
71
|
+
[tool.poe.tasks]
|
|
72
|
+
format = "ruff format ."
|
|
73
|
+
"format:imports" = "ruff check --select I --fix ."
|
|
74
|
+
check = "ruff check ."
|
|
75
|
+
"check:fix" = "ruff check --fix ."
|
|
76
|
+
typecheck = "mypy src/rrcp"
|
|
77
|
+
test = "pytest"
|
|
78
|
+
"test:cov" = "pytest --cov=src --cov-report=term-missing"
|
|
79
|
+
dev = ["check", "typecheck", "test"]
|
|
80
|
+
build = "python -m build"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from importlib.metadata import version as _pkg_version
|
|
2
|
+
|
|
3
|
+
__version__ = _pkg_version("rrcp")
|
|
4
|
+
|
|
5
|
+
from rrcp.analytics.collector import (
|
|
6
|
+
AnalyticsEvent,
|
|
7
|
+
AssistantAnalytics,
|
|
8
|
+
OnAnalyticsCallback,
|
|
9
|
+
)
|
|
10
|
+
from rrcp.broadcast.protocol import Broadcaster
|
|
11
|
+
from rrcp.broadcast.recording import RecordingBroadcaster
|
|
12
|
+
from rrcp.broadcast.socketio import SocketIOBroadcaster
|
|
13
|
+
from rrcp.handler.context import HandlerContext
|
|
14
|
+
from rrcp.handler.send import HandlerSend
|
|
15
|
+
from rrcp.handler.types import HandlerCallable
|
|
16
|
+
from rrcp.protocol.content import (
|
|
17
|
+
AudioPart,
|
|
18
|
+
ContentPart,
|
|
19
|
+
DocumentPart,
|
|
20
|
+
FormPart,
|
|
21
|
+
FormStatus,
|
|
22
|
+
ImagePart,
|
|
23
|
+
TextPart,
|
|
24
|
+
parse_content_part,
|
|
25
|
+
)
|
|
26
|
+
from rrcp.protocol.event import (
|
|
27
|
+
Event,
|
|
28
|
+
EventDraft,
|
|
29
|
+
MessageEvent,
|
|
30
|
+
ReasoningEvent,
|
|
31
|
+
RunCancelledEvent,
|
|
32
|
+
RunCompletedEvent,
|
|
33
|
+
RunFailedEvent,
|
|
34
|
+
RunStartedEvent,
|
|
35
|
+
ThreadCreatedEvent,
|
|
36
|
+
ThreadMemberAddedEvent,
|
|
37
|
+
ThreadMemberRemovedEvent,
|
|
38
|
+
ThreadTenantChangedEvent,
|
|
39
|
+
ToolCall,
|
|
40
|
+
ToolCallEvent,
|
|
41
|
+
ToolResult,
|
|
42
|
+
ToolResultEvent,
|
|
43
|
+
parse_event,
|
|
44
|
+
)
|
|
45
|
+
from rrcp.protocol.identity import (
|
|
46
|
+
AssistantIdentity,
|
|
47
|
+
Identity,
|
|
48
|
+
SystemIdentity,
|
|
49
|
+
UserIdentity,
|
|
50
|
+
parse_identity,
|
|
51
|
+
)
|
|
52
|
+
from rrcp.protocol.run import Run, RunError, RunStatus
|
|
53
|
+
from rrcp.protocol.tenant import TenantScope, matches
|
|
54
|
+
from rrcp.protocol.thread import Thread, ThreadMember, ThreadPatch
|
|
55
|
+
from rrcp.server.auth import AuthenticateCallback, AuthorizeCallback, HandshakeData
|
|
56
|
+
from rrcp.server.namespace import (
|
|
57
|
+
NamespaceViolation,
|
|
58
|
+
derive_namespace_path,
|
|
59
|
+
parse_namespace_path,
|
|
60
|
+
validate_namespace_value,
|
|
61
|
+
)
|
|
62
|
+
from rrcp.server.thread_server import ThreadServer
|
|
63
|
+
from rrcp.store.postgres.store import PostgresThreadStore
|
|
64
|
+
from rrcp.store.protocol import ThreadStore
|
|
65
|
+
from rrcp.store.types import EventCursor, Page, ThreadCursor
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"ThreadServer",
|
|
69
|
+
"AnalyticsEvent",
|
|
70
|
+
"AssistantAnalytics",
|
|
71
|
+
"AssistantIdentity",
|
|
72
|
+
"AudioPart",
|
|
73
|
+
"AuthenticateCallback",
|
|
74
|
+
"AuthorizeCallback",
|
|
75
|
+
"Broadcaster",
|
|
76
|
+
"ContentPart",
|
|
77
|
+
"DocumentPart",
|
|
78
|
+
"Event",
|
|
79
|
+
"EventCursor",
|
|
80
|
+
"EventDraft",
|
|
81
|
+
"FormPart",
|
|
82
|
+
"FormStatus",
|
|
83
|
+
"HandlerCallable",
|
|
84
|
+
"HandlerContext",
|
|
85
|
+
"HandlerSend",
|
|
86
|
+
"HandshakeData",
|
|
87
|
+
"Identity",
|
|
88
|
+
"ImagePart",
|
|
89
|
+
"MessageEvent",
|
|
90
|
+
"NamespaceViolation",
|
|
91
|
+
"OnAnalyticsCallback",
|
|
92
|
+
"Page",
|
|
93
|
+
"PostgresThreadStore",
|
|
94
|
+
"ReasoningEvent",
|
|
95
|
+
"RecordingBroadcaster",
|
|
96
|
+
"Run",
|
|
97
|
+
"RunCancelledEvent",
|
|
98
|
+
"RunCompletedEvent",
|
|
99
|
+
"RunError",
|
|
100
|
+
"RunFailedEvent",
|
|
101
|
+
"RunStartedEvent",
|
|
102
|
+
"RunStatus",
|
|
103
|
+
"SocketIOBroadcaster",
|
|
104
|
+
"SystemIdentity",
|
|
105
|
+
"TenantScope",
|
|
106
|
+
"TextPart",
|
|
107
|
+
"Thread",
|
|
108
|
+
"ThreadCreatedEvent",
|
|
109
|
+
"ThreadCursor",
|
|
110
|
+
"ThreadMember",
|
|
111
|
+
"ThreadMemberAddedEvent",
|
|
112
|
+
"ThreadMemberRemovedEvent",
|
|
113
|
+
"ThreadPatch",
|
|
114
|
+
"ThreadStore",
|
|
115
|
+
"ThreadTenantChangedEvent",
|
|
116
|
+
"ToolCall",
|
|
117
|
+
"ToolCallEvent",
|
|
118
|
+
"ToolResult",
|
|
119
|
+
"ToolResultEvent",
|
|
120
|
+
"UserIdentity",
|
|
121
|
+
"__version__",
|
|
122
|
+
"derive_namespace_path",
|
|
123
|
+
"matches",
|
|
124
|
+
"parse_content_part",
|
|
125
|
+
"parse_event",
|
|
126
|
+
"parse_identity",
|
|
127
|
+
"parse_namespace_path",
|
|
128
|
+
"validate_namespace_value",
|
|
129
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalyticsEvent(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
15
|
+
thread_id: str | None = None
|
|
16
|
+
run_id: str | None = None
|
|
17
|
+
assistant_id: str | None = None
|
|
18
|
+
timestamp: datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
OnAnalyticsCallback = Callable[[list[AnalyticsEvent]], Awaitable[None]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AssistantAnalytics:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
on_analytics: OnAnalyticsCallback | None,
|
|
28
|
+
thread_id: str,
|
|
29
|
+
run_id: str,
|
|
30
|
+
assistant_id: str,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._on_analytics = on_analytics
|
|
33
|
+
self._thread_id = thread_id
|
|
34
|
+
self._run_id = run_id
|
|
35
|
+
self._assistant_id = assistant_id
|
|
36
|
+
self._buffer: list[AnalyticsEvent] = []
|
|
37
|
+
|
|
38
|
+
def track(self, name: str, properties: dict[str, Any] | None = None) -> None:
|
|
39
|
+
self._buffer.append(
|
|
40
|
+
AnalyticsEvent(
|
|
41
|
+
name=name,
|
|
42
|
+
properties=properties or {},
|
|
43
|
+
thread_id=self._thread_id,
|
|
44
|
+
run_id=self._run_id,
|
|
45
|
+
assistant_id=self._assistant_id,
|
|
46
|
+
timestamp=datetime.now(UTC),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def flush(self) -> None:
|
|
51
|
+
if not self._buffer or self._on_analytics is None:
|
|
52
|
+
self._buffer = []
|
|
53
|
+
return
|
|
54
|
+
events = self._buffer
|
|
55
|
+
self._buffer = []
|
|
56
|
+
await self._on_analytics(events)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from rrcp.protocol.event import Event
|
|
6
|
+
from rrcp.protocol.identity import Identity
|
|
7
|
+
from rrcp.protocol.run import Run
|
|
8
|
+
from rrcp.protocol.thread import Thread
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Broadcaster(Protocol):
|
|
12
|
+
async def broadcast_event(self, event: Event, *, namespace: str | None = None) -> None: ...
|
|
13
|
+
async def broadcast_thread_updated(self, thread: Thread, *, namespace: str | None = None) -> None: ...
|
|
14
|
+
async def broadcast_members_updated(
|
|
15
|
+
self,
|
|
16
|
+
thread_id: str,
|
|
17
|
+
members: list[Identity],
|
|
18
|
+
*,
|
|
19
|
+
namespace: str | None = None,
|
|
20
|
+
) -> None: ...
|
|
21
|
+
async def broadcast_run_updated(self, run: Run, *, namespace: str | None = None) -> None: ...
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rrcp.protocol.event import Event
|
|
4
|
+
from rrcp.protocol.identity import Identity
|
|
5
|
+
from rrcp.protocol.run import Run
|
|
6
|
+
from rrcp.protocol.thread import Thread
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RecordingBroadcaster:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.events: list[Event] = []
|
|
12
|
+
self.threads_updated: list[Thread] = []
|
|
13
|
+
self.members_updated: list[tuple[str, list[Identity]]] = []
|
|
14
|
+
self.runs_updated: list[Run] = []
|
|
15
|
+
self.events_with_namespace: list[tuple[Event, str | None]] = []
|
|
16
|
+
self.threads_updated_with_namespace: list[tuple[Thread, str | None]] = []
|
|
17
|
+
self.members_updated_with_namespace: list[tuple[str, list[Identity], str | None]] = []
|
|
18
|
+
self.runs_updated_with_namespace: list[tuple[Run, str | None]] = []
|
|
19
|
+
|
|
20
|
+
async def broadcast_event(self, event: Event, *, namespace: str | None = None) -> None:
|
|
21
|
+
self.events.append(event)
|
|
22
|
+
self.events_with_namespace.append((event, namespace))
|
|
23
|
+
|
|
24
|
+
async def broadcast_thread_updated(self, thread: Thread, *, namespace: str | None = None) -> None:
|
|
25
|
+
self.threads_updated.append(thread)
|
|
26
|
+
self.threads_updated_with_namespace.append((thread, namespace))
|
|
27
|
+
|
|
28
|
+
async def broadcast_members_updated(
|
|
29
|
+
self,
|
|
30
|
+
thread_id: str,
|
|
31
|
+
members: list[Identity],
|
|
32
|
+
*,
|
|
33
|
+
namespace: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.members_updated.append((thread_id, members))
|
|
36
|
+
self.members_updated_with_namespace.append((thread_id, members, namespace))
|
|
37
|
+
|
|
38
|
+
async def broadcast_run_updated(self, run: Run, *, namespace: str | None = None) -> None:
|
|
39
|
+
self.runs_updated.append(run)
|
|
40
|
+
self.runs_updated_with_namespace.append((run, namespace))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socketio
|
|
4
|
+
|
|
5
|
+
from rrcp.protocol.event import Event
|
|
6
|
+
from rrcp.protocol.identity import Identity
|
|
7
|
+
from rrcp.protocol.run import Run
|
|
8
|
+
from rrcp.protocol.thread import Thread
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _thread_room(thread_id: str) -> str:
|
|
12
|
+
return f"thread:{thread_id}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SocketIOBroadcaster:
|
|
16
|
+
def __init__(self, sio: socketio.AsyncServer) -> None:
|
|
17
|
+
self._sio = sio
|
|
18
|
+
|
|
19
|
+
async def broadcast_event(self, event: Event, *, namespace: str | None = None) -> None:
|
|
20
|
+
await self._sio.emit(
|
|
21
|
+
"event",
|
|
22
|
+
event.model_dump(mode="json", by_alias=True),
|
|
23
|
+
room=_thread_room(event.thread_id),
|
|
24
|
+
namespace=namespace or "/",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def broadcast_thread_updated(self, thread: Thread, *, namespace: str | None = None) -> None:
|
|
28
|
+
await self._sio.emit(
|
|
29
|
+
"thread:updated",
|
|
30
|
+
thread.model_dump(mode="json", by_alias=True),
|
|
31
|
+
room=_thread_room(thread.id),
|
|
32
|
+
namespace=namespace or "/",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def broadcast_members_updated(
|
|
36
|
+
self,
|
|
37
|
+
thread_id: str,
|
|
38
|
+
members: list[Identity],
|
|
39
|
+
*,
|
|
40
|
+
namespace: str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
await self._sio.emit(
|
|
43
|
+
"members:updated",
|
|
44
|
+
{
|
|
45
|
+
"thread_id": thread_id,
|
|
46
|
+
"members": [m.model_dump(mode="json") for m in members],
|
|
47
|
+
},
|
|
48
|
+
room=_thread_room(thread_id),
|
|
49
|
+
namespace=namespace or "/",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def broadcast_run_updated(self, run: Run, *, namespace: str | None = None) -> None:
|
|
53
|
+
await self._sio.emit(
|
|
54
|
+
"run:updated",
|
|
55
|
+
run.model_dump(mode="json", by_alias=True),
|
|
56
|
+
room=_thread_room(run.thread_id),
|
|
57
|
+
namespace=namespace or "/",
|
|
58
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from rrcp.handler.context import HandlerContext
|
|
2
|
+
from rrcp.handler.executor import RunExecutor
|
|
3
|
+
from rrcp.handler.send import HandlerSend
|
|
4
|
+
from rrcp.handler.types import HandlerCallable
|
|
5
|
+
|
|
6
|
+
__all__ = ["HandlerCallable", "HandlerContext", "HandlerSend", "RunExecutor"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from rrcp.analytics.collector import AssistantAnalytics
|
|
6
|
+
from rrcp.protocol.event import Event
|
|
7
|
+
from rrcp.protocol.identity import AssistantIdentity, Identity
|
|
8
|
+
from rrcp.protocol.run import Run
|
|
9
|
+
from rrcp.protocol.thread import Thread
|
|
10
|
+
from rrcp.store.protocol import ThreadStore
|
|
11
|
+
|
|
12
|
+
InvokeAssistantCallable = Callable[[str], Awaitable[Run]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HandlerContext:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
store: ThreadStore,
|
|
19
|
+
thread: Thread,
|
|
20
|
+
run: Run,
|
|
21
|
+
assistant: AssistantIdentity,
|
|
22
|
+
analytics: AssistantAnalytics,
|
|
23
|
+
invoke_assistant: InvokeAssistantCallable | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self._store = store
|
|
26
|
+
self._invoke_assistant = invoke_assistant
|
|
27
|
+
self.thread = thread
|
|
28
|
+
self.run = run
|
|
29
|
+
self.assistant = assistant
|
|
30
|
+
self.analytics = analytics
|
|
31
|
+
|
|
32
|
+
async def events(self, limit: int | None = None) -> list[Event]:
|
|
33
|
+
page = await self._store.list_events(self.thread.id, limit=limit or 100)
|
|
34
|
+
return page.items
|
|
35
|
+
|
|
36
|
+
async def members(self) -> list[Identity]:
|
|
37
|
+
rows = await self._store.list_members(self.thread.id)
|
|
38
|
+
return [m.identity for m in rows]
|
|
39
|
+
|
|
40
|
+
async def invoke(self, assistant_id: str) -> Run:
|
|
41
|
+
if self._invoke_assistant is None:
|
|
42
|
+
raise RuntimeError("ctx.invoke is not available: no handler_resolver configured")
|
|
43
|
+
return await self._invoke_assistant(assistant_id)
|