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

17 KiB
Raw Blame History

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

  • Task 1: Create Ollama Provider Implementation (AC: 1, 3)

    • 1.1 Create services/providers/ollama_provider.py
    • 1.2 Implement OllamaTranslationProvider class extending TranslationProvider
    • 1.3 Implement translate_text() using Ollama REST API (/api/generate or /api/chat)
    • 1.4 Support custom system prompt injection via request metadata
    • 1.5 Configure default translation system prompt
  • Task 2: Implement Error Handling (AC: 2)

    • 2.1 Define error codes: OLLAMA_UNAVAILABLE, OLLAMA_MODEL_NOT_FOUND, OLLAMA_TIMEOUT, OLLAMA_GENERATION_ERROR, OLLAMA_CONTEXT_TOO_LONG
    • 2.2 Implement OllamaProviderError exception class (follow existing pattern)
    • 2.3 Map Ollama API errors to structured error responses
    • 2.4 Add retry logic with exponential backoff for transient errors
    • 2.5 Add timeout configuration (default 120s for LLM - longer than classic)
    • 2.6 Ensure all errors return JSON: {error, message, details?} format
  • Task 3: Implement Health Check (AC: 4)

    • 3.1 Implement is_available() to check Ollama service reachability
    • 3.2 Add health_check() with caching (TTL 60s) matching existing provider pattern
    • 3.3 Verify configured model is available (pulled) via /api/tags
    • 3.4 Return ProviderHealthStatus with availability, latency, and model info
  • Task 4: Registry Integration (AC: 5)

    • 4.1 Add register_ollama_provider() function
    • 4.2 Add get_ollama_provider() singleton function
    • 4.3 Update services/providers/__init__.py to auto-register Ollama when enabled
    • 4.4 Verify provider appears in fallback chain when configured
  • Task 5: Configuration Updates (AC: 1, 2)

    • 5.1 Verify OLLAMA_BASE_URL, OLLAMA_MODEL, OLLAMA_ENABLED in config.py (already present)
    • 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
    • 5.3 Update .env.example with Ollama-specific config
  • Task 6: Create Unit Tests (AC: 6)

    • 6.1 Create tests/test_providers/test_ollama_provider.py
    • 6.2 Test successful translation with mocked Ollama API
    • 6.3 Test all error scenarios (unavailable, model not found, timeout)
    • 6.4 Test custom system prompt injection
    • 6.5 Test retry logic
    • 6.6 Test health check functionality
    • 6.7 Test registry integration
  • Task 7: Update Documentation (AC: 1-6)

    • 7.1 Update services/providers/README.md with Ollama section
    • 7.2 Document Ollama setup requirements (pull models first)
    • 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):

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:

{
  "model": "llama3",
  "created_at": "2024-01-15T10:30:00Z",
  "message": {
    "role": "assistant",
    "content": "Bonjour, comment allez-vous?"
  },
  "done": true
}
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:

ollama pull llama3
ollama pull mistral

Default System Prompt for Translation

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:

{
  "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:

# 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):

# 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:

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:

# 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

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):

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