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:
parent
11e7675c52
commit
84de9a02c8
|
|
@ -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.
|
||||||
|
|
@ -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=
|
||||||
16
Dockerfile
16
Dockerfile
|
|
@ -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
117
README.md
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Tyndale AI Service
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# LLM adapters
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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=[],
|
||||||
|
)
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue