# Story 2.1: Abstraction Provider (Base + Registry)

Status: done

## Story

As a **Developer**,
I want **to create an abstract TranslationProvider base class with a ProviderRegistry**,
so that **multiple translation providers can be plugged in with fallback support and clean architecture compliance**.

## Acceptance Criteria

1. **AC1: Abstract Base Class** - `TranslationProvider` defines abstract methods: `translate_text()`, `get_name()`, `is_available()`
2. **AC2: ProviderRegistry** - Registry can register, retrieve, and list providers by name
3. **AC3: Environment Configuration** - Providers configured via environment variables (API keys, URLs)
4. **AC4: Health Check** - Each provider has `is_available()` method returning `True`/`False`
5. **AC5: Unit Tests** - Tests verify provider interface and registry functionality

## Tasks / Subtasks

- [x] **Task 1: Create Provider Base Class** (AC: 1, 4)
  - [x] 1.1 Create `services/providers/__init__.py`
  - [x] 1.2 Create `services/providers/base.py` with abstract `TranslationProvider`
  - [x] 1.3 Define abstract methods: `translate_text()`, `get_name()`, `is_available()`
  - [x] 1.4 Add optional methods: `translate_batch()`, `health_check()`
  - [x] 1.5 Add Pydantic models for request/response in `services/providers/schemas.py`

- [x] **Task 2: Create Provider Registry** (AC: 2)
  - [x] 2.1 Create `services/providers/registry.py` with `ProviderRegistry` class
  - [x] 2.2 Implement `register(name, provider_class)` method
  - [x] 2.3 Implement `get(name) -> TranslationProvider` method
  - [x] 2.4 Implement `list_available() -> List[str]` method
  - [x] 2.5 Implement `get_first_available(names: List[str]) -> TranslationProvider` for fallback
  - [x] 2.6 Add singleton pattern for global registry

- [x] **Task 3: Migrate Existing Google Provider** (AC: 1, 3, 4)
  - [x] 3.1 Create `services/providers/google_provider.py`
  - [x] 3.2 Extend `TranslationProvider` base class
  - [x] 3.3 Implement `is_available()` using a simple ping/test
  - [x] 3.4 Migrate caching logic from existing `GoogleTranslationProvider`
  - [x] 3.5 Register in registry on import

- [x] **Task 4: Environment Configuration** (AC: 3)
  - [x] 4.1 Add provider settings to `config.py` (or create `services/providers/config.py`)
  - [x] 4.2 Load API keys from environment: `GOOGLE_API_KEY`, `DEEPL_API_KEY`, `OPENAI_API_KEY`, `OLLAMA_BASE_URL`
  - [x] 4.3 Add provider enable/disable flags
  - [x] 4.4 Update `.env.example` with new variables

- [x] **Task 5: Unit Tests** (AC: 5)
  - [x] 5.1 Create `tests/test_providers/test_base.py`
  - [x] 5.2 Create `tests/test_providers/test_registry.py`
  - [x] 5.3 Create `tests/test_providers/test_google_provider.py`
  - [x] 5.4 Test abstract class cannot be instantiated directly
  - [x] 5.5 Test registry registration and retrieval
  - [x] 5.6 Test fallback chain logic

## Dev Notes

### 🚨 BROWNFIELD CONTEXT - CRITICAL

This is a **refactoring story**. The project already has provider implementations in `services/translation_service.py`:
- `TranslationProvider` (ABC) - lines 116-157
- `GoogleTranslationProvider` - lines 159-282
- `DeepLTranslationProvider` - lines 285-340
- `OllamaTranslationProvider` - lines 397-517
- `OpenAITranslationProvider` - lines 667-767
- `OpenRouterTranslationProvider` - lines 520-654

**Strategy:** Extract and reorganize into clean module structure, keeping backward compatibility.

### Target Module Structure (from Architecture)

```
services/
├── providers/
│   ├── __init__.py          # Exports + registry setup
│   ├── base.py              # Abstract TranslationProvider
│   ├── registry.py          # ProviderRegistry singleton
│   ├── schemas.py           # Pydantic request/response models
│   ├── config.py            # Provider settings from env
│   └── google_provider.py   # Google implementation (migrated)
├── translation_service.py   # Keep for now, refactor later
└── ...
```

### Existing Code to Preserve

**From `services/translation_service.py`:**

| Component | Lines | Action |
|-----------|-------|--------|
| `TranslationCache` | 53-113 | Keep in `services/translation_service.py` (used by all providers) |
| `retry_with_backoff` | 27-50 | Keep as utility |
| `TranslationProvider` (ABC) | 116-157 | Refactor to `providers/base.py` |
| `GoogleTranslationProvider` | 159-282 | Migrate to `providers/google_provider.py` |
| Other providers | Various | Migrate in subsequent stories (2.2-2.5) |

### Abstract Base Class Design

```python
# services/providers/base.py
from abc import ABC, abstractmethod
from typing import Optional, List
from pydantic import BaseModel

class TranslationRequest(BaseModel):
    text: str
    target_language: str
    source_language: str = "auto"

class TranslationResponse(BaseModel):
    translated_text: str
    provider_name: str
    from_cache: bool = False

class TranslationProvider(ABC):
    """Abstract base class for translation providers."""
    
    @abstractmethod
    def translate_text(self, request: TranslationRequest) -> TranslationResponse:
        """Translate a single text string."""
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        """Return provider name for logging and registry."""
        pass
    
    @abstractmethod
    def is_available(self) -> bool:
        """Check if provider is configured and reachable."""
        pass
    
    def translate_batch(self, requests: List[TranslationRequest]) -> List[TranslationResponse]:
        """Default batch implementation using individual calls."""
        return [self.translate_text(req) for req in requests]
    
    def health_check(self) -> dict:
        """Return health status details."""
        return {
            "name": self.get_name(),
            "available": self.is_available()
        }
```

### Provider Registry Design

```python
# services/providers/registry.py
from typing import Dict, List, Optional, Type
from .base import TranslationProvider

class ProviderRegistry:
    """Singleton registry for translation providers."""
    
    _instance: Optional['ProviderRegistry'] = None
    
    def __new__(cls) -> 'ProviderRegistry':
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._providers: Dict[str, TranslationProvider] = {}
        return cls._instance
    
    def register(self, name: str, provider: TranslationProvider) -> None:
        self._providers[name] = provider
    
    def get(self, name: str) -> Optional[TranslationProvider]:
        return self._providers.get(name)
    
    def list_available(self) -> List[str]:
        return [name for name, p in self._providers.items() if p.is_available()]
    
    def get_first_available(self, names: List[str]) -> Optional[TranslationProvider]:
        """Get first available provider from a list of names (fallback chain)."""
        for name in names:
            provider = self.get(name)
            if provider and provider.is_available():
                return provider
        return None

# Global registry instance
registry = ProviderRegistry()
```

### Environment Variables

```bash
# .env.example additions
GOOGLE_TRANSLATE_ENABLED=true
DEEPL_ENABLED=false
DEEPL_API_KEY=
OPENAI_ENABLED=false
OPENAI_API_KEY=
OLLAMA_ENABLED=false
OLLAMA_BASE_URL=http://localhost:11434
OPENROUTER_ENABLED=false
OPENROUTER_API_KEY=

# Provider fallback chain (comma-separated)
PROVIDER_FALLBACK_CHAIN=google,deepl,ollama,openrouter
```

### Backward Compatibility

During transition, keep existing `TranslationService` in `services/translation_service.py` working:

```python
# services/translation_service.py (modified)
from services.providers.registry import registry
from services.providers.google_provider import GoogleTranslationProvider

# For backward compatibility, existing code continues to work
# New code uses registry.get("google").translate_text(...)
```

### Files to Create

| File | Action | Description |
|------|--------|-------------|
| `services/providers/__init__.py` | CREATE | Package init, exports |
| `services/providers/base.py` | CREATE | Abstract TranslationProvider |
| `services/providers/registry.py` | CREATE | ProviderRegistry singleton |
| `services/providers/schemas.py` | CREATE | Pydantic models |
| `services/providers/config.py` | CREATE | Provider settings from env |
| `services/providers/google_provider.py` | CREATE | Google provider (migrated) |
| `tests/test_providers/__init__.py` | CREATE | Test package |
| `tests/test_providers/test_base.py` | CREATE | Base class tests |
| `tests/test_providers/test_registry.py` | CREATE | Registry tests |
| `tests/test_providers/test_google_provider.py` | CREATE | Google provider tests |

### Files NOT to Modify (Yet)

- `services/translation_service.py` - Keep existing classes, we'll deprecate later
- `translators/*.py` - File processors, different concern
- `routes/*.py` - Will be updated in subsequent stories
- `main.py` - No changes in this story

### Testing Commands

```bash
# Run tests
pytest tests/test_providers/ -v

# Test specific file
pytest tests/test_providers/test_registry.py -v

# With coverage
pytest tests/test_providers/ --cov=services/providers
```

### Project Structure Notes

- Python files: `snake_case` (e.g., `google_provider.py`)
- Classes: `PascalCase` (e.g., `TranslationProvider`, `ProviderRegistry`)
- Variables/functions: `snake_case`
- Follow existing patterns in `services/` folder

### Architecture Compliance

Per `_bmad-output/planning-artifacts/architecture.md`:
- [x] Uses Clean Architecture pattern (providers as pluggable adapters)
- [x] Follows naming conventions (snake_case files, PascalCase classes)
- [x] Configuration via environment variables
- [x] Async support preparation (future story will add async)

### References

- [Source: _bmad-output/planning-artifacts/architecture.md#Data Architecture]
- [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.1]
- [Source: _bmad-output/planning-artifacts/prd.md#FR6-FR7 Translation Providers]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR12-NFR13 Provider Fallback]

### Previous Story Learnings (Epic 1)

From Story 1.1:
- Use `pytest` with `pytest-asyncio` for async tests
- Create `conftest.py` with shared fixtures
- Add `__init__.py` to test directories
- Keep backward compatibility during refactoring

## Dev Agent Record

### Agent Model Used

claude-3-5-sonnet (claude-sonnet-4-20250514)

### Debug Log References

None - implementation completed without issues.

### Completion Notes List

- **Task 1 Complete**: Created `services/providers/` module with abstract base class, schemas, and Pydantic models for requests/responses
- **Task 2 Complete**: Implemented thread-safe singleton `ProviderRegistry` with register, get, list_available, and get_first_available methods
- **Task 3 Complete**: Migrated Google provider to new architecture, preserving caching support via existing `_translation_cache`
- **Task 4 Complete**: Created `services/providers/config.py` with environment-based configuration for all providers; updated `.env.example`
- **Task 5 Complete**: 45 unit tests covering base class, registry, and Google provider functionality
- **All 141 tests pass** including new provider tests and existing regression tests
- **Backward compatibility maintained**: Existing `services/translation_service.py` unchanged

### File List

**New Files Created:**
- `services/providers/__init__.py`
- `services/providers/base.py`
- `services/providers/registry.py`
- `services/providers/schemas.py`
- `services/providers/config.py`
- `services/providers/google_provider.py`
- `tests/test_providers/__init__.py`
- `tests/test_providers/test_base.py`
- `tests/test_providers/test_registry.py`
- `tests/test_providers/test_google_provider.py`

**Modified Files:**
- `.env.example` - Added provider enable flags and fallback chain configuration

## Change Log

- 2026-02-20: Story 2.1 completed - Provider abstraction layer with registry, base class, schemas, config, and Google provider migration. All 45 new tests pass. Total test suite: 141 tests passing.
- 2026-02-20: **Code Review (AI)** - Fixed 3 HIGH + 4 MEDIUM issues:
  - [FIXED] Auto-registration of Google provider in `__init__.py`
  - [FIXED] Added `error` field to `TranslationResponse` for silent failure detection
  - [FIXED] Added `success` property to `TranslationResponse`
  - [FIXED] Language code validation in `TranslationRequest` (ISO 639-1 format)
  - [FIXED] Improved dotenv loading in `config.py`
  - [ADDED] 3 new tests for error handling and validation
  - Total tests: 47 passing

## Senior Developer Review (AI)

**Review Date:** 2026-02-20  
**Reviewer:** AI Code Review (adversarial mode)  
**Outcome:** ✅ APPROVED (after fixes)

### Issues Found and Resolved

| Severity | Issue | File:Line | Status |
|----------|-------|-----------|--------|
| HIGH | Google provider not auto-registered | `__init__.py:42-50` | ✅ Fixed |
| HIGH | Silent failure returns untranslated text | `google_provider.py:135-141` | ✅ Fixed |
| HIGH | No error indication on translation failure | `schemas.py:21-36` | ✅ Fixed |
| MEDIUM | No language code validation | `schemas.py:9-22` | ✅ Fixed |
| MEDIUM | load_dotenv side effect at module level | `config.py:12-19` | ✅ Fixed |
| MEDIUM | Circular import risk (deferred) | `google_provider.py:51` | ⚠️ Acceptable |
| MEDIUM | No integration tests | `tests/` | ⚠️ Deferred |
| LOW | Class-level lock pattern | `registry.py:24-25` | ⚠️ Acceptable |
| LOW | Two provider instance patterns | `google_provider.py:178-186` | ⚠️ Acceptable |

### Git vs Story Discrepancy Note

9 modified files not in File List (from Story 1.7 in-progress):
- `alembic/env.py`, `database/`, `main.py`, `models/`, `requirements.txt`, `routes/`, `services/auth_service.py`

### Test Coverage

- 47 unit tests for providers (all passing)
- Coverage: base class, registry, Google provider, schemas, error handling
