roomkit 0.1.0__py3-none-any.whl

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 (114) hide show
  1. roomkit/AGENTS.md +362 -0
  2. roomkit/__init__.py +372 -0
  3. roomkit/_version.py +1 -0
  4. roomkit/ai_docs.py +93 -0
  5. roomkit/channels/__init__.py +194 -0
  6. roomkit/channels/ai.py +238 -0
  7. roomkit/channels/base.py +66 -0
  8. roomkit/channels/transport.py +115 -0
  9. roomkit/channels/websocket.py +85 -0
  10. roomkit/core/__init__.py +0 -0
  11. roomkit/core/_channel_ops.py +252 -0
  12. roomkit/core/_helpers.py +296 -0
  13. roomkit/core/_inbound.py +435 -0
  14. roomkit/core/_room_lifecycle.py +275 -0
  15. roomkit/core/circuit_breaker.py +84 -0
  16. roomkit/core/event_router.py +401 -0
  17. roomkit/core/framework.py +793 -0
  18. roomkit/core/hooks.py +232 -0
  19. roomkit/core/inbound_router.py +57 -0
  20. roomkit/core/locks.py +66 -0
  21. roomkit/core/rate_limiter.py +67 -0
  22. roomkit/core/retry.py +49 -0
  23. roomkit/core/router.py +24 -0
  24. roomkit/core/transcoder.py +85 -0
  25. roomkit/identity/__init__.py +0 -0
  26. roomkit/identity/base.py +27 -0
  27. roomkit/identity/mock.py +49 -0
  28. roomkit/llms.txt +52 -0
  29. roomkit/models/__init__.py +104 -0
  30. roomkit/models/channel.py +99 -0
  31. roomkit/models/context.py +35 -0
  32. roomkit/models/delivery.py +76 -0
  33. roomkit/models/enums.py +170 -0
  34. roomkit/models/event.py +203 -0
  35. roomkit/models/framework_event.py +19 -0
  36. roomkit/models/hook.py +68 -0
  37. roomkit/models/identity.py +81 -0
  38. roomkit/models/participant.py +34 -0
  39. roomkit/models/room.py +33 -0
  40. roomkit/models/task.py +36 -0
  41. roomkit/providers/__init__.py +0 -0
  42. roomkit/providers/ai/__init__.py +0 -0
  43. roomkit/providers/ai/base.py +140 -0
  44. roomkit/providers/ai/mock.py +33 -0
  45. roomkit/providers/anthropic/__init__.py +6 -0
  46. roomkit/providers/anthropic/ai.py +145 -0
  47. roomkit/providers/anthropic/config.py +14 -0
  48. roomkit/providers/elasticemail/__init__.py +6 -0
  49. roomkit/providers/elasticemail/config.py +16 -0
  50. roomkit/providers/elasticemail/email.py +97 -0
  51. roomkit/providers/email/__init__.py +0 -0
  52. roomkit/providers/email/base.py +46 -0
  53. roomkit/providers/email/mock.py +34 -0
  54. roomkit/providers/gemini/__init__.py +6 -0
  55. roomkit/providers/gemini/ai.py +153 -0
  56. roomkit/providers/gemini/config.py +14 -0
  57. roomkit/providers/http/__init__.py +15 -0
  58. roomkit/providers/http/base.py +33 -0
  59. roomkit/providers/http/config.py +14 -0
  60. roomkit/providers/http/mock.py +21 -0
  61. roomkit/providers/http/provider.py +105 -0
  62. roomkit/providers/http/webhook.py +33 -0
  63. roomkit/providers/messenger/__init__.py +15 -0
  64. roomkit/providers/messenger/base.py +33 -0
  65. roomkit/providers/messenger/config.py +17 -0
  66. roomkit/providers/messenger/facebook.py +95 -0
  67. roomkit/providers/messenger/mock.py +21 -0
  68. roomkit/providers/messenger/webhook.py +42 -0
  69. roomkit/providers/openai/__init__.py +6 -0
  70. roomkit/providers/openai/ai.py +155 -0
  71. roomkit/providers/openai/config.py +24 -0
  72. roomkit/providers/pydantic_ai/__init__.py +5 -0
  73. roomkit/providers/pydantic_ai/config.py +14 -0
  74. roomkit/providers/rcs/__init__.py +9 -0
  75. roomkit/providers/rcs/base.py +95 -0
  76. roomkit/providers/rcs/mock.py +78 -0
  77. roomkit/providers/sendgrid/__init__.py +5 -0
  78. roomkit/providers/sendgrid/config.py +13 -0
  79. roomkit/providers/sinch/__init__.py +6 -0
  80. roomkit/providers/sinch/config.py +22 -0
  81. roomkit/providers/sinch/sms.py +192 -0
  82. roomkit/providers/sms/__init__.py +15 -0
  83. roomkit/providers/sms/base.py +67 -0
  84. roomkit/providers/sms/meta.py +401 -0
  85. roomkit/providers/sms/mock.py +24 -0
  86. roomkit/providers/sms/phone.py +77 -0
  87. roomkit/providers/telnyx/__init__.py +21 -0
  88. roomkit/providers/telnyx/config.py +14 -0
  89. roomkit/providers/telnyx/rcs.py +352 -0
  90. roomkit/providers/telnyx/sms.py +231 -0
  91. roomkit/providers/twilio/__init__.py +18 -0
  92. roomkit/providers/twilio/config.py +19 -0
  93. roomkit/providers/twilio/rcs.py +183 -0
  94. roomkit/providers/twilio/sms.py +200 -0
  95. roomkit/providers/voicemeup/__init__.py +15 -0
  96. roomkit/providers/voicemeup/config.py +21 -0
  97. roomkit/providers/voicemeup/sms.py +374 -0
  98. roomkit/providers/whatsapp/__init__.py +0 -0
  99. roomkit/providers/whatsapp/base.py +44 -0
  100. roomkit/providers/whatsapp/mock.py +21 -0
  101. roomkit/py.typed +0 -0
  102. roomkit/realtime/__init__.py +17 -0
  103. roomkit/realtime/base.py +111 -0
  104. roomkit/realtime/memory.py +158 -0
  105. roomkit/sources/__init__.py +35 -0
  106. roomkit/sources/base.py +207 -0
  107. roomkit/sources/websocket.py +260 -0
  108. roomkit/store/__init__.py +0 -0
  109. roomkit/store/base.py +230 -0
  110. roomkit/store/memory.py +293 -0
  111. roomkit-0.1.0.dist-info/METADATA +567 -0
  112. roomkit-0.1.0.dist-info/RECORD +114 -0
  113. roomkit-0.1.0.dist-info/WHEEL +4 -0
  114. roomkit-0.1.0.dist-info/licenses/LICENSE +21 -0
roomkit/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
+ ```