LLM-translator/AGENTS.md

179 lines
10 KiB
Markdown
Raw Permalink 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 v3 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
│ ├── database.py # SQLite schema, users, sessions, revision persistence
│ └── sessions.py # Persistent session facade + transient progress
├── 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 + server session 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}] — password_plain is migrated on startup
├── requirements.txt # Python deps: fastapi, uvicorn[standard], pyjwt, bcrypt, openai, python-dotenv
├── .env.example # Environment variables template
├── tests/ # Backend API and session regression tests
└── README.md # Setup and operation guide
```
## 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` caches an `AsyncOpenAI` client by alias. Clients are lazily instantiated on first use.
- **Session store** — SQLite-backed, owner-scoped sessions with no automatic TTL deletion. Completed and draft sessions persist across restarts. Streaming progress remains process-local and transient.
- **Session revisions** — the latest previous translation snapshot is saved before an edit or phase rerun overwrites results, allowing one-step restore.
- **Users** — users are stored in SQLite and created by administrators. `users.json` is imported once for compatibility; password changes and deactivation invalidate existing JWTs through `token_version`.
- **JSON extraction** — LLM responses may be wrapped in markdown code blocks. `_extract_json()` strips ```` ```json` ... ```` wrappers before `json.loads()`.
- **Long-text chunking** — phases 1, 3, and 4 process text sequentially near sentence boundaries. Defaults are 1,500 characters per chunk and a 180-second timeout per LLM call; both are configurable by environment variables.
- **Progress feedback** — LLM calls stream response deltas into transient session progress. The frontend polls `/api/sessions/{session_id}/progress` and displays the current chunk and a rolling preview.
- **Malformed Phase 1 JSON** — parsing tolerates fences, surrounding prose, alternate translation keys, missing noun fields, and trailing commas. Remaining failures trigger one JSON-repair call and then a plain-translation fallback.
### Frontend
- **All Vue components use `<script setup lang="ts">`** with Composition API. No Options API.
- **State management via Pinia stores** — `auth`, `translation`, `theme`. Only model preferences, JWT, and theme are persisted to `localStorage`.
- **Server history** — translation history is canonical in SQLite. The frontend keeps only JWT, theme, and model preferences in `localStorage`, and debounces draft changes to the active server session.
- **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 by LLM phases 1, 3, and 4
"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 dependencies are enforced: Phase 2 and 3 require Phase 1, and Phase 4 requires Phase 3. Running Phase 3 automatically submits the current Phase 2 edits.
### 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 && npm run build && cd ..
# 3. Configure LLM servers and initial users
# users.json is imported once; later users are managed in the admin UI
# 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, user_id: str = Depends(get_current_user)):
session = session_store.get(session_id, user_id)
# ... call chat_complete(alias, system_prompt, user_content)
updated = session_store.update(
session_id, {"phaseN_result": result_text}, user_id
)
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** — imported transactionally once into SQLite. Plain-text passwords are accepted only when the file is writable so they can be replaced with bcrypt hashes; Docker deployments should use pre-hashed entries or bootstrap environment variables.
## 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.