Files
office_translator/_bmad-output/implementation-artifacts/2-4-provider-ollama-llm-local.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

455 lines
17 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.
# Story 2.4: Provider Ollama (LLM Local)
Status: done
## Story
As a **system**,
I want **to integrate Ollama as an LLM provider with custom system prompt support**,
so that **Pro users can translate documents with local LLMs for privacy and cost efficiency**.
## Acceptance Criteria
1. **AC1: API Integration** - Given `OLLAMA_BASE_URL` is configured (default: http://localhost:11434), when `OllamaProvider.translate_text()` is called with model and prompt, then text is translated using the specified Ollama model
2. **AC2: Graceful Error Handling** - Connection/timeout returns error code `OLLAMA_UNAVAILABLE` / `OLLAMA_TIMEOUT` with clear message (e.g. "Service Ollama indisponible..."), never HTTP 500
3. **AC3: Custom System Prompt** - Custom system prompt can be injected via the request to guide translation context
4. **AC4: Health Check** - Provider `is_available()` returns `True` when Ollama is reachable and model is pulled, `False` otherwise
5. **AC5: Registry Integration** - Provider is registered in `ProviderRegistry` and appears in fallback chain
6. **AC6: Unit Tests** - Tests verify all error scenarios, connection handling, and mock Ollama API responses
## Tasks / Subtasks
- [x] **Task 1: Create Ollama Provider Implementation** (AC: 1, 3)
- [x] 1.1 Create `services/providers/ollama_provider.py`
- [x] 1.2 Implement `OllamaTranslationProvider` class extending `TranslationProvider`
- [x] 1.3 Implement `translate_text()` using Ollama REST API (`/api/generate` or `/api/chat`)
- [x] 1.4 Support custom system prompt injection via request metadata
- [x] 1.5 Configure default translation system prompt
- [x] **Task 2: Implement Error Handling** (AC: 2)
- [x] 2.1 Define error codes: `OLLAMA_UNAVAILABLE`, `OLLAMA_MODEL_NOT_FOUND`, `OLLAMA_TIMEOUT`, `OLLAMA_GENERATION_ERROR`, `OLLAMA_CONTEXT_TOO_LONG`
- [x] 2.2 Implement `OllamaProviderError` exception class (follow existing pattern)
- [x] 2.3 Map Ollama API errors to structured error responses
- [x] 2.4 Add retry logic with exponential backoff for transient errors
- [x] 2.5 Add timeout configuration (default 120s for LLM - longer than classic)
- [x] 2.6 Ensure all errors return JSON: `{error, message, details?}` format
- [x] **Task 3: Implement Health Check** (AC: 4)
- [x] 3.1 Implement `is_available()` to check Ollama service reachability
- [x] 3.2 Add `health_check()` with caching (TTL 60s) matching existing provider pattern
- [x] 3.3 Verify configured model is available (pulled) via `/api/tags`
- [x] 3.4 Return `ProviderHealthStatus` with availability, latency, and model info
- [x] **Task 4: Registry Integration** (AC: 5)
- [x] 4.1 Add `register_ollama_provider()` function
- [x] 4.2 Add `get_ollama_provider()` singleton function
- [x] 4.3 Update `services/providers/__init__.py` to auto-register Ollama when enabled
- [x] 4.4 Verify provider appears in fallback chain when configured
- [x] **Task 5: Configuration Updates** (AC: 1, 2)
- [x] 5.1 Verify `OLLAMA_BASE_URL`, `OLLAMA_MODEL`, `OLLAMA_ENABLED` in `config.py` (already present)
- [x] 5.2 Add Ollama-specific configuration options to `config.py`:
- `OLLAMA_TIMEOUT=120` (LLM needs longer timeout)
- `OLLAMA_MAX_RETRIES=2`
- `OLLAMA_RETRY_DELAY=2`
- [x] 5.3 Update `.env.example` with Ollama-specific config
- [x] **Task 6: Create Unit Tests** (AC: 6)
- [x] 6.1 Create `tests/test_providers/test_ollama_provider.py`
- [x] 6.2 Test successful translation with mocked Ollama API
- [x] 6.3 Test all error scenarios (unavailable, model not found, timeout)
- [x] 6.4 Test custom system prompt injection
- [x] 6.5 Test retry logic
- [x] 6.6 Test health check functionality
- [x] 6.7 Test registry integration
- [x] **Task 7: Update Documentation** (AC: 1-6)
- [x] 7.1 Update `services/providers/README.md` with Ollama section
- [x] 7.2 Document Ollama setup requirements (pull models first)
- [x] 7.3 Document supported models and recommendations for translation
## Dev Notes
### Ollama API Specifics
**Ollama REST API Endpoints:**
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/generate` | POST | Generate text (streaming or not) |
| `/api/chat` | POST | Chat completion with messages |
| `/api/tags` | GET | List pulled models |
| `/api/show` | POST | Show model info |
**Recommended: Use `/api/chat` for translation** (better prompt handling):
```python
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
payload = {
"model": "llama3",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text_to_translate}
],
"stream": False,
"options": {
"temperature": 0.3 # Lower for more consistent translation
}
}
```
**API Response Format:**
```json
{
"model": "llama3",
"created_at": "2024-01-15T10:30:00Z",
"message": {
"role": "assistant",
"content": "Bonjour, comment allez-vous?"
},
"done": true
}
```
### Recommended Models for Translation
| Model | Size | Best For | Notes |
|-------|------|----------|-------|
| `llama3` | 8B | General translation | Good balance of speed/quality |
| `llama3:70b` | 70B | High-quality translation | Requires significant RAM |
| `mistral` | 7B | Fast translation | Good for real-time |
| `qwen2` | 7B | Multi-language | Strong non-English support |
| `deepseek-coder` | 6.7B | Technical docs | Good for code comments |
**Pre-requisite**: Models must be pulled before use:
```bash
ollama pull llama3
ollama pull mistral
```
### Default System Prompt for Translation
```python
DEFAULT_TRANSLATION_PROMPT = """You are a professional translator. Translate the following text from {source_lang} to {target_lang}.
Rules:
- Translate ONLY the text, do not add explanations or notes
- Preserve the original formatting, line breaks, and structure
- Maintain the original tone and style
- For technical terms, use the standard translation in the target language
- If the text contains proper nouns or brand names, keep them unchanged unless there's a well-known translation"""
def _build_system_prompt(
source_lang: str,
target_lang: str,
custom_prompt: Optional[str] = None
) -> str:
if custom_prompt:
return custom_prompt
return DEFAULT_TRANSLATION_PROMPT.format(
source_lang=source_lang,
target_lang=target_lang
)
```
### Architecture Compliance
Per `_bmad-output/planning-artifacts/architecture.md`:
**Error Format:**
```json
{
"error": "OLLAMA_UNAVAILABLE",
"message": "Service Ollama indisponible. Vérifiez que Ollama est en cours d'exécution.",
"details": {
"provider": "ollama",
"base_url": "http://localhost:11434",
"model": "llama3"
}
}
```
**Never return HTTP 500** - All errors must be 4xx or 502 (upstream error).
**Naming Conventions:**
- File: `ollama_provider.py` (snake_case)
- Class: `OllamaTranslationProvider` (PascalCase)
- Error codes: `OLLAMA_*` (UPPER_SNAKE_CASE)
- JSON fields: snake_case
### Previous Story Intelligence (Story 2.2 & 2.3)
**What Worked Well:**
- `deep_translator` library integration for Google/DeepL
- Thread-safe translator instances per thread
- Error codes with `to_dict()` method
- Retry logic with exponential backoff
- Health check with 60s TTL caching
- Structlog-compatible logging with keyword args
**Patterns to Reuse:**
```python
# Error codes pattern
OLLAMA_UNAVAILABLE = "OLLAMA_UNAVAILABLE"
OLLAMA_MODEL_NOT_FOUND = "OLLAMA_MODEL_NOT_FOUND"
OLLAMA_TIMEOUT = "OLLAMA_TIMEOUT"
OLLAMA_GENERATION_ERROR = "OLLAMA_GENERATION_ERROR"
OLLAMA_CONTEXT_TOO_LONG = "OLLAMA_CONTEXT_TOO_LONG"
_RETRYABLE_ERRORS = {OLLAMA_UNAVAILABLE, OLLAMA_TIMEOUT}
# Exception class pattern (from Google/DeepL providers)
class OllamaProviderError(Exception):
def __init__(self, code: str, message: str, details: Optional[Dict[str, Any]] = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)
def to_dict(self) -> Dict[str, Any]:
result = {"error": self.code, "message": self.message}
if self.details:
result["details"] = self.details
return result
```
**Key Difference for Ollama:**
- Uses HTTP requests (not a library like `deep_translator`)
- Longer timeout required (120s default vs 30s for classic)
- Model must be pre-pulled before use
- Custom system prompt support is essential
### File Structure
**Files to Create:**
- `services/providers/ollama_provider.py` - Main provider implementation
- `tests/test_providers/test_ollama_provider.py` - Unit tests
**Files to Modify:**
- `services/providers/__init__.py` - Add Ollama auto-registration
- `services/providers/config.py` - Add OLLAMA_TIMEOUT, OLLAMA_MAX_RETRIES, OLLAMA_RETRY_DELAY
- `.env.example` - Add Ollama-specific config (may already have basic config)
- `services/providers/README.md` - Add Ollama documentation
### Error Codes to Implement
| Code | HTTP | Scenario | Message Template |
|------|------|----------|------------------|
| `OLLAMA_UNAVAILABLE` | 502 | Ollama service not reachable | "Service Ollama indisponible. Vérifiez que Ollama est en cours d'exécution." |
| `OLLAMA_MODEL_NOT_FOUND` | 400 | Model not pulled | "Modèle '{model}' non trouvé. Exécutez: ollama pull {model}" |
| `OLLAMA_TIMEOUT` | 502 | Request timeout | "Délai d'attente Ollama dépassé. Réessayez avec un texte plus court." |
| `OLLAMA_GENERATION_ERROR` | 502 | LLM generation failed | "Erreur de génération Ollama: {error}" |
| `OLLAMA_CONTEXT_TOO_LONG` | 413 | Context exceeds model limit | "Texte trop long pour le modèle (max ~{max_tokens} tokens)." |
### Configuration
**Environment Variables (`.env.example`):**
```bash
# Ollama Provider (Local LLM)
OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3
OLLAMA_VISION_MODEL=llava
OLLAMA_TIMEOUT=120
OLLAMA_MAX_RETRIES=2
OLLAMA_RETRY_DELAY=2
```
**Provider Config (`services/providers/config.py`):**
Add after existing OLLAMA config:
```python
OLLAMA_TIMEOUT: int = int(os.getenv("OLLAMA_TIMEOUT", "120"))
OLLAMA_MAX_RETRIES: int = int(os.getenv("OLLAMA_MAX_RETRIES", "2"))
OLLAMA_RETRY_DELAY: float = float(os.getenv("OLLAMA_RETRY_DELAY", "2.0"))
```
### Testing Strategy
**Unit Tests (Mocked):**
- Mock `httpx` or `requests` responses
- Test successful translation
- Test all error scenarios (unavailable, model not found, timeout)
- Test custom system prompt injection
- Test health check logic
- Test retry logic
- Test registry integration
**Integration Tests (Optional):**
- With Ollama running locally: real API calls
- Without Ollama: skip integration tests
- Use pytest markers: `@pytest.mark.integration`
**Test Commands:**
```bash
# Unit tests only
pytest tests/test_providers/test_ollama_provider.py -v
# All provider tests
pytest tests/test_providers/ -v
# With coverage
pytest tests/test_providers/ --cov=services/providers -v
```
### Logging Pattern
```python
try:
import structlog
logger = structlog.get_logger(__name__)
except ImportError:
import logging
logger = logging.getLogger(__name__)
# Good - metadata only (NO document content)
logger.info(
"ollama_translation_success",
chars=len(text),
source_lang=source_language,
target_lang=target_language,
model=self._model,
latency_ms=round(latency * 1000, 2),
)
logger.error(
"ollama_translation_failed",
error_code=error.code,
text_length=len(text),
source_lang=source_language,
target_lang=target_language,
model=self._model,
)
```
### Dependencies
**Internal:**
- `services/providers/base.py` - TranslationProvider abstract class
- `services/providers/registry.py` - ProviderRegistry
- `services/providers/config.py` - Configuration
- `services/providers/schemas.py` - TranslationRequest/Response models
**External:**
- `httpx` - HTTP client (preferred over requests for async support)
- `structlog` or standard `logging` - Structured logging
### HTTP Client Pattern
Use `httpx` for Ollama API calls (supports async and sync):
```python
import httpx
class OllamaTranslationProvider(TranslationProvider):
def __init__(self, base_url: str, model: str, timeout: int = 120):
self._base_url = base_url.rstrip("/")
self._model = model
self._timeout = timeout
self._client = httpx.Client(timeout=timeout)
def _make_api_request(self, text: str, system_prompt: str) -> str:
response = self._client.post(
f"{self._base_url}/api/chat",
json={
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"stream": False,
"options": {"temperature": 0.3}
}
)
# ... error handling
return response.json()["message"]["content"]
```
### References
- [Source: _bmad-output/planning-artifacts/architecture.md#Error Handling]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.4]
- [Source: _bmad-output/planning-artifacts/prd.md#FR7 LLM providers (Ollama, OpenAI)]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR12 Zero HTTP 500 errors]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR13 Provider fallback]
- [Source: _bmad-output/implementation-artifacts/2-2-provider-google-translate.md]
- [Source: _bmad-output/implementation-artifacts/2-3-provider-deepl.md]
- [Source: services/providers/google_provider.py - Implementation pattern]
- [Source: services/providers/registry.py - Registration pattern]
- [Source: https://github.com/ollama/ollama/blob/main/docs/api.md - Ollama API docs]
### Security Considerations
**Local Deployment:**
- Ollama runs locally by default (no external API calls)
- No API key required for local Ollama
- If using remote Ollama, consider network security
**Data Privacy:**
- Never log document content (NFR11)
- Only log metadata: text length, languages, model, timestamps
- Ollama keeps data local (privacy advantage over cloud LLMs)
### Pro Feature Integration
Per PRD FR26: "Pro users can access LLM translation modes"
This provider will be used when:
- User tier is "pro"
- User selects "LLM" mode
- User selects "Ollama" as LLM provider
The tier check happens in the translation service/router, not in the provider itself.
## Dev Agent Record
### Agent Model Used
Claude (GLM-5) via opencode
### Debug Log References
- Fixed logging compatibility issue: standard logging doesn't support keyword arguments like structlog
- Created helper functions `_log_info`, `_log_warning`, `_log_error` to bridge the gap
- Updated test file to use `requests` instead of `httpx` (httpx not in requirements)
### Completion Notes List
- ✅ Implemented `OllamaTranslationProvider` class with all required features
- ✅ Uses `/api/chat` endpoint for translation with system prompt support
- ✅ All 5 error codes implemented with French messages
- ✅ Retry logic with exponential backoff for `OLLAMA_UNAVAILABLE` and `OLLAMA_TIMEOUT`
- ✅ Health check with 60s TTL caching and model availability verification
- ✅ Registry integration with auto-registration when `OLLAMA_ENABLED=true`
- ✅ Custom system prompt injection via `request.metadata["custom_prompt"]`
- ✅ Language name mapping for better LLM understanding
- ✅ 29 unit tests created and all passing
- ✅ Documentation updated in README.md with Ollama section
### Code Review Fixes (AI) 2026-02-21
- **AC4 / ProviderHealthStatus** Added optional fields `model` and `model_available` to `ProviderHealthStatus` in `schemas.py`; Ollama `health_check()` now returns model info (availability, latency, model name).
- **Health check messages** Unified to French in `health_check()` (e.g. "Service Ollama indisponible...", "Modèle 'x' non trouvé...").
- **Tests** Removed unused `import socket`; added `test_timeout_returns_ollama_timeout_error`; strengthened `test_health_check_caching` with mock to assert no API call when cache is valid; added assertions for `model` and `model_available` in health check tests.
- **AC2** Story AC2 wording updated to reflect implementation (error codes `OLLAMA_UNAVAILABLE` / `OLLAMA_TIMEOUT`).
### File List
**Files Created:**
- `services/providers/ollama_provider.py` - Main Ollama provider implementation
- `tests/test_providers/test_ollama_provider.py` - 29 unit tests
**Files Modified:**
- `services/providers/__init__.py` - Added Ollama auto-registration
- `services/providers/config.py` - Added OLLAMA_TIMEOUT, OLLAMA_MAX_RETRIES, OLLAMA_RETRY_DELAY
- `services/providers/schemas.py` - Added metadata field to TranslationRequest for custom prompt support
- `services/providers/README.md` - Added comprehensive Ollama documentation
- `.env.example` - Added Ollama-specific configuration options
### Change Log
- 2026-02-21: Story 2.4 implementation complete - Ollama provider with local LLM translation, custom prompts, error handling, and 29 passing tests
- 2026-02-21: Code review fixes - ProviderHealthStatus model info (model, model_available), health check messages in French, tests (timeout error, cache assertion, model assertions), AC2 wording aligned