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
@@ -0,0 +1,567 @@
1
+ Metadata-Version: 2.4
2
+ Name: roomkit
3
+ Version: 0.1.0
4
+ Summary: Pure async Python library for multi-channel conversations
5
+ Project-URL: Homepage, https://github.com/sboily/roomkit
6
+ Project-URL: Repository, https://github.com/sboily/roomkit
7
+ Project-URL: Issues, https://github.com/sboily/roomkit/issues
8
+ Author: Sylvain Boily
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: pydantic>=2.9
21
+ Provides-Extra: all
22
+ Requires-Dist: anthropic>=0.30; extra == 'all'
23
+ Requires-Dist: google-genai>=1.0.0; extra == 'all'
24
+ Requires-Dist: httpx>=0.27; extra == 'all'
25
+ Requires-Dist: openai>=1.30; extra == 'all'
26
+ Requires-Dist: pydantic-ai>=0.1; extra == 'all'
27
+ Requires-Dist: twilio>=9.0; extra == 'all'
28
+ Provides-Extra: anthropic
29
+ Requires-Dist: anthropic>=0.30; extra == 'anthropic'
30
+ Provides-Extra: dev
31
+ Requires-Dist: anthropic>=0.30; extra == 'dev'
32
+ Requires-Dist: google-genai>=1.0.0; extra == 'dev'
33
+ Requires-Dist: httpx>=0.27; extra == 'dev'
34
+ Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
35
+ Requires-Dist: mkdocs>=1.6; extra == 'dev'
36
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == 'dev'
37
+ Requires-Dist: mypy>=1.11; extra == 'dev'
38
+ Requires-Dist: openai>=1.30; extra == 'dev'
39
+ Requires-Dist: phonenumbers>=8.13; extra == 'dev'
40
+ Requires-Dist: pre-commit>=3.8; extra == 'dev'
41
+ Requires-Dist: pynacl>=1.5; extra == 'dev'
42
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
43
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
44
+ Requires-Dist: pytest>=8.0; extra == 'dev'
45
+ Requires-Dist: ruff>=0.6; extra == 'dev'
46
+ Requires-Dist: twilio>=9.0; extra == 'dev'
47
+ Requires-Dist: websockets>=13.0; extra == 'dev'
48
+ Provides-Extra: docs
49
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
50
+ Requires-Dist: mkdocs>=1.6; extra == 'docs'
51
+ Requires-Dist: mkdocstrings[python]>=0.27; extra == 'docs'
52
+ Provides-Extra: gemini
53
+ Requires-Dist: google-genai>=1.0.0; extra == 'gemini'
54
+ Provides-Extra: httpx
55
+ Requires-Dist: httpx>=0.27; extra == 'httpx'
56
+ Provides-Extra: openai
57
+ Requires-Dist: openai>=1.30; extra == 'openai'
58
+ Provides-Extra: phonenumbers
59
+ Requires-Dist: phonenumbers>=8.13; extra == 'phonenumbers'
60
+ Provides-Extra: providers
61
+ Requires-Dist: anthropic>=0.30; extra == 'providers'
62
+ Requires-Dist: google-genai>=1.0.0; extra == 'providers'
63
+ Requires-Dist: httpx>=0.27; extra == 'providers'
64
+ Requires-Dist: openai>=1.30; extra == 'providers'
65
+ Requires-Dist: twilio>=9.0; extra == 'providers'
66
+ Provides-Extra: pydantic-ai
67
+ Requires-Dist: pydantic-ai>=0.1; extra == 'pydantic-ai'
68
+ Provides-Extra: pynacl
69
+ Requires-Dist: pynacl>=1.5; extra == 'pynacl'
70
+ Provides-Extra: sources
71
+ Requires-Dist: websockets>=13.0; extra == 'sources'
72
+ Provides-Extra: twilio
73
+ Requires-Dist: twilio>=9.0; extra == 'twilio'
74
+ Provides-Extra: websocket
75
+ Requires-Dist: websockets>=13.0; extra == 'websocket'
76
+ Description-Content-Type: text/markdown
77
+
78
+ # RoomKit
79
+
80
+ [![PyPI](https://img.shields.io/pypi/v/roomkit)](https://pypi.org/project/roomkit/)
81
+ [![Python](https://img.shields.io/pypi/pyversions/roomkit)](https://pypi.org/project/roomkit/)
82
+ [![License](https://img.shields.io/github/license/sboily/roomkit)](LICENSE)
83
+
84
+ Pure async Python 3.12+ library for multi-channel conversations.
85
+
86
+ RoomKit gives you a single abstraction — the **room** — to orchestrate messages across SMS, RCS, Email, WhatsApp, Messenger, WebSocket, HTTP webhooks, and AI channels. Events flow in through any channel, get validated by hooks, and broadcast to every other channel in the room with automatic content transcoding.
87
+
88
+ ```
89
+ Inbound ──► Hook pipeline ──► Store ──► Broadcast to all channels
90
+
91
+ ┌──────────┬──────────┬────────┼────────┬────────┐
92
+ ▼ ▼ ▼ ▼ ▼ ▼
93
+ SMS/RCS WebSocket Email Messenger AI Webhook
94
+ ```
95
+
96
+ **Website:** [www.roomkit.live](https://www.roomkit.live) | **Docs:** [www.roomkit.live/docs](https://www.roomkit.live/docs/)
97
+
98
+ ## Quickstart
99
+
100
+ ```bash
101
+ pip install roomkit
102
+ ```
103
+
104
+ ```python
105
+ import asyncio
106
+ from roomkit import (
107
+ ChannelCategory, HookResult, HookTrigger,
108
+ InboundMessage, MockAIProvider, RoomContext,
109
+ RoomEvent, RoomKit, TextContent, WebSocketChannel,
110
+ )
111
+ from roomkit.channels.ai import AIChannel
112
+
113
+ async def main():
114
+ kit = RoomKit()
115
+
116
+ # Register channels
117
+ ws = WebSocketChannel("ws-user")
118
+ ai = AIChannel("ai-bot", provider=MockAIProvider(responses=["Hello!"]))
119
+ kit.register_channel(ws)
120
+ kit.register_channel(ai)
121
+
122
+ # Create a room and attach channels
123
+ await kit.create_room(room_id="room-1")
124
+ await kit.attach_channel("room-1", "ws-user")
125
+ await kit.attach_channel("room-1", "ai-bot", category=ChannelCategory.INTELLIGENCE)
126
+
127
+ # Add a pre-inbound hook
128
+ @kit.hook(HookTrigger.PRE_INBOUND, name="filter")
129
+ async def block_spam(event: RoomEvent, ctx: RoomContext) -> HookResult:
130
+ if isinstance(event.content, TextContent) and "spam" in event.content.body:
131
+ return HookResult.block("spam detected")
132
+ return HookResult.allow()
133
+
134
+ # Process a message — it gets stored, broadcast, and the AI responds
135
+ result = await kit.process_inbound(
136
+ InboundMessage(channel_id="ws-user", sender_id="user-1", content=TextContent(body="Hi"))
137
+ )
138
+ print(result.blocked) # False
139
+
140
+ # View conversation history
141
+ for event in await kit.store.list_events("room-1"):
142
+ print(f"[{event.source.channel_id}] {event.content.body}")
143
+
144
+ asyncio.run(main())
145
+ ```
146
+
147
+ More examples in [`examples/`](examples/).
148
+
149
+ ## Installation
150
+
151
+ RoomKit's core has a single dependency (pydantic). Providers that call external APIs need optional extras:
152
+
153
+ ```bash
154
+ pip install roomkit # core only
155
+ pip install roomkit[httpx] # HTTP-based providers (SMS, RCS, Email)
156
+ pip install roomkit[websocket] # WebSocket event source
157
+ pip install roomkit[anthropic] # Anthropic Claude AI
158
+ pip install roomkit[openai] # OpenAI GPT
159
+ pip install roomkit[gemini] # Google Gemini AI
160
+ pip install roomkit[providers] # all transport providers
161
+ pip install roomkit[all] # everything
162
+ ```
163
+
164
+ For development:
165
+
166
+ ```bash
167
+ git clone https://github.com/sboily/roomkit.git
168
+ cd roomkit
169
+ uv sync --extra dev
170
+ make all # lint + typecheck + test
171
+ ```
172
+
173
+ Requires **Python 3.12+**.
174
+
175
+ ## Channels
176
+
177
+ Each channel is a thin adapter between the room and an external transport. All channels implement the same interface: `handle_inbound()` converts a provider message into a `RoomEvent`, and `deliver()` pushes events out.
178
+
179
+ | Channel | Type | Media | Notes |
180
+ |---------|------|-------|-------|
181
+ | **SMS** | `sms` | text, MMS | Max 1600 chars, delivery receipts |
182
+ | **RCS** | `rcs` | text, rich, media | Rich cards, carousels, suggested actions |
183
+ | **Email** | `email` | text, rich, media | Threading support |
184
+ | **WebSocket** | `websocket` | text, rich, media | Real-time with typing, reactions |
185
+ | **Messenger** | `messenger` | text, rich, media, template | Buttons, quick replies |
186
+ | **WhatsApp** | `whatsapp` | text, rich, media, location, template | Buttons, templates |
187
+ | **HTTP** | `webhook` | text, rich | Generic webhook for any system |
188
+ | **AI** | `ai` | text, rich | Intelligence layer (not transport) |
189
+
190
+ Channels have two categories: **transport** (delivers to external systems) and **intelligence** (generates content, like AI).
191
+
192
+ ## Providers
193
+
194
+ Providers handle the actual API calls. Every provider has a mock counterpart for testing.
195
+
196
+ ### SMS Providers
197
+
198
+ | Provider | Features | Dependency |
199
+ |----------|----------|------------|
200
+ | `TwilioSMSProvider` | SMS, MMS, delivery status | `roomkit[httpx]` |
201
+ | `TelnyxSMSProvider` | SMS, MMS, delivery status | `roomkit[httpx]` |
202
+ | `SinchSMSProvider` | SMS, delivery status | `roomkit[httpx]` |
203
+ | `VoiceMeUpSMSProvider` | SMS, MMS aggregation | `roomkit[httpx]` |
204
+
205
+ ### RCS Providers
206
+
207
+ | Provider | Features | Dependency |
208
+ |----------|----------|------------|
209
+ | `TwilioRCSProvider` | Rich cards, carousels, actions | `roomkit[httpx]` |
210
+ | `TelnyxRCSProvider` | Rich cards, carousels, actions | `roomkit[httpx]` |
211
+
212
+ ### AI Providers
213
+
214
+ | Provider | Features | Dependency |
215
+ |----------|----------|------------|
216
+ | `AnthropicAIProvider` | Claude, vision, tools | `roomkit[anthropic]` |
217
+ | `OpenAIAIProvider` | GPT-4, vision, tools | `roomkit[openai]` |
218
+ | `GeminiAIProvider` | Gemini, vision, tools | `roomkit[gemini]` |
219
+
220
+ ### Other Providers
221
+
222
+ | Provider | Channel | Dependency |
223
+ |----------|---------|------------|
224
+ | `ElasticEmailProvider` | Email | `roomkit[httpx]` |
225
+ | `FacebookMessengerProvider` | Messenger | `roomkit[httpx]` |
226
+ | `WebhookHTTPProvider` | HTTP | `roomkit[httpx]` |
227
+
228
+ Each HTTP-based provider lazy-imports `httpx` so the core library stays lightweight.
229
+
230
+ ## Hooks
231
+
232
+ Hooks intercept events at specific points in the pipeline. Sync hooks can block or modify events; async hooks run after the fact for logging or side effects.
233
+
234
+ ```python
235
+ @kit.hook(HookTrigger.PRE_INBOUND, name="compliance_check")
236
+ async def check(event: RoomEvent, ctx: RoomContext) -> HookResult:
237
+ # Block, allow, or modify the event
238
+ return HookResult.allow()
239
+ ```
240
+
241
+ **Triggers:** `PRE_INBOUND`, `POST_INBOUND`, `PRE_DELIVERY`, `POST_DELIVERY`, `BEFORE_BROADCAST`, `AFTER_BROADCAST`, `ON_ROOM_CREATED`, `ON_ROOM_PAUSED`, `ON_ROOM_CLOSED`, `ON_CHANNEL_ATTACHED`, `ON_CHANNEL_DETACHED`, `ON_IDENTITY_AMBIGUOUS`, `ON_IDENTITY_UNKNOWN`, `ON_DELIVERY_STATUS`, `ON_ERROR`.
242
+
243
+ Hooks support **filtering** by channel type, channel ID, and direction:
244
+
245
+ ```python
246
+ @kit.hook(
247
+ HookTrigger.PRE_INBOUND,
248
+ channel_types={ChannelType.SMS},
249
+ directions={ChannelDirection.INBOUND},
250
+ )
251
+ async def sms_only_hook(event, ctx):
252
+ return HookResult.allow()
253
+ ```
254
+
255
+ Hooks can also inject side-effect events, create tasks, and record observations.
256
+
257
+ ## AI Integration
258
+
259
+ ### Per-Room AI Configuration
260
+
261
+ Configure AI behavior per room with custom system prompts, temperature, and tools:
262
+
263
+ ```python
264
+ from roomkit import AIConfig, AITool
265
+
266
+ room = await kit.create_room(
267
+ room_id="support-room",
268
+ ai_config=AIConfig(
269
+ system_prompt="You are a helpful support agent.",
270
+ temperature=0.7,
271
+ tools=[
272
+ AITool(
273
+ name="lookup_order",
274
+ description="Look up order status",
275
+ parameters={"type": "object", "properties": {"order_id": {"type": "string"}}}
276
+ )
277
+ ],
278
+ ),
279
+ )
280
+ ```
281
+
282
+ ### Function Calling
283
+
284
+ AI providers support function calling with automatic tool result handling:
285
+
286
+ ```python
287
+ response = await ai_provider.generate(context)
288
+ if response.tool_calls:
289
+ for call in response.tool_calls:
290
+ result = await execute_tool(call.name, call.arguments)
291
+ # Feed result back to AI
292
+ ```
293
+
294
+ ## Realtime Events
295
+
296
+ Handle ephemeral events like typing indicators, presence, and read receipts:
297
+
298
+ ```python
299
+ from roomkit import EphemeralEvent, EphemeralEventType
300
+
301
+ # Subscribe to realtime events
302
+ async def handle_realtime(event: EphemeralEvent):
303
+ if event.type == EphemeralEventType.TYPING_START:
304
+ print(f"{event.user_id} is typing...")
305
+
306
+ sub_id = await kit.subscribe_room("room-1", handle_realtime)
307
+
308
+ # Publish typing indicator
309
+ await kit.publish_typing("room-1", "user-1")
310
+
311
+ # Publish presence
312
+ await kit.publish_presence("room-1", "user-1", "online")
313
+
314
+ # Publish read receipt
315
+ await kit.publish_read_receipt("room-1", "user-1", "event-123")
316
+ ```
317
+
318
+ For distributed deployments, implement a custom `RealtimeBackend` (e.g., Redis pub/sub).
319
+
320
+ ## Identity Resolution
321
+
322
+ Resolve unknown senders to known identities with a pluggable pipeline:
323
+
324
+ ```python
325
+ from roomkit import IdentityResolver, IdentityResult, IdentificationStatus, Identity
326
+
327
+ class MyResolver(IdentityResolver):
328
+ async def resolve(self, message, context):
329
+ user = await lookup(message.sender_id)
330
+ if user:
331
+ return IdentityResult(
332
+ status=IdentificationStatus.IDENTIFIED,
333
+ identity=Identity(id=user.id, display_name=user.name),
334
+ )
335
+ return IdentityResult(status=IdentificationStatus.UNKNOWN)
336
+
337
+ kit = RoomKit(identity_resolver=MyResolver())
338
+ ```
339
+
340
+ ### Channel Type Filtering
341
+
342
+ Restrict identity resolution to specific channel types:
343
+
344
+ ```python
345
+ kit = RoomKit(
346
+ identity_resolver=MyResolver(),
347
+ identity_channel_types={ChannelType.SMS}, # Only resolve for SMS
348
+ )
349
+ ```
350
+
351
+ Supports identified, pending, ambiguous, challenge, and rejected outcomes with hook-based customization.
352
+
353
+ ## Webhook Processing
354
+
355
+ Process provider webhooks with automatic parsing and delivery status tracking:
356
+
357
+ ```python
358
+ # Generic webhook processing
359
+ result = await kit.process_webhook(
360
+ channel_id="sms-channel",
361
+ raw_payload=request_body,
362
+ headers=request_headers,
363
+ )
364
+
365
+ # Handle delivery status updates
366
+ @kit.on_delivery_status
367
+ async def handle_status(status: DeliveryStatus):
368
+ print(f"Message {status.provider_message_id}: {status.status}")
369
+ ```
370
+
371
+ ## Event-Driven Sources
372
+
373
+ For persistent connections (WebSocket, NATS, SSE), use **SourceProviders** instead of webhooks:
374
+
375
+ ```python
376
+ from roomkit import RoomKit, BaseSourceProvider, SourceStatus
377
+ from roomkit.sources import WebSocketSource
378
+
379
+ kit = RoomKit()
380
+
381
+ # Built-in WebSocket source
382
+ source = WebSocketSource(
383
+ url="wss://chat.example.com/events",
384
+ channel_id="websocket-chat",
385
+ )
386
+
387
+ # Attach with resilience options
388
+ await kit.attach_source(
389
+ "websocket-chat",
390
+ source,
391
+ auto_restart=True, # Restart on failure
392
+ max_restart_attempts=10, # Give up after 10 failures
393
+ max_concurrent_emits=20, # Backpressure control
394
+ )
395
+
396
+ # Monitor health
397
+ health = await kit.source_health("websocket-chat")
398
+ print(f"Status: {health.status}, Messages: {health.messages_received}")
399
+
400
+ # Detach when done
401
+ await kit.detach_source("websocket-chat")
402
+ ```
403
+
404
+ **Webhook vs Event-Driven:**
405
+
406
+ | Aspect | Webhooks | Event Sources |
407
+ |--------|----------|---------------|
408
+ | Connection | Stateless HTTP | Persistent (WS, TCP, etc.) |
409
+ | Initiative | External system pushes | RoomKit subscribes |
410
+ | Use cases | Twilio, SendGrid | WebSocket, NATS, SSE |
411
+
412
+ Create custom sources by extending `BaseSourceProvider`:
413
+
414
+ ```python
415
+ class NATSSource(BaseSourceProvider):
416
+ @property
417
+ def name(self) -> str:
418
+ return "nats:events"
419
+
420
+ async def start(self, emit) -> None:
421
+ self._set_status(SourceStatus.CONNECTED)
422
+ async for msg in self.subscribe():
423
+ await emit(parse_message(msg))
424
+ self._record_message()
425
+ ```
426
+
427
+ Features: exponential backoff, max restart attempts, backpressure control, health monitoring.
428
+
429
+ ## Resilience
430
+
431
+ Built-in patterns for production reliability:
432
+
433
+ - **Retry with backoff** — configurable per-channel retry policy with exponential backoff
434
+ - **Circuit breaker** — isolates failing providers so one broken channel doesn't bring down the room
435
+ - **Rate limiting** — token bucket limiter with per-second/minute/hour limits per channel
436
+ - **Content transcoding** — automatic conversion between channel capabilities (e.g. rich to text fallback)
437
+ - **Chain depth tracking** — prevents infinite event loops between channels
438
+
439
+ Configure per binding:
440
+
441
+ ```python
442
+ from roomkit import RetryPolicy, RateLimit
443
+
444
+ await kit.attach_channel("room-1", "sms-out",
445
+ metadata={"phone_number": "+15551234567"},
446
+ retry_policy=RetryPolicy(max_retries=3, base_delay_seconds=1.0),
447
+ rate_limit=RateLimit(max_per_second=5.0),
448
+ )
449
+ ```
450
+
451
+ ## Scaling
452
+
453
+ ### Per-room locking
454
+
455
+ RoomKit serializes event processing per room using a `RoomLockManager`. The default `InMemoryLockManager` works for single-process deployments. For multi-process or distributed setups, subclass `RoomLockManager` with a distributed lock (Redis, Postgres advisory locks, etc.):
456
+
457
+ ```python
458
+ from roomkit import RoomKit, RoomLockManager
459
+
460
+ # Single process (default)
461
+ kit = RoomKit()
462
+
463
+ # Distributed
464
+ kit = RoomKit(lock_manager=MyRedisLockManager())
465
+ ```
466
+
467
+ ### Backpressure
468
+
469
+ RoomKit does not enforce a global concurrency limit on `process_inbound` calls. Each call acquires a per-room lock internally, but across different rooms, processing is fully concurrent.
470
+
471
+ To prevent resource exhaustion, add concurrency control upstream:
472
+
473
+ ```python
474
+ import asyncio
475
+ from roomkit import RoomKit, InboundMessage, InboundResult
476
+
477
+ kit = RoomKit()
478
+ semaphore = asyncio.Semaphore(100) # max 100 concurrent rooms processing
479
+
480
+ async def handle_webhook(message: InboundMessage) -> InboundResult:
481
+ async with semaphore:
482
+ return await kit.process_inbound(message)
483
+ ```
484
+
485
+ ## Room Lifecycle
486
+
487
+ Rooms transition through states automatically based on activity timers:
488
+
489
+ ```
490
+ ACTIVE ──(inactive timeout)──► PAUSED ──(closed timeout)──► CLOSED
491
+ ```
492
+
493
+ ```python
494
+ from roomkit import RoomTimers
495
+
496
+ await kit.create_room(
497
+ room_id="support-123",
498
+ timers=RoomTimers(inactive_after_seconds=300, closed_after_seconds=3600),
499
+ )
500
+
501
+ # Check and apply timer transitions
502
+ transitioned = await kit.sweep_room_timers()
503
+ ```
504
+
505
+ ## Storage
506
+
507
+ The `ConversationStore` ABC defines the persistence interface. `InMemoryStore` is included for development and testing. Implement the ABC to use any database.
508
+
509
+ ```python
510
+ kit = RoomKit() # uses InMemoryStore by default
511
+ kit = RoomKit(store=MyPostgresStore()) # plug in your own
512
+ ```
513
+
514
+ The store handles rooms, events, bindings, participants, identities, tasks, and observations.
515
+
516
+ ## AI Assistant Support
517
+
518
+ RoomKit includes files to help AI coding assistants understand the library:
519
+
520
+ - **[llms.txt](https://www.roomkit.live/llms.txt)** — Structured documentation for LLM context windows
521
+ - **[AGENTS.md](AGENTS.md)** — Coding guidelines and patterns for AI assistants
522
+ - **[MCP Integration](https://www.roomkit.live/docs/mcp/)** — Model Context Protocol support
523
+
524
+ Access programmatically:
525
+
526
+ ```python
527
+ from roomkit import get_llms_txt, get_agents_md
528
+
529
+ llms_content = get_llms_txt()
530
+ agents_content = get_agents_md()
531
+ ```
532
+
533
+ ## Project Structure
534
+
535
+ ```
536
+ src/roomkit/
537
+ channels/ Channel implementations (sms, rcs, email, websocket, ai, ...)
538
+ core/ Framework, hooks, routing, retry, circuit breaker
539
+ identity/ Identity resolution pipeline
540
+ models/ Pydantic data models and enums
541
+ providers/ Provider implementations grouped by vendor
542
+ realtime/ Ephemeral events (typing, presence, read receipts)
543
+ sources/ Event-driven sources (WebSocket, custom)
544
+ store/ Storage abstraction and in-memory implementation
545
+ ```
546
+
547
+ ## Documentation
548
+
549
+ - **[Website](https://www.roomkit.live)** — Landing page and overview
550
+ - **[Documentation](https://www.roomkit.live/docs/)** — Full documentation
551
+ - **[API Reference](https://www.roomkit.live/docs/api/)** — Complete API docs
552
+ - **[RFC](https://www.roomkit.live/docs/roomkit-rfc/)** — Design document
553
+
554
+ ## Contributing
555
+
556
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Quick version:
557
+
558
+ ```bash
559
+ uv sync --extra dev
560
+ make all # ruff check + mypy --strict + pytest
561
+ ```
562
+
563
+ All new code needs tests. Aim for >90% coverage.
564
+
565
+ ## License
566
+
567
+ [MIT](LICENSE)
@@ -0,0 +1,114 @@
1
+ roomkit/__init__.py,sha256=PYIvFXG8e6MEwEPXKltIaGxnLvft5mlqTcaJd1Vi2jA,10093
2
+ roomkit/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ roomkit/ai_docs.py,sha256=mnKnwraTD5J38vm7Rm_q-2BYPBh2vXQ7OgT8i-B_niY,2336
4
+ roomkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ roomkit/channels/__init__.py,sha256=FQYB5c_y8dxyB7Os0l7o157-LVlneQrGltxF18HMugk,5023
6
+ roomkit/channels/ai.py,sha256=7RK-PXsPwHw4ADhJL2Z1MGlCXseKpdg8-J1bls-AoMM,8330
7
+ roomkit/channels/base.py,sha256=rS7vUOzLFZwjd8ulB3iihlbLQ1FhgTjj8OmYAYWhETM,2137
8
+ roomkit/channels/transport.py,sha256=2bnAxv1-J6h-CZcoizKd5gZghmQZms5Pyspo4pB1RuY,4744
9
+ roomkit/channels/websocket.py,sha256=CfidIKW_DK7Q6yljjlN5mR79OTHOIgX67IUU5Bj7CPE,2954
10
+ roomkit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ roomkit/core/_channel_ops.py,sha256=SSDddZTxqcsSDCQ6PhQfQRhJs-KGGAO3moQqFQcJ-GI,10271
12
+ roomkit/core/_helpers.py,sha256=-PxREqVe-cV3mZhPaCsechuQs5bUfVqNHSKdu2EM8j8,11270
13
+ roomkit/core/_inbound.py,sha256=5PkRo6VOZKi6nJ4Kb9CJJvxPyEND6jwt8U-xDh7PrLY,18078
14
+ roomkit/core/_room_lifecycle.py,sha256=xo2UwDZLuZA1xXXGeg85qdj1j3_-fJNM2B_J3E5DRJU,10661
15
+ roomkit/core/circuit_breaker.py,sha256=kEXEI8EEkD5LZyegAaFZb5U4mrX2IDnAjI0avv46exs,2873
16
+ roomkit/core/event_router.py,sha256=TNweMDuzG0OPk3qvJ-CiQgtcANCMJD5FdNAtTtIRPSo,16634
17
+ roomkit/core/framework.py,sha256=NzWe39s-S0MH7EyuEQhAN9xteEW6ck9P_43imbSU8-A,29509
18
+ roomkit/core/hooks.py,sha256=OWgKOHU53Pxdfg0GH8vhvBbWGdf6xete9m4kkC-I2TE,8479
19
+ roomkit/core/inbound_router.py,sha256=QvuFAjWedeV02tJC6xU8JQGWk-qlhKnQl58JOL6crw4,1773
20
+ roomkit/core/locks.py,sha256=_2hqKbCjtWVyeKiiXD_StAHYdghW_Daw0VIPeiNfoJA,2060
21
+ roomkit/core/rate_limiter.py,sha256=u6AEyB4LVy85fWSOPk5FbBvV9KyRUYV87D-X7FFwaCE,2624
22
+ roomkit/core/retry.py,sha256=OpBLZMHisfvjGwdI05G-SAPNz48t9062V_neHLLVllA,1396
23
+ roomkit/core/router.py,sha256=cIxIDt9gnHswF3p9dycOo7Ls4XCzKQ3LLtbugvWZGYY,680
24
+ roomkit/core/transcoder.py,sha256=MLSPgf8Mj6PdZjkzX0NQHXw858Z9fBYbxyBPFieCkhg,3093
25
+ roomkit/identity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ roomkit/identity/base.py,sha256=1BOFNm7jc90MUeRCFO4uWQBjSRJpz1j3SCGmWHLQ25I,850
27
+ roomkit/identity/mock.py,sha256=DNZyAZ80r2dceHKxMl-zhVe15mQemLNHNUN4UFd-ZfA,1792
28
+ roomkit/models/__init__.py,sha256=1xjIztFpQnOsWa6CteMIz5NaEuAr_0leq00nFdk7NDc,2242
29
+ roomkit/models/channel.py,sha256=70Jxynrsi-LSMtrWgtbpzysEUIFS9H_MXzjRfmyECa8,3525
30
+ roomkit/models/context.py,sha256=1AoTwUBhff_eSCD6tGv66Gj7qoIeGeuvjH3ZFaxsgwA,1335
31
+ roomkit/models/delivery.py,sha256=0HEGvkymYjocfWY52DX1kYO7pFNIEQ7rdHwhlx6WQTQ,2242
32
+ roomkit/models/enums.py,sha256=x_nZ-Ng13-FR-f6XNVEVsYgHdYRXpI2eJVO1vlFOC7I,3768
33
+ roomkit/models/event.py,sha256=n6fgTR-KWDMA2E9YE8tsAiWlltT8jCgbd5aoI0vJW_A,5887
34
+ roomkit/models/framework_event.py,sha256=ZVJPXymC8VjiG4pKaKw2zHq9XimJE0jOYVUw1yLY500,509
35
+ roomkit/models/hook.py,sha256=MnVmh5gjVBeiBZhWzpz9pmOPslQZ_N4xpzHAc7h5TPY,2192
36
+ roomkit/models/identity.py,sha256=l7lpHVknLGzZJwGumnGfHeI0u3bzJmPdnfK9jDkA1uA,2582
37
+ roomkit/models/participant.py,sha256=NX2wZylXKp9OG-4YA9Rc7Elpi7sdrHbT6gnIW2tPO7k,991
38
+ roomkit/models/room.py,sha256=M2XuWKqudRKErcxL1awOrFYkGMlcPbxou5gghYA3UH0,1000
39
+ roomkit/models/task.py,sha256=9hMfZc8Z_ccD2cV7Adb8XC39rEH6iShF3zfQ3T5jG_k,961
40
+ roomkit/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ roomkit/providers/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ roomkit/providers/ai/base.py,sha256=ZqJqZ_DJpTPMInSHZcJ4-yi-JwcmOdquDYRiXMxBB2k,3949
43
+ roomkit/providers/ai/mock.py,sha256=aMOJwY0vX9WMc-BuCfV8e-bkp0nAjeseLdIlu5xDdJs,987
44
+ roomkit/providers/anthropic/__init__.py,sha256=2kwNeqgDpXwV13ksMALO8BSWnZA_yslBdPNhPrUYYH0,207
45
+ roomkit/providers/anthropic/ai.py,sha256=SrdLvLARHYe2EXM3JospigZ6q6wxObZD5VbMIGxvRvc,4691
46
+ roomkit/providers/anthropic/config.py,sha256=dcNFCtpoNPuUPn_SkG58eDg-0bb1jckr5Fes8RpWGhs,326
47
+ roomkit/providers/elasticemail/__init__.py,sha256=0h2RG9wt3HwDPAb3axTwN3MG9s-iuxdK7NFZqvCjtiM,228
48
+ roomkit/providers/elasticemail/config.py,sha256=5MLU0R2IJo-O4LFR7C-F9Nz9VOKxGWRDFynGoliUw_Y,412
49
+ roomkit/providers/elasticemail/email.py,sha256=xDz4gbe3izP070W_56i3P-WpPbPDF1WnPkX11IYlTTY,3365
50
+ roomkit/providers/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ roomkit/providers/email/base.py,sha256=Sl-A7wnlilbKDaKstM8i2T05jxYkrBATi-CktcLNEtE,1306
52
+ roomkit/providers/email/mock.py,sha256=r7IhMIouV9bQjZpExGyYrQ2OrHmWhGdgofOmRZLmA5A,900
53
+ roomkit/providers/gemini/__init__.py,sha256=rEXAnbH28iHm4agFvEqJH4LljU2d27hhdbAUt9mBMds,193
54
+ roomkit/providers/gemini/ai.py,sha256=_SM4j1ScIskYBazaT16cDFs-svvldpQX31oBwdE5A3k,5326
55
+ roomkit/providers/gemini/config.py,sha256=iBcksrmKYr2Rx9gsIwxUecN30xZP-BU23BHY2fsYh1c,341
56
+ roomkit/providers/http/__init__.py,sha256=y0Mr2Qu0uoTvZFBpPKCcZmGXsnA79YG1gIKvJwXLZ80,473
57
+ roomkit/providers/http/base.py,sha256=W4lTxUH_-IBxOqSaOpk8NWa7vc2QgSXIbDZp5g9-4kw,865
58
+ roomkit/providers/http/config.py,sha256=DdgmHS8c8dZMtEtp5HmxdvAU4uAEvPXcT1FRKZGiTJU,341
59
+ roomkit/providers/http/mock.py,sha256=7kujHCWdMP4kVH8VM9wTyhQWGz9ZrOJJjG1HitVTMEw,648
60
+ roomkit/providers/http/provider.py,sha256=c6u6wqd9a6Y4q6ogxAyXfnDSsDoRWx3lGsXyZhopFvM,3484
61
+ roomkit/providers/http/webhook.py,sha256=uEunVVwIlLsT0IlC64pKKLkJAiMEUYx4WZmuCUbD50o,886
62
+ roomkit/providers/messenger/__init__.py,sha256=Yp3Kn_FwdpddzysGKHArxIjB4QfjkJiPCh1OZM2VjaI,532
63
+ roomkit/providers/messenger/base.py,sha256=mA6pNUtQW0KhQnCFNGk9pH14KEeV_1Om6_RM6Sq-nrI,905
64
+ roomkit/providers/messenger/config.py,sha256=2r2QWCFJxKn3DTjlPuUCg1Jk710h4GquFWBh7sbo38k,430
65
+ roomkit/providers/messenger/facebook.py,sha256=iQXmEZ9qXwbhKmi3vDBovKnwDhhr90JB87Dthgdvjzo,3215
66
+ roomkit/providers/messenger/mock.py,sha256=iexGJRsPIUS9JxDYaRbRagRxWcRXL0W3fFw_3yY9u1A,673
67
+ roomkit/providers/messenger/webhook.py,sha256=zD54df12OQl3IhQxpC5xJs7UdDXnDDNb6zLZ-mWBHNg,1530
68
+ roomkit/providers/openai/__init__.py,sha256=rnbPVtymGeKX4jV5DlFcUOIv7csIiL7fov-Ea3m5fCo,186
69
+ roomkit/providers/openai/ai.py,sha256=duBPJWAGQ3r24CCqWfdvkyYeIhP4g2RlKvfV6-uFxco,4888
70
+ roomkit/providers/openai/config.py,sha256=3Nul5LafjJwVziEWXBd9DKpknVThADneCyNq2IJCreI,705
71
+ roomkit/providers/pydantic_ai/__init__.py,sha256=0TCON1PkeLymIDd3JwInn5_UTNCrPj4F9dNjr3QMkyA,127
72
+ roomkit/providers/pydantic_ai/config.py,sha256=h4AHprHCroZY7UVWv_Tym-yqKnxcMjZGM4Ky4SS9YtU,331
73
+ roomkit/providers/rcs/__init__.py,sha256=bwuwVj-eBoas_38D3ghdCOMW7YiX19Qq1gk2N7ATR4M,217
74
+ roomkit/providers/rcs/base.py,sha256=uduI-V6Rk8xsLkVLbWnNr5s-A_q2zoohXYXJHVs5JiI,2881
75
+ roomkit/providers/rcs/mock.py,sha256=F4OQpvYXRmYYhXhVeoTmTd8zu_5gXcR__jPeDPsRDJ8,2212
76
+ roomkit/providers/sendgrid/__init__.py,sha256=i_IP6yGn2mSVmc6TfJRyda1whDFj6dmqfTR0-2u5pyw,117
77
+ roomkit/providers/sendgrid/config.py,sha256=xyqqKridfUw7AThz6Z2fJPV0e4beMpHug3NcZUa3aPs,279
78
+ roomkit/providers/sinch/__init__.py,sha256=5sYsxkY35ldyfXVWwWX_ZV5jXVa7kGKNknm_SRu4xjI,226
79
+ roomkit/providers/sinch/config.py,sha256=vWBNdPYOJRJiGJX8xFc_t6FyH2OdkzzLCm0Azp_zeuY,559
80
+ roomkit/providers/sinch/sms.py,sha256=eNUORI6W0QKl7gxJ1UYh80OeotA_BJbPF4BWO6aVhF8,6276
81
+ roomkit/providers/sms/__init__.py,sha256=DUpHOvbymXxLkKGsACaAAPNicFr2W7ACDDJO2Q1UweM,414
82
+ roomkit/providers/sms/base.py,sha256=MJ2Wz3G_LsLrlKnhELZRrpsNaZNsh-1VqdFG7bXxNec,2081
83
+ roomkit/providers/sms/meta.py,sha256=uMhpaP818PcP3yoOeifWTXrsBrNO9YdrN5ccG10s6-c,13452
84
+ roomkit/providers/sms/mock.py,sha256=psp3GEunBX22_QeuUEpNGzQJ_wUnBafF-iVk7bwKOws,758
85
+ roomkit/providers/sms/phone.py,sha256=7hPhtkYNyz-gzzisaRsqpp-ETkcCe_qnN7BDHC4LM-E,2529
86
+ roomkit/providers/telnyx/__init__.py,sha256=gR4Wl1wBh3jHxncBz5ywBGrZ1Fk1YeSf-ifa7nfL1HQ,462
87
+ roomkit/providers/telnyx/config.py,sha256=IjSWQz7uHClhVdIhx22qF1ZUJijiuWzhDy3JsODvchE,309
88
+ roomkit/providers/telnyx/rcs.py,sha256=1PngjG5M-Z98WT5Cwxak30OV_3S68TohwDeNxQGQisQ,12091
89
+ roomkit/providers/telnyx/sms.py,sha256=6E3hZ_WRKw1XeJ4Wq36sE3VER87-8f0NxXRobPD8hlI,7996
90
+ roomkit/providers/twilio/__init__.py,sha256=xalOYLH071MDDRMHk9AextP1wIb2Yp5e96GgM2fpBYE,449
91
+ roomkit/providers/twilio/config.py,sha256=raqd3usAbFnN6DRuqhH82j1Fu-xHsNjLi0oORieiKgU,473
92
+ roomkit/providers/twilio/rcs.py,sha256=aGA8seEPXwV5KzLuSSiTLetNYd3RsGDnaJUKb7NqrPY,6162
93
+ roomkit/providers/twilio/sms.py,sha256=yxCyGcEOTPEpznxTYOxSesSwTi69Z8v6ViHmixqUqEk,6769
94
+ roomkit/providers/voicemeup/__init__.py,sha256=f9DEfqYGkkq0Gz0WyO3YZ3Z4FqMUDga_UOgbb52ZJhE,350
95
+ roomkit/providers/voicemeup/config.py,sha256=rlinegfi0Ui4m4ly06a_LRc15PbRmmTzSGEtc329Js4,551
96
+ roomkit/providers/voicemeup/sms.py,sha256=wojWXYB9zuI0VMVwaVbqOMKvi287dspESgit9CGydi4,13132
97
+ roomkit/providers/whatsapp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
+ roomkit/providers/whatsapp/base.py,sha256=QyDqHG82XWIigmMaOePYOlCs2QJMzW9pT0GC6YyTvfk,1351
99
+ roomkit/providers/whatsapp/mock.py,sha256=i4wtsVMsBpx6LXjQpgvK7aoOEH2dMxu5W9SWaG2H0XE,668
100
+ roomkit/realtime/__init__.py,sha256=unS1HPdMmfYSMf1WVo9mvMDuw3ceP4bkyB60cCTcT8w,360
101
+ roomkit/realtime/base.py,sha256=8savEf_efHVLkmuPDwwX1IVbXru_GMhVtlfVbP1qMTg,3476
102
+ roomkit/realtime/memory.py,sha256=aIh2dy2g9V6n_VhEtSuzcYpB7UtDMQR6dpXK-B91Ne8,5128
103
+ roomkit/sources/__init__.py,sha256=Qh2QGw2k0kX5cPBbgG4zzVoDV7-SJx3XzSkrtDtPiT8,854
104
+ roomkit/sources/base.py,sha256=8oHjw0vdcOMYxwtenN-dwuBwCx42bmMdfC_KZwriAaE,6637
105
+ roomkit/sources/websocket.py,sha256=j60gsILNutqJNPdVqyOBbQ5sUV3XOes2LwhLMKZIOE0,8521
106
+ roomkit/store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
+ roomkit/store/base.py,sha256=Q5p-lOQmWJqiBd7jD-1inee6qoYBhlb-C06Z2bzJ8mI,6781
108
+ roomkit/store/memory.py,sha256=_cAUBuC7BUwOKEa6aomr4aG7t-J0CuiK7izagQ-Daig,11932
109
+ roomkit/AGENTS.md,sha256=lETRMzpOTJ7ilhbm3_ufZYu5Y7Q0nTlIEU2-oT2XN8Q,10490
110
+ roomkit/llms.txt,sha256=QWeLJaSnt2nKUmmQxChnEFPO675ikMFm0TDHMdEKn8s,4039
111
+ roomkit-0.1.0.dist-info/METADATA,sha256=aA83baVrZkex6_VsyO85PSxYvjZy7Gn4Tg85aGH0VcA,19391
112
+ roomkit-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
113
+ roomkit-0.1.0.dist-info/licenses/LICENSE,sha256=GIXnbN1bOyI_7Ydnf7EXBxrFi344wlsdVV4acemzRcE,1070
114
+ roomkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any