roomkit 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.
- roomkit-0.1.0/.gitignore +31 -0
- roomkit-0.1.0/AGENTS.md +362 -0
- roomkit-0.1.0/LICENSE +21 -0
- roomkit-0.1.0/PKG-INFO +567 -0
- roomkit-0.1.0/README.md +490 -0
- roomkit-0.1.0/docs/ai-integration.md +102 -0
- roomkit-0.1.0/docs/api/channel-models.md +11 -0
- roomkit-0.1.0/docs/api/channel.md +3 -0
- roomkit-0.1.0/docs/api/channels.md +15 -0
- roomkit-0.1.0/docs/api/delivery.md +9 -0
- roomkit-0.1.0/docs/api/enums.md +29 -0
- roomkit-0.1.0/docs/api/events.md +31 -0
- roomkit-0.1.0/docs/api/hook-models.md +9 -0
- roomkit-0.1.0/docs/api/hooks.md +5 -0
- roomkit-0.1.0/docs/api/identity.md +99 -0
- roomkit-0.1.0/docs/api/index.md +36 -0
- roomkit-0.1.0/docs/api/providers-ai.md +103 -0
- roomkit-0.1.0/docs/api/providers-email.md +5 -0
- roomkit-0.1.0/docs/api/providers-http.md +7 -0
- roomkit-0.1.0/docs/api/providers-messenger.md +5 -0
- roomkit-0.1.0/docs/api/providers-sms.md +245 -0
- roomkit-0.1.0/docs/api/providers-whatsapp.md +5 -0
- roomkit-0.1.0/docs/api/realtime.md +228 -0
- roomkit-0.1.0/docs/api/room.md +5 -0
- roomkit-0.1.0/docs/api/roomkit.md +56 -0
- roomkit-0.1.0/docs/api/routing.md +5 -0
- roomkit-0.1.0/docs/api/sources.md +755 -0
- roomkit-0.1.0/docs/api/store.md +5 -0
- roomkit-0.1.0/docs/architecture.md +446 -0
- roomkit-0.1.0/docs/cpaas-comparison.md +301 -0
- roomkit-0.1.0/docs/features.md +1217 -0
- roomkit-0.1.0/docs/index.md +50 -0
- roomkit-0.1.0/docs/llms.txt +49 -0
- roomkit-0.1.0/docs/mcp.md +233 -0
- roomkit-0.1.0/docs/roomkit-rfc.md +3861 -0
- roomkit-0.1.0/docs/technical.md +864 -0
- roomkit-0.1.0/llms.txt +52 -0
- roomkit-0.1.0/pyproject.toml +131 -0
- roomkit-0.1.0/src/roomkit/__init__.py +372 -0
- roomkit-0.1.0/src/roomkit/_version.py +1 -0
- roomkit-0.1.0/src/roomkit/ai_docs.py +93 -0
- roomkit-0.1.0/src/roomkit/channels/__init__.py +194 -0
- roomkit-0.1.0/src/roomkit/channels/ai.py +238 -0
- roomkit-0.1.0/src/roomkit/channels/base.py +66 -0
- roomkit-0.1.0/src/roomkit/channels/transport.py +115 -0
- roomkit-0.1.0/src/roomkit/channels/websocket.py +85 -0
- roomkit-0.1.0/src/roomkit/core/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/core/_channel_ops.py +252 -0
- roomkit-0.1.0/src/roomkit/core/_helpers.py +296 -0
- roomkit-0.1.0/src/roomkit/core/_inbound.py +435 -0
- roomkit-0.1.0/src/roomkit/core/_room_lifecycle.py +275 -0
- roomkit-0.1.0/src/roomkit/core/circuit_breaker.py +84 -0
- roomkit-0.1.0/src/roomkit/core/event_router.py +401 -0
- roomkit-0.1.0/src/roomkit/core/framework.py +793 -0
- roomkit-0.1.0/src/roomkit/core/hooks.py +232 -0
- roomkit-0.1.0/src/roomkit/core/inbound_router.py +57 -0
- roomkit-0.1.0/src/roomkit/core/locks.py +66 -0
- roomkit-0.1.0/src/roomkit/core/rate_limiter.py +67 -0
- roomkit-0.1.0/src/roomkit/core/retry.py +49 -0
- roomkit-0.1.0/src/roomkit/core/router.py +24 -0
- roomkit-0.1.0/src/roomkit/core/transcoder.py +85 -0
- roomkit-0.1.0/src/roomkit/identity/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/identity/base.py +27 -0
- roomkit-0.1.0/src/roomkit/identity/mock.py +49 -0
- roomkit-0.1.0/src/roomkit/models/__init__.py +104 -0
- roomkit-0.1.0/src/roomkit/models/channel.py +99 -0
- roomkit-0.1.0/src/roomkit/models/context.py +35 -0
- roomkit-0.1.0/src/roomkit/models/delivery.py +76 -0
- roomkit-0.1.0/src/roomkit/models/enums.py +170 -0
- roomkit-0.1.0/src/roomkit/models/event.py +203 -0
- roomkit-0.1.0/src/roomkit/models/framework_event.py +19 -0
- roomkit-0.1.0/src/roomkit/models/hook.py +68 -0
- roomkit-0.1.0/src/roomkit/models/identity.py +81 -0
- roomkit-0.1.0/src/roomkit/models/participant.py +34 -0
- roomkit-0.1.0/src/roomkit/models/room.py +33 -0
- roomkit-0.1.0/src/roomkit/models/task.py +36 -0
- roomkit-0.1.0/src/roomkit/providers/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/providers/ai/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/providers/ai/base.py +140 -0
- roomkit-0.1.0/src/roomkit/providers/ai/mock.py +33 -0
- roomkit-0.1.0/src/roomkit/providers/anthropic/__init__.py +6 -0
- roomkit-0.1.0/src/roomkit/providers/anthropic/ai.py +145 -0
- roomkit-0.1.0/src/roomkit/providers/anthropic/config.py +14 -0
- roomkit-0.1.0/src/roomkit/providers/elasticemail/__init__.py +6 -0
- roomkit-0.1.0/src/roomkit/providers/elasticemail/config.py +16 -0
- roomkit-0.1.0/src/roomkit/providers/elasticemail/email.py +97 -0
- roomkit-0.1.0/src/roomkit/providers/email/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/providers/email/base.py +46 -0
- roomkit-0.1.0/src/roomkit/providers/email/mock.py +34 -0
- roomkit-0.1.0/src/roomkit/providers/gemini/__init__.py +6 -0
- roomkit-0.1.0/src/roomkit/providers/gemini/ai.py +153 -0
- roomkit-0.1.0/src/roomkit/providers/gemini/config.py +14 -0
- roomkit-0.1.0/src/roomkit/providers/http/__init__.py +15 -0
- roomkit-0.1.0/src/roomkit/providers/http/base.py +33 -0
- roomkit-0.1.0/src/roomkit/providers/http/config.py +14 -0
- roomkit-0.1.0/src/roomkit/providers/http/mock.py +21 -0
- roomkit-0.1.0/src/roomkit/providers/http/provider.py +105 -0
- roomkit-0.1.0/src/roomkit/providers/http/webhook.py +33 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/__init__.py +15 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/base.py +33 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/config.py +17 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/facebook.py +95 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/mock.py +21 -0
- roomkit-0.1.0/src/roomkit/providers/messenger/webhook.py +42 -0
- roomkit-0.1.0/src/roomkit/providers/openai/__init__.py +6 -0
- roomkit-0.1.0/src/roomkit/providers/openai/ai.py +155 -0
- roomkit-0.1.0/src/roomkit/providers/openai/config.py +24 -0
- roomkit-0.1.0/src/roomkit/providers/pydantic_ai/__init__.py +5 -0
- roomkit-0.1.0/src/roomkit/providers/pydantic_ai/config.py +14 -0
- roomkit-0.1.0/src/roomkit/providers/rcs/__init__.py +9 -0
- roomkit-0.1.0/src/roomkit/providers/rcs/base.py +95 -0
- roomkit-0.1.0/src/roomkit/providers/rcs/mock.py +78 -0
- roomkit-0.1.0/src/roomkit/providers/sendgrid/__init__.py +5 -0
- roomkit-0.1.0/src/roomkit/providers/sendgrid/config.py +13 -0
- roomkit-0.1.0/src/roomkit/providers/sinch/__init__.py +6 -0
- roomkit-0.1.0/src/roomkit/providers/sinch/config.py +22 -0
- roomkit-0.1.0/src/roomkit/providers/sinch/sms.py +192 -0
- roomkit-0.1.0/src/roomkit/providers/sms/__init__.py +15 -0
- roomkit-0.1.0/src/roomkit/providers/sms/base.py +67 -0
- roomkit-0.1.0/src/roomkit/providers/sms/meta.py +401 -0
- roomkit-0.1.0/src/roomkit/providers/sms/mock.py +24 -0
- roomkit-0.1.0/src/roomkit/providers/sms/phone.py +77 -0
- roomkit-0.1.0/src/roomkit/providers/telnyx/__init__.py +21 -0
- roomkit-0.1.0/src/roomkit/providers/telnyx/config.py +14 -0
- roomkit-0.1.0/src/roomkit/providers/telnyx/rcs.py +352 -0
- roomkit-0.1.0/src/roomkit/providers/telnyx/sms.py +231 -0
- roomkit-0.1.0/src/roomkit/providers/twilio/__init__.py +18 -0
- roomkit-0.1.0/src/roomkit/providers/twilio/config.py +19 -0
- roomkit-0.1.0/src/roomkit/providers/twilio/rcs.py +183 -0
- roomkit-0.1.0/src/roomkit/providers/twilio/sms.py +200 -0
- roomkit-0.1.0/src/roomkit/providers/voicemeup/__init__.py +15 -0
- roomkit-0.1.0/src/roomkit/providers/voicemeup/config.py +21 -0
- roomkit-0.1.0/src/roomkit/providers/voicemeup/sms.py +374 -0
- roomkit-0.1.0/src/roomkit/providers/whatsapp/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/providers/whatsapp/base.py +44 -0
- roomkit-0.1.0/src/roomkit/providers/whatsapp/mock.py +21 -0
- roomkit-0.1.0/src/roomkit/py.typed +0 -0
- roomkit-0.1.0/src/roomkit/realtime/__init__.py +17 -0
- roomkit-0.1.0/src/roomkit/realtime/base.py +111 -0
- roomkit-0.1.0/src/roomkit/realtime/memory.py +158 -0
- roomkit-0.1.0/src/roomkit/sources/__init__.py +35 -0
- roomkit-0.1.0/src/roomkit/sources/base.py +207 -0
- roomkit-0.1.0/src/roomkit/sources/websocket.py +260 -0
- roomkit-0.1.0/src/roomkit/store/__init__.py +0 -0
- roomkit-0.1.0/src/roomkit/store/base.py +230 -0
- roomkit-0.1.0/src/roomkit/store/memory.py +293 -0
- roomkit-0.1.0/tests/__init__.py +0 -0
- roomkit-0.1.0/tests/conftest.py +97 -0
- roomkit-0.1.0/tests/test_ai_docs.py +31 -0
- roomkit-0.1.0/tests/test_channel_abc.py +202 -0
- roomkit-0.1.0/tests/test_channels/__init__.py +0 -0
- roomkit-0.1.0/tests/test_channels/test_ai.py +389 -0
- roomkit-0.1.0/tests/test_channels/test_email.py +46 -0
- roomkit-0.1.0/tests/test_channels/test_http.py +48 -0
- roomkit-0.1.0/tests/test_channels/test_messenger.py +49 -0
- roomkit-0.1.0/tests/test_channels/test_sms.py +47 -0
- roomkit-0.1.0/tests/test_channels/test_websocket.py +54 -0
- roomkit-0.1.0/tests/test_channels/test_whatsapp.py +46 -0
- roomkit-0.1.0/tests/test_circuit_breaker.py +97 -0
- roomkit-0.1.0/tests/test_configs.py +46 -0
- roomkit-0.1.0/tests/test_enums.py +161 -0
- roomkit-0.1.0/tests/test_framework.py +755 -0
- roomkit-0.1.0/tests/test_framework_events.py +335 -0
- roomkit-0.1.0/tests/test_framework_queries.py +314 -0
- roomkit-0.1.0/tests/test_hook_side_effects.py +248 -0
- roomkit-0.1.0/tests/test_hooks.py +596 -0
- roomkit-0.1.0/tests/test_identity_hooks.py +185 -0
- roomkit-0.1.0/tests/test_identity_pipeline.py +856 -0
- roomkit-0.1.0/tests/test_inbound_routing.py +165 -0
- roomkit-0.1.0/tests/test_integration/__init__.py +0 -0
- roomkit-0.1.0/tests/test_integration/test_ai_assistant.py +116 -0
- roomkit-0.1.0/tests/test_integration/test_ai_chain_depth.py +112 -0
- roomkit-0.1.0/tests/test_integration/test_cross_channel.py +93 -0
- roomkit-0.1.0/tests/test_integration/test_dynamic_channels.py +128 -0
- roomkit-0.1.0/tests/test_integration/test_human_ai.py +94 -0
- roomkit-0.1.0/tests/test_integration/test_identity_resolution.py +95 -0
- roomkit-0.1.0/tests/test_integration/test_observer.py +131 -0
- roomkit-0.1.0/tests/test_integration/test_sin_scanning.py +156 -0
- roomkit-0.1.0/tests/test_lifecycle_hooks.py +143 -0
- roomkit-0.1.0/tests/test_locks.py +53 -0
- roomkit-0.1.0/tests/test_mock_providers.py +95 -0
- roomkit-0.1.0/tests/test_models.py +661 -0
- roomkit-0.1.0/tests/test_observability.py +139 -0
- roomkit-0.1.0/tests/test_providers/__init__.py +0 -0
- roomkit-0.1.0/tests/test_providers/test_anthropic.py +255 -0
- roomkit-0.1.0/tests/test_providers/test_elasticemail.py +175 -0
- roomkit-0.1.0/tests/test_providers/test_gemini.py +238 -0
- roomkit-0.1.0/tests/test_providers/test_http.py +192 -0
- roomkit-0.1.0/tests/test_providers/test_messenger.py +207 -0
- roomkit-0.1.0/tests/test_providers/test_openai.py +268 -0
- roomkit-0.1.0/tests/test_providers/test_phone.py +65 -0
- roomkit-0.1.0/tests/test_providers/test_rcs.py +130 -0
- roomkit-0.1.0/tests/test_providers/test_sinch.py +342 -0
- roomkit-0.1.0/tests/test_providers/test_sms_utils.py +581 -0
- roomkit-0.1.0/tests/test_providers/test_telnyx.py +481 -0
- roomkit-0.1.0/tests/test_providers/test_telnyx_rcs.py +322 -0
- roomkit-0.1.0/tests/test_providers/test_twilio.py +363 -0
- roomkit-0.1.0/tests/test_providers/test_voicemeup.py +488 -0
- roomkit-0.1.0/tests/test_public_api.py +40 -0
- roomkit-0.1.0/tests/test_realtime.py +405 -0
- roomkit-0.1.0/tests/test_resilience.py +666 -0
- roomkit-0.1.0/tests/test_router.py +611 -0
- roomkit-0.1.0/tests/test_sources.py +1038 -0
- roomkit-0.1.0/tests/test_sources_websocket.py +490 -0
- roomkit-0.1.0/tests/test_store_memory.py +412 -0
- roomkit-0.1.0/tests/test_websocket_lifecycle.py +122 -0
roomkit-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
*.whl
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
.env
|
|
15
|
+
*.env
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
htmlcov/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
coverage.xml
|
|
23
|
+
*.log
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
*~
|
|
31
|
+
site
|
roomkit-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# RoomKit
|
|
2
|
+
|
|
3
|
+
> Pure async Python library for multi-channel conversations with rooms, hooks, and pluggable backends.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
uv sync
|
|
10
|
+
|
|
11
|
+
# Run all tests
|
|
12
|
+
pytest tests/ -q
|
|
13
|
+
|
|
14
|
+
# Run specific test file
|
|
15
|
+
pytest tests/test_framework.py -v
|
|
16
|
+
|
|
17
|
+
# Lint check
|
|
18
|
+
ruff check src/roomkit/
|
|
19
|
+
|
|
20
|
+
# Lint fix
|
|
21
|
+
ruff check src/roomkit/ --fix
|
|
22
|
+
|
|
23
|
+
# Type check (optional, not enforced in CI)
|
|
24
|
+
mypy src/roomkit/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/roomkit/
|
|
31
|
+
├── __init__.py # Public API exports - ALL public classes exported here
|
|
32
|
+
├── _version.py # Version string (auto-managed)
|
|
33
|
+
├── core/
|
|
34
|
+
│ ├── framework.py # RoomKit class - central orchestrator
|
|
35
|
+
│ ├── _inbound.py # Inbound message processing pipeline
|
|
36
|
+
│ ├── _room_lifecycle.py # Room CRUD operations
|
|
37
|
+
│ ├── _channel_ops.py # Channel attach/detach/mute operations
|
|
38
|
+
│ ├── _helpers.py # Shared helper methods
|
|
39
|
+
│ ├── hooks.py # HookEngine, HookRegistration
|
|
40
|
+
│ ├── locks.py # RoomLockManager ABC, InMemoryLockManager
|
|
41
|
+
│ ├── event_router.py # Broadcast routing to channels
|
|
42
|
+
│ └── inbound_router.py # InboundRoomRouter ABC
|
|
43
|
+
├── channels/
|
|
44
|
+
│ ├── base.py # Channel ABC
|
|
45
|
+
│ ├── transport.py # TransportChannel (SMS, Email, etc.)
|
|
46
|
+
│ ├── ai.py # AIChannel (intelligence layer)
|
|
47
|
+
│ └── websocket.py # WebSocketChannel
|
|
48
|
+
├── providers/
|
|
49
|
+
│ ├── ai/base.py # AIProvider ABC, AIContext, AIResponse
|
|
50
|
+
│ ├── sms/base.py # SMSProvider ABC
|
|
51
|
+
│ ├── twilio/sms.py # TwilioSMSProvider
|
|
52
|
+
│ ├── telnyx/sms.py # TelnyxSMSProvider
|
|
53
|
+
│ └── ... # Other provider implementations
|
|
54
|
+
├── models/
|
|
55
|
+
│ ├── room.py # Room, RoomTimers
|
|
56
|
+
│ ├── event.py # RoomEvent, content types
|
|
57
|
+
│ ├── participant.py # Participant
|
|
58
|
+
│ ├── identity.py # Identity, IdentityResult, IdentityHookResult
|
|
59
|
+
│ ├── delivery.py # InboundMessage, InboundResult, DeliveryResult
|
|
60
|
+
│ ├── channel.py # ChannelBinding, ChannelCapabilities
|
|
61
|
+
│ ├── hook.py # HookResult, InjectedEvent
|
|
62
|
+
│ └── enums.py # All enumerations
|
|
63
|
+
├── store/
|
|
64
|
+
│ ├── base.py # ConversationStore ABC
|
|
65
|
+
│ └── memory.py # InMemoryStore
|
|
66
|
+
├── realtime/
|
|
67
|
+
│ ├── base.py # RealtimeBackend ABC, EphemeralEvent
|
|
68
|
+
│ └── memory.py # InMemoryRealtime
|
|
69
|
+
└── identity/
|
|
70
|
+
├── base.py # IdentityResolver ABC
|
|
71
|
+
└── mock.py # MockIdentityResolver
|
|
72
|
+
|
|
73
|
+
tests/
|
|
74
|
+
├── conftest.py # Shared fixtures
|
|
75
|
+
├── test_framework.py # Core RoomKit tests, SimpleChannel fixture
|
|
76
|
+
├── test_hooks.py # Hook system tests
|
|
77
|
+
├── test_identity_pipeline.py # Identity resolution tests
|
|
78
|
+
└── ...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Architecture Patterns
|
|
82
|
+
|
|
83
|
+
### ABC + Default Implementation
|
|
84
|
+
|
|
85
|
+
RoomKit uses abstract base classes with in-memory defaults:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# ABC in base.py
|
|
89
|
+
class ConversationStore(ABC):
|
|
90
|
+
@abstractmethod
|
|
91
|
+
async def create_room(self, room: Room) -> Room: ...
|
|
92
|
+
|
|
93
|
+
# Default in memory.py
|
|
94
|
+
class InMemoryStore(ConversationStore):
|
|
95
|
+
async def create_room(self, room: Room) -> Room:
|
|
96
|
+
self._rooms[room.id] = room
|
|
97
|
+
return room
|
|
98
|
+
|
|
99
|
+
# Usage - default is automatic
|
|
100
|
+
kit = RoomKit() # Uses InMemoryStore
|
|
101
|
+
kit = RoomKit(store=PostgresStore(...)) # Custom backend
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This pattern applies to: `ConversationStore`, `RoomLockManager`, `RealtimeBackend`, `IdentityResolver`, `InboundRoomRouter`.
|
|
105
|
+
|
|
106
|
+
### Channel Implementation
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from roomkit.channels.transport import TransportChannel
|
|
110
|
+
from roomkit.models.enums import ChannelType
|
|
111
|
+
|
|
112
|
+
class SMSChannel(TransportChannel):
|
|
113
|
+
channel_type = ChannelType.SMS
|
|
114
|
+
|
|
115
|
+
async def deliver(
|
|
116
|
+
self, event: RoomEvent, binding: ChannelBinding, context: RoomContext
|
|
117
|
+
) -> ChannelOutput:
|
|
118
|
+
phone = binding.metadata.get("phone_number")
|
|
119
|
+
text = self.extract_text(event)
|
|
120
|
+
result = await self._provider.send(to=phone, body=text)
|
|
121
|
+
return ChannelOutput(provider_result=result)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Provider Implementation
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from roomkit.providers.sms.base import SMSProvider
|
|
128
|
+
|
|
129
|
+
class TwilioSMSProvider(SMSProvider):
|
|
130
|
+
def __init__(self, config: TwilioConfig) -> None:
|
|
131
|
+
self._config = config
|
|
132
|
+
self._client = httpx.AsyncClient(...)
|
|
133
|
+
|
|
134
|
+
async def send(self, to: str, body: str, media_urls: list[str] | None = None) -> ProviderResult:
|
|
135
|
+
response = await self._client.post(...)
|
|
136
|
+
return ProviderResult(message_id=response["sid"], raw=response)
|
|
137
|
+
|
|
138
|
+
async def close(self) -> None:
|
|
139
|
+
await self._client.aclose()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Hook Implementation
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from roomkit import RoomKit, HookTrigger, HookExecution, HookResult
|
|
146
|
+
|
|
147
|
+
kit = RoomKit()
|
|
148
|
+
|
|
149
|
+
# Sync hook - can block/modify events
|
|
150
|
+
@kit.hook(HookTrigger.BEFORE_BROADCAST)
|
|
151
|
+
async def content_filter(event: RoomEvent, ctx: RoomContext) -> HookResult:
|
|
152
|
+
if "spam" in event.content.body.lower():
|
|
153
|
+
return HookResult.block("Spam detected")
|
|
154
|
+
return HookResult.allow()
|
|
155
|
+
|
|
156
|
+
# Async hook - fire-and-forget side effects
|
|
157
|
+
@kit.hook(HookTrigger.AFTER_BROADCAST, execution=HookExecution.ASYNC)
|
|
158
|
+
async def log_event(event: RoomEvent, ctx: RoomContext) -> None:
|
|
159
|
+
await analytics.track("message", {"room": event.room_id})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Code Style
|
|
163
|
+
|
|
164
|
+
### Required in All Files
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from __future__ import annotations # Always first import
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Models Use Pydantic
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from pydantic import BaseModel, Field
|
|
174
|
+
|
|
175
|
+
class Room(BaseModel):
|
|
176
|
+
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
177
|
+
status: RoomStatus = RoomStatus.ACTIVE
|
|
178
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Async-First
|
|
182
|
+
|
|
183
|
+
All I/O operations must be async:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Correct
|
|
187
|
+
async def send(self, to: str, body: str) -> ProviderResult:
|
|
188
|
+
response = await self._client.post(...)
|
|
189
|
+
return ProviderResult(...)
|
|
190
|
+
|
|
191
|
+
# Wrong - blocks event loop
|
|
192
|
+
def send(self, to: str, body: str) -> ProviderResult:
|
|
193
|
+
response = requests.post(...) # Never use sync HTTP
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Type Hints Required
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# All public methods must have type hints
|
|
200
|
+
async def process_inbound(self, message: InboundMessage) -> InboundResult:
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
# Use | for unions (Python 3.10+)
|
|
204
|
+
def get_room(self, room_id: str) -> Room | None:
|
|
205
|
+
...
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Imports
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# Standard library
|
|
212
|
+
from __future__ import annotations
|
|
213
|
+
import asyncio
|
|
214
|
+
from typing import TYPE_CHECKING, Any
|
|
215
|
+
|
|
216
|
+
# Third party (if needed)
|
|
217
|
+
import httpx
|
|
218
|
+
|
|
219
|
+
# Local imports - absolute from roomkit
|
|
220
|
+
from roomkit.models.room import Room
|
|
221
|
+
from roomkit.models.enums import RoomStatus
|
|
222
|
+
|
|
223
|
+
# TYPE_CHECKING imports for circular dependencies
|
|
224
|
+
if TYPE_CHECKING:
|
|
225
|
+
from roomkit.core.framework import RoomKit
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Testing Patterns
|
|
229
|
+
|
|
230
|
+
### Test Class Structure
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
class TestFeatureName:
|
|
234
|
+
async def test_specific_behavior(self) -> None:
|
|
235
|
+
"""Docstring describing what is tested."""
|
|
236
|
+
kit = RoomKit()
|
|
237
|
+
# ... test code
|
|
238
|
+
assert result.expected == actual
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Use SimpleChannel for Tests
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from tests.test_framework import SimpleChannel
|
|
245
|
+
|
|
246
|
+
async def test_something() -> None:
|
|
247
|
+
kit = RoomKit()
|
|
248
|
+
ch = SimpleChannel("test-ch")
|
|
249
|
+
kit.register_channel(ch)
|
|
250
|
+
await kit.create_room(room_id="r1")
|
|
251
|
+
await kit.attach_channel("r1", "test-ch")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Async Test Methods
|
|
255
|
+
|
|
256
|
+
All test methods that use async code must be `async def`:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
# Correct
|
|
260
|
+
async def test_create_room(self) -> None:
|
|
261
|
+
kit = RoomKit()
|
|
262
|
+
room = await kit.create_room()
|
|
263
|
+
assert room.status == RoomStatus.ACTIVE
|
|
264
|
+
|
|
265
|
+
# Wrong - won't work
|
|
266
|
+
def test_create_room(self) -> None:
|
|
267
|
+
kit = RoomKit()
|
|
268
|
+
room = await kit.create_room() # SyntaxError
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Common Tasks
|
|
272
|
+
|
|
273
|
+
### Adding a New Provider
|
|
274
|
+
|
|
275
|
+
1. Create config in `providers/<name>/config.py`
|
|
276
|
+
2. Create provider in `providers/<name>/<type>.py`
|
|
277
|
+
3. Create webhook parser if needed: `parse_<name>_webhook()`
|
|
278
|
+
4. Export from `__init__.py`
|
|
279
|
+
5. Add to main `roomkit/__init__.py` exports
|
|
280
|
+
6. Add tests in `tests/test_<name>_provider.py`
|
|
281
|
+
|
|
282
|
+
### Adding a New Channel Type
|
|
283
|
+
|
|
284
|
+
1. Add enum value to `ChannelType` in `models/enums.py`
|
|
285
|
+
2. Create channel class extending `TransportChannel` or `Channel`
|
|
286
|
+
3. Export from `channels/__init__.py`
|
|
287
|
+
4. Add to main `roomkit/__init__.py` exports
|
|
288
|
+
|
|
289
|
+
### Adding a New Hook Trigger
|
|
290
|
+
|
|
291
|
+
1. Add enum value to `HookTrigger` in `models/enums.py`
|
|
292
|
+
2. Add firing logic in appropriate mixin (`_inbound.py`, `_room_lifecycle.py`, etc.)
|
|
293
|
+
3. Add tests
|
|
294
|
+
|
|
295
|
+
## Boundaries
|
|
296
|
+
|
|
297
|
+
### Always Do
|
|
298
|
+
|
|
299
|
+
- Run `ruff check src/roomkit/` before committing
|
|
300
|
+
- Add tests for new features
|
|
301
|
+
- Export new public classes from `roomkit/__init__.py`
|
|
302
|
+
- Follow existing ABC patterns for new pluggable backends
|
|
303
|
+
- Use `model_copy(update={...})` for Pydantic model updates (immutable pattern)
|
|
304
|
+
|
|
305
|
+
### Ask First
|
|
306
|
+
|
|
307
|
+
- Adding new dependencies to `pyproject.toml`
|
|
308
|
+
- Changing public API signatures
|
|
309
|
+
- Modifying hook trigger behavior
|
|
310
|
+
- Changes to the inbound processing pipeline
|
|
311
|
+
|
|
312
|
+
### Never Do
|
|
313
|
+
|
|
314
|
+
- Modify `_version.py` manually
|
|
315
|
+
- Use synchronous I/O (requests, open()) in async methods
|
|
316
|
+
- Add `print()` statements - use `logging.getLogger("roomkit.xxx")`
|
|
317
|
+
- Break backward compatibility of public API
|
|
318
|
+
- Commit without running tests
|
|
319
|
+
- Add secrets or credentials to code
|
|
320
|
+
|
|
321
|
+
## Key Concepts
|
|
322
|
+
|
|
323
|
+
### Room Lifecycle
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
ACTIVE → PAUSED → CLOSED → ARCHIVED
|
|
327
|
+
↑ ↓
|
|
328
|
+
└──────────┘ (can close from paused)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Event Flow
|
|
332
|
+
|
|
333
|
+
```
|
|
334
|
+
Inbound Message
|
|
335
|
+
→ InboundRoomRouter.route()
|
|
336
|
+
→ Channel.handle_inbound()
|
|
337
|
+
→ IdentityResolver.resolve()
|
|
338
|
+
→ Identity hooks (ON_IDENTITY_AMBIGUOUS/UNKNOWN)
|
|
339
|
+
→ Room lock acquired
|
|
340
|
+
→ Idempotency check
|
|
341
|
+
→ BEFORE_BROADCAST hooks (sync, can block/modify)
|
|
342
|
+
→ Store event
|
|
343
|
+
→ EventRouter.broadcast() to all channels
|
|
344
|
+
→ AFTER_BROADCAST hooks (async, fire-and-forget)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Identity Resolution
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# IdentityResult statuses and their hooks:
|
|
351
|
+
IDENTIFIED → No hook, participant_id stamped on event
|
|
352
|
+
AMBIGUOUS → ON_IDENTITY_AMBIGUOUS hook
|
|
353
|
+
PENDING → ON_IDENTITY_AMBIGUOUS hook
|
|
354
|
+
UNKNOWN → ON_IDENTITY_UNKNOWN hook
|
|
355
|
+
REJECTED → ON_IDENTITY_UNKNOWN hook
|
|
356
|
+
|
|
357
|
+
# Hook can return:
|
|
358
|
+
IdentityHookResult.resolved(identity) # Identity found
|
|
359
|
+
IdentityHookResult.pending(candidates) # Wait for manual resolution
|
|
360
|
+
IdentityHookResult.challenge(inject) # Ask sender to identify
|
|
361
|
+
IdentityHookResult.reject(reason) # Block the message
|
|
362
|
+
```
|
roomkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sylvain Boily
|
|
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.
|