LLM-translator/AGENTS.md

8.7 KiB
Raw Blame History

LLM Translator — Agent Guide

Project Overview

A 4-phase translation pipeline web app. Users input source text, then sequentially execute:

  1. Phase 1 (Rough Translation) — LLM translates + extracts proper nouns, summary, style
  2. Phase 2 (Proper Noun Review) — User reviews/edits extracted proper nouns and style
  3. Phase 3 (Re-translation) — LLM re-translates with proper noun/style constraints applied
  4. Phase 4 (Polish) — LLM polishes for readability while preserving proper nouns

Architecture: FastAPI backend + Vue 3 SPA (Vite build, served as static files by FastAPI). Single-server deployment: uvicorn backend.main:app serves both API and frontend.

Tech Stack

Layer Technology
Backend Python 3.14+, FastAPI, Uvicorn
Auth JWT (PyJWT), bcrypt (direct via bcrypt module)
LLM Client OpenAI SDK (openai) — OpenAI-compatible API format
Frontend Vue 3 + Composition API <script setup>, TypeScript, Pinia
Styling Tailwind CSS v4 with darkMode: 'class' strategy
Build Vite (frontend), no bundler for backend

Directory Structure

LLM-translator/
├── backend/                   # FastAPI application
│   ├── main.py                # App entry: routes, CORS, static file mounting
│   ├── auth.py                # JWT auth + bcrypt password verification
│   ├── llm_client.py          # OpenAI-compatible client (async)
│   ├── models.py              # Pydantic schemas for all API I/O
│   ├── prompts.py             # Phase 14 prompt templates
│   └── sessions.py            # In-memory session store (TTL 24h, UUID keys)
├── frontend/                  # Vue 3 SPA source
│   ├── src/
│   │   ├── main.ts            # App entry + dark mode pre-mount class application
│   │   ├── App.vue            # Root: LoginView | TranslatorView based on auth state
│   │   ├── views/
│   │   │   ├── LoginView.vue  # ID/PW login form → localStorage JWT
│   │   │   └── TranslatorView.vue  # Main UI: model selectors, phase buttons, comparison
│   │   ├── components/
│   │   │   ├── LLMSelector.vue        # Model dropdown (dark mode aware)
│   │   │   ├── ProperNounsEditor.vue  # Proper noun table + style input
│   │   │   └── ComparisonView.vue     # 3-column Phase 1/3/4 result comparison
│   │   ├── stores/
│   │   │   ├── auth.ts          # JWT state (check/login/logout)
│   │   │   ├── translation.ts   # Pipeline state + localStorage persistence
│   │   │   └── theme.ts         # Dark mode toggle + localStorage + system preference detection
│   │   ├── api.ts               # Axios client with JWT interceptor
│   │   └── types.ts             # TypeScript interfaces mirroring backend models
│   ├── tailwind.config.js       # darkMode: 'class' configured here
│   ├── vite.config.ts           # Dev proxy to /api → localhost:8000; build output → dist/
│   └── package.json
├── config/
│   ├── llms.json                # LLM server configs [{alias, base_url, api_key, model, context_size}]
│   └── users.json               # Users [{id, password_hash}] — hashed on first startup from plain-text
├── requirements.txt             # Python deps: fastapi, uvicorn[standard], pyjwt, bcrypt, openai, python-dotenv
├── .env.example                 # Environment variables template
└── README.md                    # (TBD)

Key Patterns & Conventions

Backend

  • No passlib — use bcrypt module directly (bcrypt.hashpw, bcrypt.checkpw). The project uses Python 3.14 which has compatibility issues with passlib's bcrypt wrapper.
  • LLM client cachingllm_client.py maintains a per-alias {AsyncOpenAI, LLMConfig} cache dict keyed by alias string. Clients are lazily instantiated on first use.
  • Session store — simple in-memory dict (sessions.py). Key is UUID hex (16 chars). TTL 24h from last access. No persistence beyond process lifetime.
  • JSON extraction — LLM responses may be wrapped in markdown code blocks. _extract_json() strips ```json` ... wrappers before json.loads().

Frontend

  • All Vue components use <script setup lang="ts"> with Composition API. No Options API.
  • State management via Pinia storesauth, translation, theme. Stores persist model selections and history to localStorage.
  • Dark mode is class-based (<html class="dark">). Applied in main.ts before mount to prevent flash, then toggled by useThemeStore.toggle(). Every UI component has corresponding dark: Tailwind classes.
  • API calls go through /api proxy during dev (vite.config.ts). In production the same origin serves both API and static files.

LLM Configuration

Models are defined in config/llms.json. Each entry:

{
  "alias": "Qwen3.6-35B",      // Display name, used as modelPhase1..4 values
  "base_url": "http://...",     // OpenAI-compatible base URL (must end with /v1)
  "api_key": "...",             // API key for this server
  "model": "hx370/qwen3.6-35b-a3b",  // Exact model name as used by the LLM provider
  "context_size": 65536         // Token context window size (for length warnings)
}

Translation Pipeline Data Flow

source_text ──► Phase1(LLM) ──► {translated, proper_nouns[], summary, style}
                                    │
                                    ▼ user edits in ProperNounsEditor
                              phase2_proper_nouns[], phase2_style
                                    │
                                    ▼ Phase3(LLM): re-translate with constraints
                              phase3_result
                                    │
                                    ▼ Phase4(LLM): polish for readability
                              phase4_result (final)

Phase buttons are always freely executable — user can jump to any phase independently. Dependencies are only logical: P2 needs P1 data, P3 needs P1 result, P4 needs P3 result.

Theme / Dark Mode

  • theme.ts store manages state in Pinia + localStorage. On init, checks saved value or falls back to system preference (prefers-color-scheme).
  • Every component with background/border/text colors must include matching dark: variants.
  • Default light theme: white/slate backgrounds, blue accents. Dark theme: gray-800/900 backgrounds, adjusted borders, preserved accent colors (blue, purple, emerald remain readable).

Running the Project

# 1. Install dependencies
pip install -r requirements.txt
cd frontend && npm install && cd ..

# 2. Build frontend (required before serving in production mode)
cd frontend && npx vite build && cd ..

# 3. Configure LLM servers and users
#    Edit config/llms.json and config/users.json directly

# 4. Start server
uvicorn backend.main:app --host 0.0.0.0 --port 8000
# Or with env vars for custom paths:
JWT_SECRET_KEY=your-secret uvicorn backend.main:app --reload

# 5. Open http://localhost:8000

Default login: admin / changeme123. First startup auto-hashes plain-text passwords in users.json and saves the file.

Adding a New Phase or Modifying Prompts

  1. Edit backend/prompts.py: add new prompt templates using f-strings with source data parameters
  2. Add API endpoint in backend/main.py following the existing pattern:
    @app.post("/api/translate/{session_id}/phaseN")
    async def run_phaseN(session_id: str):
        session = session_store.get(session_id)
        # ... call chat_complete(alias, system_prompt, user_content)
        updated = session_store.update(session_id, {"phaseN_result": result_text})
        return SessionResponse(session_id=session_id, data=updated).model_dump()
    
  3. Add frontend component and button in TranslatorView.vue
  4. Update translation.ts store with new execution method + state

File I/O Notes

  • config/llms.json — loaded once at startup by llm_client.py._load_llm_configs(). Not re-read on requests. Changes require server restart.
  • config/users.json — loaded once at startup by auth.py._load_users_file(). Plain-text passwords are auto-migrated to bcrypt hashes and saved back on first run only.

Testing Against LLM Server

# Quick connectivity test (use exact model name from llms.json)
curl -s http://btuna.net:45455/v1/chat/completions \
  -H "Authorization: Bearer btuna-public-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"hx370/qwen3.6-35b-a3b","messages":[{"role":"user","content":"test"}],"max_tokens":10}'

If you get PermissionDeniedError, the API key lacks access to that model — try a different model or contact server admin for correct permissions.