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.
Files changed (83) hide show
  1. rrcp-0.1.0a0/.gitignore +10 -0
  2. rrcp-0.1.0a0/CHANGELOG.md +5 -0
  3. rrcp-0.1.0a0/CLAUDE.md +142 -0
  4. rrcp-0.1.0a0/PKG-INFO +35 -0
  5. rrcp-0.1.0a0/README.md +9 -0
  6. rrcp-0.1.0a0/docker-compose.test.yml +14 -0
  7. rrcp-0.1.0a0/pyproject.toml +80 -0
  8. rrcp-0.1.0a0/src/rrcp/__init__.py +129 -0
  9. rrcp-0.1.0a0/src/rrcp/analytics/__init__.py +3 -0
  10. rrcp-0.1.0a0/src/rrcp/analytics/collector.py +56 -0
  11. rrcp-0.1.0a0/src/rrcp/broadcast/__init__.py +5 -0
  12. rrcp-0.1.0a0/src/rrcp/broadcast/protocol.py +21 -0
  13. rrcp-0.1.0a0/src/rrcp/broadcast/recording.py +40 -0
  14. rrcp-0.1.0a0/src/rrcp/broadcast/socketio.py +58 -0
  15. rrcp-0.1.0a0/src/rrcp/handler/__init__.py +6 -0
  16. rrcp-0.1.0a0/src/rrcp/handler/context.py +43 -0
  17. rrcp-0.1.0a0/src/rrcp/handler/executor.py +245 -0
  18. rrcp-0.1.0a0/src/rrcp/handler/send.py +100 -0
  19. rrcp-0.1.0a0/src/rrcp/handler/types.py +14 -0
  20. rrcp-0.1.0a0/src/rrcp/protocol/__init__.py +0 -0
  21. rrcp-0.1.0a0/src/rrcp/protocol/content.py +66 -0
  22. rrcp-0.1.0a0/src/rrcp/protocol/event.py +142 -0
  23. rrcp-0.1.0a0/src/rrcp/protocol/identity.py +36 -0
  24. rrcp-0.1.0a0/src/rrcp/protocol/run.py +32 -0
  25. rrcp-0.1.0a0/src/rrcp/protocol/tenant.py +10 -0
  26. rrcp-0.1.0a0/src/rrcp/protocol/thread.py +37 -0
  27. rrcp-0.1.0a0/src/rrcp/server/__init__.py +3 -0
  28. rrcp-0.1.0a0/src/rrcp/server/auth.py +19 -0
  29. rrcp-0.1.0a0/src/rrcp/server/namespace.py +59 -0
  30. rrcp-0.1.0a0/src/rrcp/server/rest/__init__.py +0 -0
  31. rrcp-0.1.0a0/src/rrcp/server/rest/deps.py +45 -0
  32. rrcp-0.1.0a0/src/rrcp/server/rest/invocations.py +78 -0
  33. rrcp-0.1.0a0/src/rrcp/server/rest/members.py +73 -0
  34. rrcp-0.1.0a0/src/rrcp/server/rest/messages.py +69 -0
  35. rrcp-0.1.0a0/src/rrcp/server/rest/runs.py +53 -0
  36. rrcp-0.1.0a0/src/rrcp/server/rest/threads.py +141 -0
  37. rrcp-0.1.0a0/src/rrcp/server/thread_server.py +167 -0
  38. rrcp-0.1.0a0/src/rrcp/socketio/__init__.py +3 -0
  39. rrcp-0.1.0a0/src/rrcp/socketio/server.py +411 -0
  40. rrcp-0.1.0a0/src/rrcp/store/__init__.py +4 -0
  41. rrcp-0.1.0a0/src/rrcp/store/postgres/__init__.py +0 -0
  42. rrcp-0.1.0a0/src/rrcp/store/postgres/schema.sql +55 -0
  43. rrcp-0.1.0a0/src/rrcp/store/postgres/store.py +378 -0
  44. rrcp-0.1.0a0/src/rrcp/store/protocol.py +56 -0
  45. rrcp-0.1.0a0/src/rrcp/store/types.py +29 -0
  46. rrcp-0.1.0a0/tests/__init__.py +0 -0
  47. rrcp-0.1.0a0/tests/broadcast/__init__.py +0 -0
  48. rrcp-0.1.0a0/tests/broadcast/test_recording.py +145 -0
  49. rrcp-0.1.0a0/tests/conftest.py +37 -0
  50. rrcp-0.1.0a0/tests/handler/__init__.py +0 -0
  51. rrcp-0.1.0a0/tests/handler/test_analytics.py +33 -0
  52. rrcp-0.1.0a0/tests/handler/test_ctx_invoke.py +81 -0
  53. rrcp-0.1.0a0/tests/handler/test_executor.py +157 -0
  54. rrcp-0.1.0a0/tests/handler/test_send.py +39 -0
  55. rrcp-0.1.0a0/tests/protocol/__init__.py +0 -0
  56. rrcp-0.1.0a0/tests/protocol/test_content.py +63 -0
  57. rrcp-0.1.0a0/tests/protocol/test_event.py +124 -0
  58. rrcp-0.1.0a0/tests/protocol/test_identity.py +44 -0
  59. rrcp-0.1.0a0/tests/protocol/test_run.py +24 -0
  60. rrcp-0.1.0a0/tests/protocol/test_tenant.py +22 -0
  61. rrcp-0.1.0a0/tests/protocol/test_thread.py +35 -0
  62. rrcp-0.1.0a0/tests/server/__init__.py +0 -0
  63. rrcp-0.1.0a0/tests/server/test_acp_namespace.py +139 -0
  64. rrcp-0.1.0a0/tests/server/test_authorize.py +80 -0
  65. rrcp-0.1.0a0/tests/server/test_e2e_chat.py +80 -0
  66. rrcp-0.1.0a0/tests/server/test_e2e_chat_with_assistant.py +86 -0
  67. rrcp-0.1.0a0/tests/server/test_namespace.py +112 -0
  68. rrcp-0.1.0a0/tests/server/test_rest_invoke.py +163 -0
  69. rrcp-0.1.0a0/tests/server/test_rest_members.py +79 -0
  70. rrcp-0.1.0a0/tests/server/test_rest_messages.py +99 -0
  71. rrcp-0.1.0a0/tests/server/test_rest_threads.py +163 -0
  72. rrcp-0.1.0a0/tests/socketio/__init__.py +0 -0
  73. rrcp-0.1.0a0/tests/socketio/test_live_integration.py +252 -0
  74. rrcp-0.1.0a0/tests/socketio/test_namespace_integration.py +337 -0
  75. rrcp-0.1.0a0/tests/socketio/test_thread_namespace_dispatch.py +197 -0
  76. rrcp-0.1.0a0/tests/store/__init__.py +0 -0
  77. rrcp-0.1.0a0/tests/store/test_postgres_events.py +70 -0
  78. rrcp-0.1.0a0/tests/store/test_postgres_list.py +50 -0
  79. rrcp-0.1.0a0/tests/store/test_postgres_members.py +54 -0
  80. rrcp-0.1.0a0/tests/store/test_postgres_runs.py +101 -0
  81. rrcp-0.1.0a0/tests/store/test_postgres_threads.py +59 -0
  82. rrcp-0.1.0a0/tests/test_setup.py +7 -0
  83. rrcp-0.1.0a0/uv.lock +1514 -0
@@ -0,0 +1,10 @@
1
+ .venv
2
+ venv
3
+ __pycache__
4
+ .claude
5
+ .ruff_cache
6
+ .pytest_cache
7
+ .mypy_cache
8
+ dist
9
+ *.egg-info
10
+ /docs/plans
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a0
4
+
5
+ First version.
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,9 @@
1
+ # rrcp
2
+
3
+ rrcp - agent communication protocol
4
+
5
+ ## License
6
+
7
+ ## License
8
+
9
+ MIT — see [`LICENSE`](../../LICENSE).
@@ -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,3 @@
1
+ from rrcp.analytics.collector import AnalyticsEvent, AssistantAnalytics, OnAnalyticsCallback
2
+
3
+ __all__ = ["AnalyticsEvent", "AssistantAnalytics", "OnAnalyticsCallback"]
@@ -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,5 @@
1
+ from rrcp.broadcast.protocol import Broadcaster
2
+ from rrcp.broadcast.recording import RecordingBroadcaster
3
+ from rrcp.broadcast.socketio import SocketIOBroadcaster
4
+
5
+ __all__ = ["Broadcaster", "RecordingBroadcaster", "SocketIOBroadcaster"]
@@ -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)