|
1 | 1 | import asyncio |
| 2 | +from collections.abc import Mapping |
2 | 3 | import json |
3 | 4 | import logging |
4 | | -from uuid import UUID |
5 | 5 |
|
6 | 6 | import asyncpg |
7 | | -from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response |
| 7 | +from fastapi import APIRouter, Header, HTTPException, Request, Response |
8 | 8 |
|
9 | | -from api.dependencies import get_current_user, get_db |
| 9 | +from api.db.connection import get_pool |
10 | 10 | from api.services.extraction import run_extraction_task |
11 | | -from api.services.proxy import build_proxy_result |
| 11 | +from api.services.proxy import ProxyResult, build_proxy_passthrough_result, build_proxy_result |
| 12 | +from api.services.users import get_cached_user_by_api_key, get_user_by_api_key |
12 | 13 |
|
13 | 14 |
|
14 | 15 | logger = logging.getLogger(__name__) |
|
18 | 19 | @router.post("/v1/chat") |
19 | 20 | async def proxy_chat( |
20 | 21 | request: Request, |
| 22 | + x_engram_key: str = Header(...), |
21 | 23 | x_engram_user_id: str = Header(...), |
22 | 24 | x_engram_provider: str = Header(default="openai"), |
23 | 25 | x_engram_disable_injection: bool = Header(default=False), |
24 | 26 | x_engram_disable_extraction: bool = Header(default=False), |
25 | | - db: asyncpg.Connection = Depends(get_db), |
26 | | - user: asyncpg.Record = Depends(get_current_user), |
27 | 27 | ) -> Response: |
28 | 28 | body = await parse_request_body(request) |
| 29 | + result = await build_proxy_response_with_available_auth( |
| 30 | + x_engram_key, |
| 31 | + x_engram_user_id, |
| 32 | + x_engram_provider, |
| 33 | + x_engram_disable_injection, |
| 34 | + x_engram_disable_extraction, |
| 35 | + body, |
| 36 | + request.headers, |
| 37 | + ) |
| 38 | + return create_response(result) |
| 39 | + |
| 40 | + |
| 41 | +async def build_proxy_response_with_available_auth( |
| 42 | + api_key: str, |
| 43 | + requested_external_id: str, |
| 44 | + provider: str, |
| 45 | + disable_injection: bool, |
| 46 | + disable_extraction: bool, |
| 47 | + body: dict[str, object], |
| 48 | + headers: Mapping[str, str], |
| 49 | +) -> ProxyResult: |
| 50 | + try: |
| 51 | + pool = get_pool() |
| 52 | + except RuntimeError as error: |
| 53 | + logger.warning("Database pool unavailable, trying cached proxy auth: %s", error) |
| 54 | + return await build_cached_proxy_response(api_key, requested_external_id, provider, body, headers) |
| 55 | + acquire_context = pool.acquire() |
29 | 56 | try: |
| 57 | + db = await acquire_context.__aenter__() |
| 58 | + except (asyncpg.PostgresError, OSError, ConnectionError, RuntimeError) as error: |
| 59 | + logger.warning("Database unavailable, trying cached proxy auth: %s", error) |
| 60 | + return await build_cached_proxy_response(api_key, requested_external_id, provider, body, headers) |
| 61 | + try: |
| 62 | + try: |
| 63 | + user = await get_user_by_api_key(api_key, db) |
| 64 | + except (asyncpg.PostgresError, OSError, ConnectionError) as error: |
| 65 | + logger.warning("Database auth unavailable, trying cached proxy auth: %s", error) |
| 66 | + return await build_cached_proxy_response(api_key, requested_external_id, provider, body, headers) |
| 67 | + if user is None: |
| 68 | + raise HTTPException(status_code=401, detail="Invalid API key") |
30 | 69 | result = await build_proxy_result( |
31 | 70 | user["id"], |
32 | 71 | user["external_id"], |
33 | | - x_engram_user_id, |
| 72 | + requested_external_id, |
34 | 73 | body, |
35 | | - x_engram_provider, |
36 | | - x_engram_disable_injection, |
37 | | - request.headers, |
| 74 | + provider, |
| 75 | + disable_injection, |
| 76 | + headers, |
38 | 77 | db, |
| 78 | + int(user["max_memories_injected"]), |
| 79 | + float(user["retrieval_threshold"]), |
39 | 80 | ) |
40 | 81 | except PermissionError as error: |
41 | 82 | raise HTTPException(status_code=403, detail=str(error)) from error |
42 | 83 | except ValueError as error: |
43 | 84 | raise HTTPException(status_code=422, detail=str(error)) from error |
44 | 85 | except RuntimeError as error: |
45 | 86 | raise HTTPException(status_code=502, detail=str(error)) from error |
46 | | - if not x_engram_disable_extraction and result.status_code < 400: |
| 87 | + finally: |
| 88 | + try: |
| 89 | + await acquire_context.__aexit__(None, None, None) |
| 90 | + except (asyncpg.PostgresError, OSError, ConnectionError) as error: |
| 91 | + logger.warning("Database connection release failed: %s", error) |
| 92 | + if not disable_extraction and result.status_code < 400: |
47 | 93 | try: |
48 | | - asyncio.create_task(run_extraction_task(user["id"], result.conversation_id, body, result.content)) |
| 94 | + asyncio.create_task(run_extraction_task(user["id"], result.conversation_id, body, result.content, float(user["dedup_threshold"]))) |
49 | 95 | except Exception as error: |
50 | 96 | logger.warning("Failed to schedule extraction: %s", error) |
| 97 | + return result |
| 98 | + |
| 99 | + |
| 100 | +async def build_cached_proxy_response( |
| 101 | + api_key: str, |
| 102 | + requested_external_id: str, |
| 103 | + provider: str, |
| 104 | + body: dict[str, object], |
| 105 | + headers: Mapping[str, str], |
| 106 | +) -> ProxyResult: |
| 107 | + user = get_cached_user_by_api_key(api_key) |
| 108 | + if user is None: |
| 109 | + raise HTTPException(status_code=503, detail="Database unavailable and API key is not cached") |
| 110 | + try: |
| 111 | + return await build_proxy_passthrough_result(str(user["external_id"]), requested_external_id, body, provider, headers) |
| 112 | + except PermissionError as error: |
| 113 | + raise HTTPException(status_code=403, detail=str(error)) from error |
| 114 | + except ValueError as error: |
| 115 | + raise HTTPException(status_code=422, detail=str(error)) from error |
| 116 | + except RuntimeError as error: |
| 117 | + raise HTTPException(status_code=502, detail=str(error)) from error |
| 118 | + |
| 119 | + |
| 120 | +def create_response(result: ProxyResult) -> Response: |
51 | 121 | return Response( |
52 | 122 | content=result.content, |
53 | 123 | status_code=result.status_code, |
|
0 commit comments