LLM-translator/AGENTS.md

169 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 caching** — `llm_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 stores** — `auth`, `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:
```json
{
"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
```bash
# 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:
```python
@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
```bash
# 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.