# Documentation d'intégration Laravel — Microservice KYC + Wellness
**Version** : 3.0.0 | **Date** : 18 mai 2026 | **Serveur** : 135.181.211.44:20900

---

## Table des matières

1. [Architecture générale](#1-architecture-générale)
2. [Configuration Laravel](#2-configuration-laravel)
3. [Service de base — KycHttpClient](#3-service-de-base--kychttpclient)
4. [KycService — OCR + Face Matching](#4-kycservice--ocr--face-matching)
5. [TranscriptionService — Audio Whisper](#5-transcriptionservice--audio-whisper)
6. [SentimentService — Analyse NLP](#6-sentimentservice--analyse-nlp)
7. [EmbeddingService — Recherche Sémantique](#7-embeddingservice--recherche-sémantique)
8. [DocumentService — Extraction de Champs](#8-documentservice--extraction-de-champs)
9. [WellnessService — Chatbot LLM](#9-wellnessservice--chatbot-llm)
10. [MedicalService — BioMistral + Garde-fous](#10-medicalservice--biomistral--garde-fous)
11. [Référence complète des endpoints](#11-référence-complète-des-endpoints)
12. [Gestion des erreurs](#12-gestion-des-erreurs)
13. [Tests Feature Laravel](#13-tests-feature-laravel)

---

## 1. Architecture générale

```
┌─────────────────────────────────────────────────────┐
│             Application Laravel                     │
│  Routes → Controllers → Services → KycHttpClient  │
└──────────────────────┬──────────────────────────────┘
                       │ HTTP (Guzzle / Http Facade)
                       │ 135.181.211.44:20900
┌──────────────────────▼──────────────────────────────┐
│          Microservice Python FastAPI                 │
│  ┌──────────┐ ┌──────────┐ ┌────────────────────┐  │
│  │ PRD 1    │ │ PRD 2    │ │ PRD 3              │  │
│  │ OCR      │ │ Whisper  │ │ Phi-4-mini         │  │
│  │ AuraFace │ │ Sentiment│ │ BioMistral-7B      │  │
│  │ KYC      │ │ BGE-M3   │ │ Gemma 4 E4B (lazy) │  │
│  │          │ │ Donut    │ │                    │  │
│  └──────────┘ └──────────┘ └────────────────────┘  │
└─────────────────────────────────────────────────────┘
```

**Modèles actifs au démarrage** :
| Modèle | RAM | Rôle |
|---|---|---|
| PaddleOCR v5 | ~210 MB | OCR documents |
| AuraFace-v1 | ~408 MB | Face matching |
| Phi-4-mini | ~2.5 GB | Chatbot wellness rapide |
| BioMistral-7B | ~4.1 GB | Questions médicales |

**Modèles lazy** (chargés au 1er appel) :
| Modèle | RAM | Rôle |
|---|---|---|
| Whisper Large-v3 | ~1.5 GB | Transcription audio |
| DistilBERT + RoBERTa | ~540 MB | Sentiment |
| all-MiniLM + BGE-M3 | ~660 MB | Embeddings |
| Donut CORD-v2 | ~2 GB | Extraction documents |
| Gemma 4 E4B | ~5 GB | Vision multimodale |

---

## 2. Configuration Laravel

### 2.1 Variables d'environnement — `.env`

```dotenv
# Microservice KYC
KYC_SERVICE_URL=http://135.181.211.44:20900
KYC_SERVICE_TIMEOUT=60
KYC_SERVICE_LLM_TIMEOUT=300
KYC_SERVICE_AUDIO_TIMEOUT=120
```

### 2.2 Fichier de configuration — `config/kyc.php`

```php
<?php

return [
    'url'             => env('KYC_SERVICE_URL', 'http://135.181.211.44:20900'),
    'timeout'         => (int) env('KYC_SERVICE_TIMEOUT', 60),
    'llm_timeout'     => (int) env('KYC_SERVICE_LLM_TIMEOUT', 300),
    'audio_timeout'   => (int) env('KYC_SERVICE_AUDIO_TIMEOUT', 120),
];
```

### 2.3 Enregistrement dans `AppServiceProvider.php`

```php
use App\Services\Kyc\KycHttpClient;

public function register(): void
{
    $this->app->singleton(KycHttpClient::class, function ($app) {
        return new KycHttpClient(
            baseUrl: config('kyc.url'),
            timeout: config('kyc.timeout'),
        );
    });
}
```

---

## 3. Service de base — KycHttpClient

Créer `app/Services/Kyc/KycHttpClient.php` :

```php
<?php

namespace App\Services\Kyc;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class KycHttpClient
{
    public function __construct(
        private string $baseUrl,
        private int    $timeout = 60,
    ) {}

    /**
     * Requête JSON POST.
     */
    public function post(string $endpoint, array $data, int $timeout = null): array
    {
        try {
            $response = Http::timeout($timeout ?? $this->timeout)
                ->post("{$this->baseUrl}{$endpoint}", $data);

            return $this->handleResponse($response, $endpoint);

        } catch (\Exception $e) {
            Log::error("KYC Service error [{$endpoint}]: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * Requête multipart (upload fichiers).
     */
    public function postMultipart(string $endpoint, array $fields, int $timeout = null): array
    {
        try {
            $request = Http::timeout($timeout ?? $this->timeout);

            foreach ($fields as $name => $value) {
                if (is_array($value) && isset($value['contents'])) {
                    $request = $request->attach(
                        $name,
                        $value['contents'],
                        $value['filename'] ?? 'file',
                        $value['headers']  ?? [],
                    );
                }
            }

            $response = $request->post("{$this->baseUrl}{$endpoint}");

            return $this->handleResponse($response, $endpoint);

        } catch (\Exception $e) {
            Log::error("KYC Service multipart error [{$endpoint}]: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * Requête GET.
     */
    public function get(string $endpoint): array
    {
        try {
            $response = Http::timeout($this->timeout)
                ->get("{$this->baseUrl}{$endpoint}");

            return $this->handleResponse($response, $endpoint);

        } catch (\Exception $e) {
            Log::error("KYC Service GET error [{$endpoint}]: " . $e->getMessage());
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    /**
     * Vérifier que le service est opérationnel.
     */
    public function isHealthy(): bool
    {
        $result = $this->get('/health');
        return isset($result['status']) && $result['status'] === 'ok';
    }

    private function handleResponse(Response $response, string $endpoint): array
    {
        if ($response->failed()) {
            Log::warning("KYC Service HTTP error [{$endpoint}]: " . $response->status());
            return [
                'success' => false,
                'error'   => "HTTP {$response->status()}: " . $response->body(),
            ];
        }

        return $response->json() ?? ['success' => false, 'error' => 'Empty response'];
    }
}
```

---

## 4. KycService — OCR + Face Matching

Créer `app/Services/Kyc/KycService.php` :

```php
<?php

namespace App\Services\Kyc;

use Illuminate\Http\UploadedFile;

class KycService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Extraire le texte d'un document d'identité.
     *
     * @param  UploadedFile  $document  Image (JPG/PNG, max 5 MB)
     * @return array{success: bool, ocr: array, elapsed_sec: float, error: ?string}
     */
    public function extractText(UploadedFile $document): array
    {
        return $this->client->postMultipart('/ocr', [
            'file' => [
                'contents' => file_get_contents($document->getRealPath()),
                'filename' => $document->getClientOriginalName(),
                'headers'  => ['Content-Type' => $document->getMimeType()],
            ],
        ]);
    }

    /**
     * Comparer le visage d'un document avec un selfie.
     *
     * @param  UploadedFile  $document  Image document d'identité
     * @param  UploadedFile  $selfie    Photo selfie
     * @return array{success: bool, similarity_score: float, decision: string}
     */
    public function matchFaces(UploadedFile $document, UploadedFile $selfie): array
    {
        return $this->client->postMultipart('/face-match', [
            'document' => [
                'contents' => file_get_contents($document->getRealPath()),
                'filename' => $document->getClientOriginalName(),
                'headers'  => ['Content-Type' => $document->getMimeType()],
            ],
            'selfie' => [
                'contents' => file_get_contents($selfie->getRealPath()),
                'filename' => $selfie->getClientOriginalName(),
                'headers'  => ['Content-Type' => $selfie->getMimeType()],
            ],
        ]);
    }

    /**
     * Pipeline KYC complet : OCR + Face Match + Décision.
     *
     * @return array{
     *   success: bool,
     *   kyc_passed: bool,
     *   final_decision: string,  // APPROVED | REVIEW | REJECTED
     *   steps: array
     * }
     */
    public function verify(UploadedFile $document, UploadedFile $selfie): array
    {
        return $this->client->postMultipart('/kyc/verify', [
            'document' => [
                'contents' => file_get_contents($document->getRealPath()),
                'filename' => $document->getClientOriginalName(),
                'headers'  => ['Content-Type' => $document->getMimeType()],
            ],
            'selfie' => [
                'contents' => file_get_contents($selfie->getRealPath()),
                'filename' => $selfie->getClientOriginalName(),
                'headers'  => ['Content-Type' => $selfie->getMimeType()],
            ],
        ]);
    }
}
```

### Exemple Controller

```php
<?php

namespace App\Http\Controllers;

use App\Services\Kyc\KycService;
use Illuminate\Http\Request;

class KycController extends Controller
{
    public function __construct(private KycService $kyc) {}

    public function verify(Request $request)
    {
        $request->validate([
            'document' => 'required|image|max:5120',
            'selfie'   => 'required|image|max:5120',
        ]);

        $result = $this->kyc->verify(
            $request->file('document'),
            $request->file('selfie'),
        );

        if (! $result['success']) {
            return response()->json(['error' => $result['error']], 422);
        }

        // Sauvegarder en base
        auth()->user()->kyc()->create([
            'decision'         => $result['final_decision'],
            'similarity_score' => $result['steps']['face_match']['similarity_score'] ?? 0,
            'ocr_text'         => $result['steps']['ocr']['raw_text'] ?? '',
        ]);

        return response()->json([
            'kyc_passed' => $result['kyc_passed'],
            'decision'   => $result['final_decision'],
        ]);
    }
}
```

---

## 5. TranscriptionService — Audio Whisper

Créer `app/Services/Kyc/TranscriptionService.php` :

```php
<?php

namespace App\Services\Kyc;

use Illuminate\Http\UploadedFile;

class TranscriptionService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Transcrire un fichier audio.
     * Formats supportés : wav, mp3, mp4, m4a, ogg (max 50 MB)
     *
     * @return array{
     *   success: bool,
     *   full_text: string,
     *   segments: array,
     *   language: string,
     *   duration_sec: float,
     *   elapsed_sec: float
     * }
     */
    public function transcribe(UploadedFile $audio, string $language = 'fr'): array
    {
        return $this->client->postMultipart('/audio/transcribe', [
            'file' => [
                'contents' => file_get_contents($audio->getRealPath()),
                'filename' => $audio->getClientOriginalName(),
                'headers'  => ['Content-Type' => $audio->getMimeType()],
            ],
            'language' => ['contents' => $language],
        ], timeout: 120);
    }

    /**
     * Transcrire ET identifier les locuteurs (qui parle quand).
     * Idéal pour : comptes-rendus réunion, analyse d'appels.
     *
     * @return array{
     *   success: bool,
     *   full_text: string,
     *   attributed_segments: array,  // [{start, end, speaker, text}]
     *   num_speakers: int,
     *   diarization_active: bool
     * }
     */
    public function transcribeAndDiarize(
        UploadedFile $audio,
        string $language = 'fr',
        ?int $numSpeakers = null
    ): array {
        $fields = [
            'file' => [
                'contents' => file_get_contents($audio->getRealPath()),
                'filename' => $audio->getClientOriginalName(),
                'headers'  => ['Content-Type' => $audio->getMimeType()],
            ],
            'language' => ['contents' => $language],
        ];

        if ($numSpeakers) {
            $fields['num_speakers'] = ['contents' => (string) $numSpeakers];
        }

        return $this->client->postMultipart(
            '/audio/transcribe-and-diarize',
            $fields,
            timeout: 180
        );
    }
}
```

### Exemple Controller

```php
public function transcribe(Request $request, TranscriptionService $service)
{
    $request->validate(['audio' => 'required|file|max:51200']);

    $result = $service->transcribe($request->file('audio'), 'fr');

    return response()->json([
        'text'     => $result['full_text'] ?? '',
        'duration' => $result['duration_sec'] ?? 0,
        'language' => $result['language'] ?? 'fr',
    ]);
}
```

---

## 6. SentimentService — Analyse NLP

Créer `app/Services/Kyc/SentimentService.php` :

```php
<?php

namespace App\Services\Kyc;

class SentimentService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Analyser le sentiment d'un ou plusieurs textes.
     *
     * @param  string[]  $texts  Liste de textes (max 1000)
     * @param  string    $mode   'general' (DistilBERT) ou 'social' (RoBERTa)
     *
     * @return array{
     *   success: bool,
     *   results: array,   // [{text, label, score, sentiment_fr}]
     *   summary: array{positive_pct, negative_pct, neutral_pct, dominant}
     * }
     */
    public function analyze(array $texts, string $mode = 'general'): array
    {
        return $this->client->post('/nlp/sentiment', [
            'texts' => $texts,
            'mode'  => $mode,
        ]);
    }

    /**
     * Calculer la similarité sémantique entre deux textes (score 0-1).
     */
    public function similarity(
        string $text1,
        string $text2,
        string $model = 'mini'
    ): array {
        return $this->client->post('/nlp/similarity', [
            'text1'      => $text1,
            'text2'      => $text2,
            'model_type' => $model,
        ]);
    }
}
```

### Exemple Controller

```php
// Analyser les avis clients
public function analyzeReviews(Request $request, SentimentService $service)
{
    $reviews = Review::latest()->take(100)->pluck('content')->toArray();

    $result = $service->analyze($reviews, mode: 'social');

    return response()->json([
        'dominant'     => $result['summary']['dominant'],
        'positive_pct' => $result['summary']['positive_pct'],
        'negative_pct' => $result['summary']['negative_pct'],
    ]);
}
```

---

## 7. EmbeddingService — Recherche Sémantique

Créer `app/Services/Kyc/EmbeddingService.php` :

```php
<?php

namespace App\Services\Kyc;

class EmbeddingService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Générer des vecteurs d'embeddings pour des textes.
     *
     * @param  string[]  $texts      Textes à vectoriser
     * @param  string    $modelType  'mini' (rapide, 384D) ou 'bge' (précis, 1024D)
     *
     * @return array{
     *   success: bool,
     *   embeddings: float[][],
     *   dimension: int,
     *   model: string
     * }
     */
    public function encode(array $texts, string $modelType = 'mini'): array
    {
        return $this->client->post('/nlp/embed', [
            'texts'      => $texts,
            'model_type' => $modelType,
        ]);
    }

    /**
     * Calculer la similarité sémantique entre deux textes.
     * Score : 0 (sans rapport) → 1 (identiques).
     */
    public function similarity(string $text1, string $text2): array
    {
        return $this->client->post('/nlp/similarity', [
            'text1'      => $text1,
            'text2'      => $text2,
            'model_type' => 'mini',
        ]);
    }

    /**
     * Recherche sémantique dans un corpus de textes.
     * Retourne les N textes les plus proches de la requête.
     *
     * @param  string    $query    Question ou texte de recherche
     * @param  string[]  $corpus   Corpus de textes à parcourir
     * @param  int       $topN     Nombre de résultats
     */
    public function search(string $query, array $corpus, int $topN = 5): array
    {
        // Encoder la requête + le corpus en une seule requête
        $allTexts = array_merge([$query], $corpus);
        $result   = $this->encode($allTexts, 'mini');

        if (! $result['success']) {
            return ['success' => false, 'results' => [], 'error' => $result['error']];
        }

        $embeddings  = $result['embeddings'];
        $queryVec    = $embeddings[0];
        $corpusVecs  = array_slice($embeddings, 1);

        // Calculer similarités cosinus (vecteurs déjà normalisés)
        $scores = [];
        foreach ($corpusVecs as $i => $vec) {
            $dot = 0.0;
            foreach ($vec as $j => $v) {
                $dot += $v * $queryVec[$j];
            }
            $scores[$i] = $dot;
        }

        arsort($scores);
        $topResults = array_slice($scores, 0, $topN, true);

        return [
            'success' => true,
            'results' => array_map(fn($i, $score) => [
                'text'  => $corpus[$i],
                'score' => round($score, 4),
                'rank'  => array_search($i, array_keys($topResults)) + 1,
            ], array_keys($topResults), $topResults),
        ];
    }
}
```

---

## 8. DocumentService — Extraction de Champs

Créer `app/Services/Kyc/DocumentService.php` :

```php
<?php

namespace App\Services\Kyc;

use Illuminate\Http\UploadedFile;

class DocumentService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Extraire les champs structurés d'un document financier.
     * Donut comprend la sémantique : montant, date, TVA, articles...
     *
     * @param  UploadedFile  $document  Image (JPG/PNG, max 5 MB)
     *
     * @return array{
     *   success: bool,
     *   fields: array,         // {total_price, items, date, merchant_name...}
     *   document_type: string, // 'receipt/invoice'
     *   raw_output: string
     * }
     */
    public function extractFields(UploadedFile $document): array
    {
        return $this->client->postMultipart('/document/extract-fields', [
            'file' => [
                'contents' => file_get_contents($document->getRealPath()),
                'filename' => $document->getClientOriginalName(),
                'headers'  => ['Content-Type' => $document->getMimeType()],
            ],
        ], timeout: 60);
    }
}
```

### Exemple Controller

```php
// Traitement automatique de factures
public function processInvoice(Request $request, DocumentService $service)
{
    $request->validate(['invoice' => 'required|image|max:5120']);

    $result = $service->extractFields($request->file('invoice'));

    if (! $result['success']) {
        return response()->json(['error' => $result['error']], 422);
    }

    $fields = $result['fields'];

    // Sauvegarder les champs extraits
    Invoice::create([
        'total'     => $fields['total_price'] ?? null,
        'date'      => $fields['date'] ?? null,
        'merchant'  => $fields['nm_nm'] ?? null,
        'raw_data'  => json_encode($fields),
    ]);

    return response()->json($fields);
}
```

---

## 9. WellnessService — Chatbot LLM

Créer `app/Services/Kyc/WellnessService.php` :

```php
<?php

namespace App\Services\Kyc;

use Illuminate\Http\UploadedFile;

class WellnessService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Envoyer un message au chatbot wellness.
     * Le routeur intelligent sélectionne automatiquement :
     *   - Phi-4-mini  : messages simples, salutations
     *   - BioMistral  : domaine médical (avec garde-fous)
     *   - Gemma 4 E4B : si image ou audio joint
     *
     * @param  array   $messages    Historique [{role, content}]
     * @param  string  $domain      general|medical|nutrition|fitness|pregnancy|sleep|mental_health
     * @param  string|null $forceModel  phi4|biomistral|gemma4_e4b
     *
     * @return array{
     *   success: bool,
     *   response: string,
     *   model: string,
     *   routing_reason: string,
     *   tokens_used: int,
     *   elapsed_sec: float
     * }
     */
    public function chat(
        array $messages,
        string $domain = 'general',
        ?string $forceModel = null
    ): array {
        return $this->client->post('/wellness/chat', [
            'messages'    => $messages,
            'domain'      => $domain,
            'force_model' => $forceModel,
        ], timeout: config('kyc.llm_timeout', 300));
    }

    /**
     * Analyser une image (repas, posture, résultat médical).
     * Utilise Gemma 4 E4B (lazy loading au 1er appel).
     *
     * @param  UploadedFile  $image   Photo à analyser
     * @param  string        $prompt  Question sur l'image
     */
    public function analyzeImage(UploadedFile $image, string $prompt): array
    {
        return $this->client->postMultipart('/wellness/vision', [
            'file' => [
                'contents' => file_get_contents($image->getRealPath()),
                'filename' => $image->getClientOriginalName(),
                'headers'  => ['Content-Type' => $image->getMimeType()],
            ],
            'prompt' => ['contents' => $prompt],
        ], timeout: 300);
    }

    /**
     * Générer un programme personnalisé (fitness, nutrition, sommeil...).
     * Utilise Phi-4-mini.
     *
     * @param  array   $profile  {age, weight_kg, height_cm, goal, level, constraints[]}
     * @param  string  $type     fitness|nutrition|sleep|mental_health
     * @param  int     $weeks    Durée du programme en semaines
     */
    public function generateProgram(array $profile, string $type = 'fitness', int $weeks = 4): array
    {
        return $this->client->post('/wellness/program', [
            'profile'        => $profile,
            'type'           => $type,
            'duration_weeks' => $weeks,
        ], timeout: config('kyc.llm_timeout', 300));
    }

    /**
     * RAG Wellness : répondre à une question en s'appuyant sur des sources.
     *
     * @param  string  $question       Question de l'utilisateur
     * @param  array   $knowledgeBase  [{text, source}] — résultats de recherche sémantique
     * @param  string  $model          phi4|biomistral
     */
    public function ragAnswer(string $question, array $knowledgeBase = [], string $model = 'phi4'): array
    {
        return $this->client->post('/wellness/rag', [
            'question'               => $question,
            'knowledge_base_results' => $knowledgeBase,
            'model'                  => $model,
        ], timeout: config('kyc.llm_timeout', 300));
    }

    /**
     * Statut détaillé de tous les LLMs.
     */
    public function llmStatus(): array
    {
        return $this->client->get('/llm/status');
    }
}
```

### Exemple Controller Chatbot

```php
<?php

namespace App\Http\Controllers\Api;

use App\Services\Kyc\WellnessService;
use Illuminate\Http\Request;

class WellnessChatController extends Controller
{
    public function __construct(private WellnessService $wellness) {}

    /**
     * POST /api/wellness/chat
     */
    public function chat(Request $request)
    {
        $request->validate([
            'message' => 'required|string|max:2000',
            'domain'  => 'in:general,medical,nutrition,fitness,pregnancy,sleep,mental_health',
        ]);

        // Récupérer l'historique depuis la session
        $history = $request->session()->get('chat_history', []);

        // Ajouter le nouveau message
        $history[] = ['role' => 'user', 'content' => $request->message];

        $result = $this->wellness->chat(
            messages: $history,
            domain:   $request->domain ?? 'general',
        );

        if (! $result['success']) {
            return response()->json(['error' => $result['error']], 500);
        }

        // Sauvegarder la réponse dans l'historique
        $history[] = ['role' => 'assistant', 'content' => $result['response']];
        $request->session()->put('chat_history', array_slice($history, -20)); // 20 derniers messages

        return response()->json([
            'response'       => $result['response'],
            'model'          => $result['model'],
            'routing_reason' => $result['routing_reason'],
            'elapsed_sec'    => $result['elapsed_sec'],
        ]);
    }

    /**
     * POST /api/wellness/vision
     */
    public function vision(Request $request)
    {
        $request->validate([
            'image'  => 'required|image|max:5120',
            'prompt' => 'string|max:500',
        ]);

        $result = $this->wellness->analyzeImage(
            $request->file('image'),
            $request->prompt ?? 'Analyse cette image et donne-moi des conseils bien-être.',
        );

        return response()->json($result);
    }

    /**
     * POST /api/wellness/program
     */
    public function program(Request $request)
    {
        $request->validate([
            'profile.age'        => 'required|integer|min:10|max:120',
            'profile.weight_kg'  => 'nullable|numeric',
            'profile.height_cm'  => 'nullable|numeric',
            'profile.goal'       => 'required|string',
            'type'               => 'required|in:fitness,nutrition,sleep,mental_health',
            'duration_weeks'     => 'integer|min:1|max:52',
        ]);

        $result = $this->wellness->generateProgram(
            profile: $request->profile,
            type:    $request->type,
            weeks:   $request->duration_weeks ?? 4,
        );

        return response()->json($result);
    }
}
```

---

## 10. MedicalService — BioMistral + Garde-fous

Créer `app/Services/Kyc/MedicalService.php` :

```php
<?php

namespace App\Services\Kyc;

class MedicalService
{
    public function __construct(private KycHttpClient $client) {}

    /**
     * Poser une question médicale à BioMistral-7B.
     *
     * ⚠️  GARDE-FOUS ACTIFS côté microservice :
     *   1. Détection automatique des urgences → réponse hardcodée (15/SAMU)
     *   2. Système prompt sécurisé (jamais de diagnostic)
     *   3. Disclaimer médical ajouté automatiquement
     *   4. Redirection médecin obligatoire
     *
     * @param  string  $question  Question médicale de l'utilisateur
     * @param  array   $history   Historique de conversation [{role, content}]
     *
     * @return array{
     *   success: bool,
     *   response: string,       // Réponse avec disclaimer obligatoire
     *   is_emergency: bool,     // true si urgence détectée
     *   model: string,
     *   elapsed_sec: float
     * }
     */
    public function ask(string $question, array $history = []): array
    {
        $messages = $history;
        if ($question) {
            $messages[] = ['role' => 'user', 'content' => $question];
        }

        return $this->client->post('/medical/ask', [
            'messages' => $messages,
            'question' => $question,
        ], timeout: config('kyc.llm_timeout', 300));
    }
}
```

### Exemple Controller

```php
<?php

namespace App\Http\Controllers\Api;

use App\Services\Kyc\MedicalService;
use Illuminate\Http\Request;

class MedicalController extends Controller
{
    public function __construct(private MedicalService $medical) {}

    /**
     * POST /api/medical/ask
     */
    public function ask(Request $request)
    {
        $request->validate(['question' => 'required|string|max:2000']);

        $result = $this->medical->ask($request->question);

        // Logguer toutes les urgences détectées
        if ($result['is_emergency'] ?? false) {
            \Log::warning('Medical emergency detected', [
                'user_id'  => auth()->id(),
                'question' => substr($request->question, 0, 100),
            ]);
        }

        return response()->json([
            'response'     => $result['response'],
            'is_emergency' => $result['is_emergency'] ?? false,
            'model'        => $result['model'],
        ]);
    }
}
```

---

## 11. Référence complète des endpoints

### Base URL : `http://135.181.211.44:20900`

| # | Méthode | Endpoint | Service Laravel | Timeout |
|---|---|---|---|---|
| 1 | GET | `/health` | `KycHttpClient::isHealthy()` | 5s |
| 2 | POST | `/ocr` | `KycService::extractText()` | 30s |
| 3 | POST | `/face-match` | `KycService::matchFaces()` | 30s |
| 4 | POST | `/kyc/verify` | `KycService::verify()` | 60s |
| 5 | POST | `/nlp/sentiment` | `SentimentService::analyze()` | 30s |
| 6 | POST | `/nlp/embed` | `EmbeddingService::encode()` | 30s |
| 7 | POST | `/nlp/similarity` | `EmbeddingService::similarity()` | 30s |
| 8 | POST | `/document/extract-fields` | `DocumentService::extractFields()` | 60s |
| 9 | POST | `/audio/transcribe` | `TranscriptionService::transcribe()` | 120s |
| 10 | POST | `/audio/transcribe-and-diarize` | `TranscriptionService::transcribeAndDiarize()` | 180s |
| 11 | POST | `/wellness/chat` | `WellnessService::chat()` | 300s |
| 12 | POST | `/wellness/vision` | `WellnessService::analyzeImage()` | 300s |
| 13 | POST | `/medical/ask` | `MedicalService::ask()` | 300s |
| 14 | POST | `/wellness/program` | `WellnessService::generateProgram()` | 300s |
| 15 | POST | `/wellness/rag` | `WellnessService::ragAnswer()` | 300s |
| 16 | GET | `/llm/status` | `WellnessService::llmStatus()` | 5s |

### Détail des payloads

#### POST `/kyc/verify`
```
Content-Type: multipart/form-data
Fields: document (image), selfie (image)

Response:
{
  "success": true,
  "kyc_passed": true,
  "final_decision": "APPROVED",   // APPROVED | REVIEW | REJECTED
  "elapsed_sec": 0.89,
  "steps": {
    "ocr": { "raw_text": "NOM: DUPONT...", "lines": [...] },
    "face_match": { "similarity_score": 0.78, "decision": "APPROVED" }
  }
}
```

#### POST `/nlp/sentiment`
```json
{ "texts": ["texte 1", "texte 2"], "mode": "general" }

Response:
{
  "success": true,
  "results": [{ "text": "...", "label": "POSITIVE", "score": 0.99, "sentiment_fr": "positif" }],
  "summary": { "positive_pct": 100.0, "negative_pct": 0.0, "dominant": "positif" }
}
```

#### POST `/wellness/chat`
```json
{
  "messages": [{"role": "user", "content": "Comment mieux dormir ?"}],
  "domain": "sleep",
  "force_model": null
}

Response:
{
  "success": true,
  "response": "Pour améliorer votre sommeil...",
  "model": "phi4-mini",
  "routing_reason": "message simple → Phi-4-mini",
  "tokens_used": 245,
  "elapsed_sec": 8.4
}
```

#### POST `/medical/ask`
```json
{ "question": "Quels sont les symptômes du diabète ?" }

Response:
{
  "success": true,
  "response": "...⚕️ *Information médicale générale...*",
  "is_emergency": false,
  "model": "biomistral-7b"
}
```

---

## 12. Gestion des erreurs

```php
<?php

namespace App\Services\Kyc;

class KycException extends \RuntimeException {}
class KycServiceUnavailableException extends KycException {}
class KycModelNotReadyException extends KycException {}

// Dans KycHttpClient, version robuste avec retry
public function postWithRetry(string $endpoint, array $data, int $retries = 2): array
{
    $lastError = null;

    for ($attempt = 0; $attempt <= $retries; $attempt++) {
        try {
            $result = $this->post($endpoint, $data);

            if ($result['success']) {
                return $result;
            }

            // Erreur "modèle non chargé" → attendre et réessayer
            if (str_contains($result['error'] ?? '', 'non chargé')) {
                sleep(2 * ($attempt + 1));
                continue;
            }

            return $result;

        } catch (\Exception $e) {
            $lastError = $e;
            sleep(1);
        }
    }

    return ['success' => false, 'error' => $lastError?->getMessage() ?? 'Max retries reached'];
}
```

### Middleware de vérification santé

```php
<?php

namespace App\Http\Middleware;

use App\Services\Kyc\KycHttpClient;
use Closure;

class EnsureKycServiceHealthy
{
    public function __construct(private KycHttpClient $client) {}

    public function handle($request, Closure $next)
    {
        if (! $this->client->isHealthy()) {
            return response()->json([
                'error' => 'Le service KYC est temporairement indisponible.'
            ], 503);
        }

        return $next($request);
    }
}
```

### Utilisation dans les routes

```php
// routes/api.php
Route::middleware(['auth:sanctum', EnsureKycServiceHealthy::class])->group(function () {
    Route::post('/kyc/verify',           [KycController::class, 'verify']);
    Route::post('/wellness/chat',        [WellnessChatController::class, 'chat']);
    Route::post('/wellness/vision',      [WellnessChatController::class, 'vision']);
    Route::post('/wellness/program',     [WellnessChatController::class, 'program']);
    Route::post('/medical/ask',          [MedicalController::class, 'ask']);
    Route::post('/audio/transcribe',     [TranscriptionController::class, 'transcribe']);
    Route::post('/document/extract',     [DocumentController::class, 'extract']);
});
```

---

## 13. Tests Feature Laravel

```php
<?php

namespace Tests\Feature;

use App\Services\Kyc\KycHttpClient;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;

class KycServiceTest extends TestCase
{
    protected function mockKycClient(array $response): void
    {
        $this->mock(KycHttpClient::class, function ($mock) use ($response) {
            $mock->shouldReceive('post')->andReturn($response);
            $mock->shouldReceive('postMultipart')->andReturn($response);
            $mock->shouldReceive('get')->andReturn(['status' => 'ok']);
            $mock->shouldReceive('isHealthy')->andReturn(true);
        });
    }

    /** @test */
    public function kyc_verify_returns_decision()
    {
        $this->mockKycClient([
            'success'        => true,
            'kyc_passed'     => true,
            'final_decision' => 'APPROVED',
            'elapsed_sec'    => 0.5,
            'steps'          => ['ocr' => [], 'face_match' => ['similarity_score' => 0.8]],
        ]);

        $response = $this->actingAs($this->createUser())
            ->postJson('/api/kyc/verify', [
                'document' => UploadedFile::fake()->image('doc.jpg'),
                'selfie'   => UploadedFile::fake()->image('selfie.jpg'),
            ]);

        $response->assertOk()
                 ->assertJsonPath('decision', 'APPROVED')
                 ->assertJsonPath('kyc_passed', true);
    }

    /** @test */
    public function wellness_chat_routes_to_phi4_for_simple_message()
    {
        $this->mockKycClient([
            'success'        => true,
            'response'       => 'Bonjour ! Comment puis-je vous aider ?',
            'model'          => 'phi4-mini',
            'routing_reason' => 'message simple → Phi-4-mini',
            'tokens_used'    => 50,
            'elapsed_sec'    => 8.4,
        ]);

        $response = $this->actingAs($this->createUser())
            ->postJson('/api/wellness/chat', [
                'message' => 'Bonjour !',
                'domain'  => 'general',
            ]);

        $response->assertOk()
                 ->assertJsonPath('model', 'phi4-mini');
    }

    /** @test */
    public function medical_ask_detects_emergency()
    {
        $this->mockKycClient([
            'success'      => true,
            'response'     => '🚨 URGENCE MÉDICALE — Appelez le 15 (SAMU)',
            'is_emergency' => true,
            'model'        => 'biomistral-7b-safety-filter',
            'elapsed_sec'  => 0.001,
        ]);

        $response = $this->actingAs($this->createUser())
            ->postJson('/api/medical/ask', [
                'question' => 'je veux mourir',
            ]);

        $response->assertOk()
                 ->assertJsonPath('is_emergency', true);
    }

    /** @test */
    public function sentiment_analysis_returns_dominant()
    {
        $this->mockKycClient([
            'success' => true,
            'results' => [['text' => '...', 'label' => 'POSITIVE', 'score' => 0.99, 'sentiment_fr' => 'positif']],
            'summary' => ['dominant' => 'positif', 'positive_pct' => 100.0, 'negative_pct' => 0.0],
        ]);

        $response = $this->actingAs($this->createUser())
            ->postJson('/api/nlp/sentiment', [
                'texts' => ['I love this product !'],
            ]);

        $response->assertOk()
                 ->assertJsonPath('summary.dominant', 'positif');
    }
}
```

---

## Annexe — SaaS possibles sur ce microservice

| SaaS | Endpoints utilisés |
|---|---|
| **KYC Identité** | `/kyc/verify` |
| **Transcription Audio** | `/audio/transcribe` |
| **Compte-Rendu Réunion** | `/audio/transcribe-and-diarize` |
| **Analyse Avis Clients** | `/nlp/sentiment` |
| **Chatbot RAG** | `/nlp/embed` + `/wellness/rag` |
| **Coach Nutrition** | `/wellness/chat` + `/wellness/vision` |
| **Coach Fitness** | `/wellness/chat` + `/wellness/program` |
| **Assistant Grossesse** | `/wellness/chat` + `/medical/ask` |
| **Assistant Sommeil** | `/wellness/chat` + `/audio/transcribe` |
| **Santé Mentale** | `/wellness/chat` + `/nlp/sentiment` |
| **Comptabilité Auto** | `/document/extract-fields` + `/ocr` |

---

*Documentation générée le 18 mai 2026 — Microservice v3.0.0 — Port 20900*
