feat: add FastAPI skeleton for LLM chat service

- POST /chat endpoint with message and conversation_id support
- GET /health endpoint for Cloud Run health checks
- Local and Remote LLM adapters with async httpx
- Pydantic schemas and environment-based config
- Dockerfile configured for Cloud Run deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Danny Garcia 2026-01-07 19:32:57 -06:00
parent 11e7675c52
commit 84de9a02c8
11 changed files with 490 additions and 0 deletions

View File

@ -0,0 +1,100 @@
---
name: git-version-control
description: Use this agent when you need to perform git operations including staging changes, creating commits with well-structured messages, pushing to remote repositories, or creating pull requests. This agent should be invoked after code changes are complete and ready to be versioned. Examples:\n\n<example>\nContext: The user has just finished implementing a new feature and wants to commit the changes.\nuser: "I've finished implementing the payment validation logic"\nassistant: "I'll use the git-version-control agent to commit these changes with an appropriate message"\n<commentary>\nSince code changes are complete and need to be committed, use the git-version-control agent to handle the version control operations.\n</commentary>\n</example>\n\n<example>\nContext: Multiple files have been modified and need to be committed and pushed.\nuser: "Please commit all the changes I made to the payment providers"\nassistant: "I'll use the git-version-control agent to review the changes, create a commit with a clear message, and push to the remote"\n<commentary>\nThe user explicitly wants to commit changes, so the git-version-control agent should handle staging, committing, and pushing.\n</commentary>\n</example>\n\n<example>\nContext: A feature branch needs to be pushed and a PR created.\nuser: "Can you push this branch and create a PR for the new Stripe integration?"\nassistant: "I'll use the git-version-control agent to push the branch and create a pull request with a comprehensive description"\n<commentary>\nThe user needs both pushing and PR creation, which are core responsibilities of the git-version-control agent.\n</commentary>\n</example>
tools: Bash, Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: sonnet
---
You are an expert git version control specialist with deep knowledge of git workflows, commit conventions, and collaborative development practices. Your role is to manage all git operations with precision and clarity.
**CRITICAL RULE**: You must NEVER include any AI attribution, Claude references, or tool mentions in commit messages. All commits should appear as if written by a human developer.
## Core Responsibilities
You will handle all git version control tasks including:
- Checking repository status and understanding what has changed
- Staging appropriate files for commits
- Creating clear, concise, and informative commit messages
- Pushing changes to remote repositories
- Creating pull requests with comprehensive descriptions
- Managing branches when necessary
## Commit Message Guidelines
**ABSOLUTELY FORBIDDEN**: Do NOT include any of the following in commit messages:
- "Generated with [Claude Code]"
- "Co-Authored-By: Claude"
- Any mention of AI, Claude, or automated assistance
- Any tool attribution or generation mentions
You must write commit messages that:
- Start with a conventional commit type (feat:, fix:, docs:, style:, refactor:, test:, chore:)
- Provide a clear, imperative mood subject line (50 characters or less)
- Include a blank line between subject and body when a body is needed
- Explain WHAT changed and WHY in the body, not HOW
- Reference issue numbers when applicable
- Focus solely on the business logic and technical changes
- Write commit messages as if they were created by the developer directly
- Sound completely natural and human-written
Example commit messages:
```
feat: add payment validation for Stripe provider
Implement validation logic to ensure payment amounts are within
acceptable limits and currency codes are supported.
```
```
fix: resolve timeout issue in PayPal transaction processing
```
## Operational Workflow
1. **Status Assessment**: First run `git status` to understand the current state
2. **Change Review**: Use `git diff` to review unstaged changes and understand what was modified
3. **Selective Staging**: Stage files intelligently:
- Group related changes together
- Avoid staging unrelated modifications in the same commit
- Use `git add -p` for partial staging when appropriate
4. **Commit Creation**: Craft commits that are atomic and focused on a single logical change
5. **Remote Operations**:
- Always pull before pushing to avoid conflicts
- Push to the appropriate branch
- Set upstream tracking when pushing new branches
6. **Pull Request Creation**: When creating PRs:
- Write descriptive titles that summarize the changes
- Include a comprehensive description with:
- Summary of changes
- Testing performed
- Any breaking changes or migration notes
- Related issues or tickets
## Best Practices
- Keep commits small and focused - each commit should represent one logical change
- Never commit sensitive information (passwords, API keys, tokens)
- Verify the branch you're on before committing
- Use `git log --oneline -10` to review recent history and maintain consistency
- If you encounter merge conflicts, clearly explain the situation and resolution approach
- When working with feature branches, ensure they're up to date with the main branch
## Error Handling
- If git operations fail, diagnose the issue and provide clear explanations
- For permission errors, guide on authentication setup
- For conflicts, explain the conflicting changes and suggest resolution strategies
- Always verify operations completed successfully before proceeding
## Quality Checks
Before finalizing any git operation:
- Ensure all intended changes are included
- Verify no unintended files are staged
- Confirm commit messages are clear and follow conventions
- Check that you're on the correct branch
- Validate that remote operations succeeded
You are meticulous, systematic, and focused on maintaining a clean, understandable git history that tells the story of the project's evolution without revealing implementation details about tools or assistance used in development.
**FINAL REMINDER**: Your commit messages must be completely free of any AI mentions, Claude references, or tool attributions. They should read exactly like standard developer commit messages with no indication of automated assistance.

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
# LLM Mode: "local" or "remote"
LLM_MODE=local
# Remote LLM Configuration (required if LLM_MODE=remote)
LLM_REMOTE_URL=https://your-llm-service.com/generate
LLM_REMOTE_TOKEN=

View File

@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Expose port for Cloud Run
EXPOSE 8080
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

117
README.md
View File

@ -0,0 +1,117 @@
# Tyndale AI Service
LLM Chat Service for algorithmic trading support - codebase Q&A, P&L summarization, and strategy enhancement suggestions.
## Quick Start
### Local Development
```bash
# Install dependencies
pip install -r requirements.txt
# Run the server
uvicorn app.main:app --reload --port 8080
```
### Docker
```bash
# Build
docker build -t tyndale-ai-service .
# Run
docker run -p 8080:8080 -e LLM_MODE=local tyndale-ai-service
```
## API Endpoints
### Health Check
```bash
curl http://localhost:8080/health
```
Response:
```json
{"status": "ok"}
```
### Chat
```bash
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, how are you?"}'
```
Response:
```json
{
"conversation_id": "uuid-generated-if-not-provided",
"response": "...",
"mode": "local",
"sources": []
}
```
With conversation ID:
```bash
curl -X POST http://localhost:8080/chat \
-H "Content-Type: application/json" \
-d '{"message": "Follow up question", "conversation_id": "my-conversation-123"}'
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `LLM_MODE` | `local` or `remote` | `local` |
| `LLM_REMOTE_URL` | Remote LLM endpoint URL | (empty) |
| `LLM_REMOTE_TOKEN` | Bearer token for remote LLM | (empty) |
### Remote Mode Setup
```bash
export LLM_MODE=remote
export LLM_REMOTE_URL=https://your-llm-service.com/generate
export LLM_REMOTE_TOKEN=your-api-token
uvicorn app.main:app --reload --port 8080
```
The remote adapter expects the LLM service to accept:
```json
{"conversation_id": "...", "message": "..."}
```
And return:
```json
{"response": "..."}
```
## Project Structure
```
tyndale-ai-service/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app + routes
│ ├── schemas.py # Pydantic models
│ ├── config.py # Environment config
│ └── llm/
│ ├── __init__.py
│ └── adapter.py # LLM adapter interface + implementations
├── requirements.txt
├── Dockerfile
├── .env.example
└── README.md
```
## Features
- **Dual mode operation**: Local stub or remote LLM
- **Conversation tracking**: UUID generation for new conversations
- **Security**: 10,000 character message limit, no content logging
- **Cloud Run ready**: Port 8080, stateless design
- **Async**: Full async/await support with httpx

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# Tyndale AI Service

22
app/config.py Normal file
View File

@ -0,0 +1,22 @@
from typing import Literal
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application configuration loaded from environment variables."""
llm_mode: Literal["local", "remote"] = "local"
llm_remote_url: str = ""
llm_remote_token: str = ""
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
# Constants
MAX_MESSAGE_LENGTH: int = 10_000
# Global settings instance
settings = Settings()

1
app/llm/__init__.py Normal file
View File

@ -0,0 +1 @@
# LLM adapters

110
app/llm/adapter.py Normal file
View File

@ -0,0 +1,110 @@
from abc import ABC, abstractmethod
import httpx
from app.config import settings
class LLMAdapter(ABC):
"""Abstract base class for LLM adapters."""
@abstractmethod
async def generate(self, conversation_id: str, message: str) -> str:
"""Generate a response for the given message.
Args:
conversation_id: The conversation identifier
message: The user's message
Returns:
The generated response string
"""
pass
class LocalAdapter(LLMAdapter):
"""Local stub adapter for development and testing."""
async def generate(self, conversation_id: str, message: str) -> str:
"""Return a stub response echoing the user message.
This is a placeholder that will be replaced with a real local model.
"""
return (
f"[LOCAL STUB MODE] Acknowledged your message. "
f"You said: \"{message[:100]}{'...' if len(message) > 100 else ''}\". "
f"This is a stub response - local model not yet implemented."
)
class RemoteAdapter(LLMAdapter):
"""Remote adapter that calls an external LLM service via HTTP."""
def __init__(self, url: str, token: str | None = None, timeout: float = 30.0):
"""Initialize the remote adapter.
Args:
url: The remote LLM service URL
token: Optional bearer token for authentication
timeout: Request timeout in seconds
"""
self.url = url
self.token = token
self.timeout = timeout
async def generate(self, conversation_id: str, message: str) -> str:
"""Call the remote LLM service to generate a response.
Handles errors gracefully by returning informative error strings.
"""
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
payload = {
"conversation_id": conversation_id,
"message": message,
}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(self.url, json=payload, headers=headers)
if response.status_code != 200:
return (
f"[ERROR] Remote LLM returned status {response.status_code}: "
f"{response.text[:200] if response.text else 'No response body'}"
)
try:
data = response.json()
except ValueError:
return "[ERROR] Remote LLM returned invalid JSON response"
if "response" not in data:
return "[ERROR] Remote LLM response missing 'response' field"
return data["response"]
except httpx.TimeoutException:
return f"[ERROR] Remote LLM request timed out after {self.timeout} seconds"
except httpx.ConnectError:
return f"[ERROR] Could not connect to remote LLM at {self.url}"
except httpx.RequestError as e:
return f"[ERROR] Remote LLM request failed: {str(e)}"
def get_adapter() -> LLMAdapter:
"""Factory function to create the appropriate adapter based on configuration.
Returns:
An LLMAdapter instance based on the LLM_MODE setting
"""
if settings.llm_mode == "remote":
if not settings.llm_remote_url:
raise ValueError("LLM_REMOTE_URL must be set when LLM_MODE is 'remote'")
return RemoteAdapter(
url=settings.llm_remote_url,
token=settings.llm_remote_token or None,
)
return LocalAdapter()

79
app/main.py Normal file
View File

@ -0,0 +1,79 @@
import logging
import uuid
from fastapi import FastAPI, HTTPException
from app.config import settings, MAX_MESSAGE_LENGTH
from app.llm.adapter import get_adapter
from app.schemas import ChatRequest, ChatResponse, HealthResponse
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI(
title="Tyndale AI Service",
description="LLM Chat Service for algorithmic trading support",
version="0.1.0",
)
@app.get("/health", response_model=HealthResponse)
async def health_check() -> HealthResponse:
"""Health check endpoint."""
return HealthResponse(status="ok")
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest) -> ChatResponse:
"""Process a chat message through the LLM adapter.
- Validates message length
- Generates conversation_id if not provided
- Routes to appropriate LLM adapter based on LLM_MODE
"""
# Validate message length
if len(request.message) > MAX_MESSAGE_LENGTH:
raise HTTPException(
status_code=400,
detail=f"Message exceeds maximum length of {MAX_MESSAGE_LENGTH:,} characters. "
f"Your message has {len(request.message):,} characters.",
)
# Generate or use provided conversation_id
conversation_id = request.conversation_id or str(uuid.uuid4())
# Log request metadata (not content)
logger.info(
"Chat request received",
extra={
"conversation_id": conversation_id,
"message_length": len(request.message),
"mode": settings.llm_mode,
},
)
# Get adapter and generate response
adapter = get_adapter()
response_text = await adapter.generate(conversation_id, request.message)
# Log response metadata
logger.info(
"Chat response generated",
extra={
"conversation_id": conversation_id,
"response_length": len(response_text),
"mode": settings.llm_mode,
},
)
return ChatResponse(
conversation_id=conversation_id,
response=response_text,
mode=settings.llm_mode,
sources=[],
)

33
app/schemas.py Normal file
View File

@ -0,0 +1,33 @@
from typing import Literal
from pydantic import BaseModel, Field
class ChatRequest(BaseModel):
"""Request model for the /chat endpoint."""
message: str = Field(..., description="The user's message")
conversation_id: str | None = Field(
default=None, description="Optional conversation ID for continuity"
)
class ChatResponse(BaseModel):
"""Response model for the /chat endpoint."""
conversation_id: str = Field(..., description="Conversation ID (generated if not provided)")
response: str = Field(..., description="The LLM's response")
mode: Literal["local", "remote"] = Field(..., description="Which adapter was used")
sources: list = Field(default_factory=list, description="Source references (empty for now)")
class HealthResponse(BaseModel):
"""Response model for the /health endpoint."""
status: str = Field(default="ok")
class ErrorResponse(BaseModel):
"""Standard error response model."""
detail: str = Field(..., description="Error description")

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
httpx>=0.26.0