feat: implement durable authenticated chat run coordinator

This commit is contained in:
snowspeeder committed 2026-10-03 08:57:41 -04:00
1 parent 08b7e20be8
commit 550f7c2e91
6 files changed
+905 -3

No files matched your search

+32 -3
View File
@@ -206,13 +206,25 @@ class Lifecycle:
await asyncio.gather(*tasks, return_exceptions=True)
def create_app(settings=None, provisioner=None, model_catalog=None):
def create_app(settings=None, provisioner=None, model_catalog=None, chat_adapter=None, feature_provider=None):
settings = settings or Settings()
@asynccontextmanager
async def lifespan(app):
store = Accounts(settings.db_path)
app.state.accounts = store
from .chat_store import ChatStore
from .chat_runs import ChatRuns
app.state.chat_store = await asyncio.to_thread(ChatStore, store)
from .chat_features import FeatureStore, FeatureProvider
from pathlib import Path
feature_root = str(Path(settings.db_path).absolute().parent / 'chat-features')
app.state.feature_store = await asyncio.to_thread(FeatureStore, store, feature_root)
app.state.feature_provider = feature_provider or FeatureProvider(store)
from .chat_features import create_feature_router
app.include_router(create_feature_router(app.state.feature_store, app.state.feature_provider))
app.state.chat_runs = await asyncio.to_thread(ChatRuns, store, app.state.chat_store, chat_adapter, app.state.feature_store)
app.state.chat_runs.feature_provider = app.state.feature_provider
try:
if not store.users():
if not settings.admin_password:
@@ -231,9 +243,13 @@ def create_app(settings=None, provisioner=None, model_catalog=None):
app.state.lifecycle.schedule(user['id'])
app.state.dummy_hash = await asyncio.to_thread(password_hash, 'timing-only-password')
loop = asyncio.create_task(app.state.lifecycle.loop(settings.reconcile_interval))
await app.state.chat_runs.start()
try:
yield
finally:
await app.state.chat_runs.stop()
if hasattr(app.state.feature_provider, 'aclose'):
await app.state.feature_provider.aclose()
loop.cancel()
await asyncio.gather(loop, return_exceptions=True)
await app.state.lifecycle.stop()
@@ -407,6 +423,7 @@ def create_app(settings=None, provisioner=None, model_catalog=None):
if user['status'] != 'ready' or not user['hermes_url'] or not user['hermes_api_key']:
raise HTTPException(503, 'User Hermes is not ready')
return {'user': public_user(user), 'hermes_url': user['hermes_url'], 'hermes_api_key': user['hermes_api_key'],
'conversation_id': user.get('conversation_id'),
'model_settings': app.state.accounts.get_model_settings(user['id'])}
@app.get('/internal/users/{user_id}', dependencies=[Depends(internal_authorized)])
@@ -430,13 +447,21 @@ def create_app(settings=None, provisioner=None, model_catalog=None):
raise HTTPException(400, 'Invalid room grant')
if type(ttl) is not int or not 1 <= ttl <= 86400:
raise HTTPException(400, 'Invalid room expiry')
conversation_id = body.get('conversation_id')
try:
await asyncio.to_thread(app.state.accounts.grant_room, user_id, room, ttl, body.get('session_token'))
if conversation_id is None:
conversation = await asyncio.to_thread(app.state.chat_store.create_conversation, user_id)
conversation_id = conversation['id']
else:
await asyncio.to_thread(app.state.chat_store.get_conversation, user_id, conversation_id)
await asyncio.to_thread(app.state.accounts.grant_room, user_id, room, ttl, body.get('session_token'), conversation_id)
except KeyError:
raise HTTPException(404, 'Conversation not found') from None
except ValueError:
raise HTTPException(409, 'Account not ready') from None
except sqlite3.IntegrityError:
raise HTTPException(409, 'Room already granted') from None
return {'room_name': room, 'user_id': user_id}
return {'room_name': room, 'user_id': user_id, 'conversation_id': conversation_id}
@app.get('/internal/rooms/{room_name}', dependencies=[Depends(internal_authorized)])
async def resolve_room(room_name: str):
@@ -446,4 +471,8 @@ def create_app(settings=None, provisioner=None, model_catalog=None):
async def health():
return {'status': 'ok'}
from .chat_api import create_chat_router
app.include_router(create_chat_router())
from .chat_exports import create_export_router
app.include_router(create_export_router())
return app