tyndale-ai-service/OPENAI_INTEGRATION_PLAN.md

4.6 KiB

OpenAI Integration Plan for Tyndale AI Service

Summary

Replace the LocalAdapter stub with an OpenAI API client, using FastAPI's Depends() for dependency injection and keeping configuration minimal (API key + model only).


Files to Modify

File Action
requirements.txt Add openai>=1.0.0
app/config.py Add openai_api_key, openai_model settings
app/llm/exceptions.py Create: Custom exception hierarchy
app/llm/adapter.py Add OpenAIAdapter class, refactor get_adapter() for DI
app/llm/__init__.py Export new components
app/schemas.py Add "openai" to mode literal
app/main.py Use Depends() for adapter injection
.env.example Add OpenAI env vars
Dockerfile Add OPENAI_MODEL default env

Implementation Steps

1. Add OpenAI dependency

File: requirements.txt

openai>=1.0.0

2. Extend configuration

File: app/config.py

  • Add llm_mode: Literal["local", "remote", "openai"]
  • Add openai_api_key: str = ""
  • Add openai_model: str = "gpt-4o-mini"

3. Create exception hierarchy

File: app/llm/exceptions.py (new)

  • LLMError (base)
  • LLMAuthenticationError (401)
  • LLMRateLimitError (429)
  • LLMConnectionError (503)
  • LLMConfigurationError (500)
  • llm_exception_to_http() helper

4. Add OpenAIAdapter class

File: app/llm/adapter.py

class OpenAIAdapter(LLMAdapter):
    def __init__(self, api_key: str, model: str = "gpt-4o-mini"):
        self.client = OpenAI(api_key=api_key)
        self.model = model

    async def generate(self, conversation_id: str, message: str) -> str:
        # Use asyncio.to_thread() since OpenAI SDK is synchronous
        response = await asyncio.to_thread(
            self.client.chat.completions.create,
            model=self.model,
            messages=[{"role": "user", "content": message}],
        )
        return response.choices[0].message.content

5. Refactor get_adapter() for dependency injection

File: app/llm/adapter.py

from functools import lru_cache
from typing import Annotated
from fastapi import Depends

@lru_cache()
def get_settings() -> Settings:
    return settings

def get_adapter(settings: Annotated[Settings, Depends(get_settings)]) -> LLMAdapter:
    if settings.llm_mode == "openai":
        return OpenAIAdapter(api_key=settings.openai_api_key, model=settings.openai_model)
    if settings.llm_mode == "remote":
        return RemoteAdapter(url=settings.llm_remote_url, token=settings.llm_remote_token)
    return LocalAdapter()

# Type alias for clean injection
AdapterDependency = Annotated[LLMAdapter, Depends(get_adapter)]

6. Update /chat endpoint

File: app/main.py

from app.llm.adapter import AdapterDependency
from app.llm.exceptions import LLMError, llm_exception_to_http

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, adapter: AdapterDependency) -> ChatResponse:
    # ... validation ...
    try:
        response_text = await adapter.generate(conversation_id, request.message)
    except LLMError as e:
        raise llm_exception_to_http(e)
    # ... return response ...

7. Update mode type

File: app/schemas.py

  • Change mode: Literal["local", "remote"] to mode: Literal["local", "remote", "openai"]

8. Update environment examples

File: .env.example

LLM_MODE=openai
OPENAI_API_KEY=sk-your-key-here
OPENAI_MODEL=gpt-4o-mini

9. Update Dockerfile

File: Dockerfile

  • Add ENV OPENAI_MODEL=gpt-4o-mini (API key passed at runtime for security)

Error Handling

OpenAI Exception Custom Exception HTTP Status
AuthenticationError LLMAuthenticationError 401
RateLimitError LLMRateLimitError 429
APIConnectionError LLMConnectionError 503
APIError LLMError varies

Verification Steps

  1. Install dependencies:

    pip install -r requirements.txt
    
  2. Run locally with OpenAI:

    export LLM_MODE=openai
    export OPENAI_API_KEY=sk-your-key
    uvicorn app.main:app --reload
    
  3. Test the /chat endpoint:

    curl -X POST http://localhost:8000/chat \
      -H "Content-Type: application/json" \
      -d '{"message": "Hello, what is 2+2?"}'
    
  4. Verify response contains OpenAI response and mode="openai"

  5. Test Docker build:

    docker build -t tyndale-ai .
    docker run -p 8080:8080 -e LLM_MODE=openai -e OPENAI_API_KEY=sk-your-key tyndale-ai
    
  6. Test error handling (optional): Use invalid API key to verify 401 response