# Gemini — Arquitetura

> Módulo que integra a API do Google Gemini (`generativelanguage.googleapis.com`) na v5.
> Existem **duas integrações independentes** que só compartilham a credencial.

---

## 1. Visão geral das duas integrações

| | Caminho A — **Geração de conteúdo** | Caminho B — **Mascaramento LGPD** |
|---|---|---|
| Objetivo | Redigir título + corpo de notícias/publicações | Anonimizar texto livre de pedidos (e-SIC/Ouvidoria) |
| Entry point | `Prefecture\GeminiController@preencher` | `DataMaskTrait::maskTextWithAi()` |
| Rota | `POST /preencher-formulario` (nome `gemini.ia`) | **Sem rota** — chamada interna durante o `store` do pedido |
| Modelo | `gemini-2.0-flash` (hardcoded) | `gemini-2.5-flash` (default de `env('GEMINI_API_MODEL')`, mas **fixo na URL**) |
| Timeout | **nenhum** (Guzzle 5 → 0 = infinito) | `10.0` s |
| Sem chave | HTTP 500 com `title = "Erro de Configuração"` | `Log::warning` + retorna máscara local |
| Erro HTTP | HTTP 500 (ou status do upstream) com a mensagem crua | `Log::error` + retorna máscara local |
| Fallback | **não existe** | **maskTextDeterministic()** (padrão determinístico local) |
| Consome a chave em | `configurations` apenas | `configurations` **ou** `env('GEMINI_API_KEY')` |

> **Risco estrutural:** os dois caminhos não compartilham código. Mudar o modelo, o
> timeout ou a forma de obter a chave em um **não** afeta o outro.

---

## 2. Fluxo A — Geração de conteúdo (CMS)

```
[Operador no CMS: Notícias/Publicações]
        │  clica "Preencher com IA"
        ▼
v5/resources/views/prefecture/contents/components/ia-modal/index.blade.php
        │  #promptInput + #btnGerarIA
        ▼
v5/public/assets/js/custom/utilities/helpers/ia-modal.js
        │  $.ajax POST { route('gemini.ia') }  + header X-CSRF-TOKEN
        ▼
v5/routes/web.php:203
        │  Route::post('/preencher-formulario', 'Prefecture\GeminiController@preencher')
        │  grupo: Route::middleware(['check.site.manutention'])  (sem check.logged)
        ▼
v5/app/Http/Middleware/CheckSiteManutention.php
        │  GET / POST
        ▼
v5/app/Http/Requests/GeminiPromptRequest.php
        │  rules(): prompt => required|string|min:4|max:5000
        │  authorize(): true
        ▼
v5/app/Http/Controllers/Prefecture/GeminiController.php:11  preencher()
        │  1. lê prompt
        │  2. concatena instrucaoAdicional
        │  3. Configuration::where('key','gemini_api_key')->value('value')
        │  4. monta URL com ?key=<chave>
        ▼
GuzzleHttp\Client()->post($url, ['json' => ['contents' => [['parts' => [['text' => ...]]]]]])
        │
        ▼
Google Gemini — POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=<chave>
        │  Resposta 200: {"candidates":[{"content":{"parts":[{"text":"..."}]}}]}
        ▼
[heurística título/corpo]  →  response()->json(['title' => ..., 'text' => ...])
        ▼
ia-modal.js  →  $('#title').val(data.title)  +  $('#text').summernote('code', data.text)
```

### 2.1 Fluxo em Mermaid

```mermaid
sequenceDiagram
    participant O as Operador (CMS)
    participant V as Blade + ia-modal.js
    participant R as Rota gemini.ia
    participant C as GeminiController@preencher
    participant DB as configurations
    participant G as API Gemini

    O->>V: "Preencher com IA" + prompt
    V->>R: POST {prompt} (X-CSRF-TOKEN)
    R->>C: FormRequest validado (min:4|max:5000)
    C->>DB: where('key','gemini_api_key')->value('value')
    DB-->>C: chave (ou vazio)
    alt chave vazia
        C-->>V: 500 {title:"Erro de Configuração", text:"..."}
    else chave ok
        C->>G: POST generateContent (modelo gemini-2.0-flash)
        G-->>C: 200 {candidates[0].content.parts[0].text}
        C->>C: split heurístico título/corpo
        C-->>V: 200 {title, text}
        V->>V: #title.val() + #text.summernote('code')
    end
```

---

## 3. Fluxo B — Mascaramento LGPD (Portal)

```
[Cidadão preenche pedido no portal (e-SIC ou Ouvidoria)]
        ▼
v5/app/Http/Controllers/Portal/InformationController.php:112 / :320
v5/app/Http/Controllers/Portal/OmbudsmanController.php:180
        │  'mask_request' => $this->MaskRequestWithGeminiAi($requestText)
        ▼
v5/app/Http/Controllers/Portal/InformationController.php:388
        │  MaskRequestWithGeminiAi() → $this->maskTextWithAi()
        ▼
v5/app/Http/Traits/DataMaskTrait.php:213  maskTextWithAi()
        │
        ├─ 1) maskTextDeterministic($text)   ← baseline regex (CPF, CNPJ, RG, e-mail…)
        │
        ├─ 2) resolve credencial:
        │      env('GEMINI_API_URL')
        │      env('GEMINI_API_MODEL', 'gemini-2.5-flash')   ← lido e NÃO usado
        │      Configuration::where('key','gemini_api_key')->first()
        │      → value do banco, senão env('GEMINI_API_KEY')
        │
        ├─ 3) sem chave → Log::warning + retorna $maskedPhase1
        │
        ├─ 4) POST generateContent (timeout 10.0)
        │      200  → candidates[0].content.parts[0].text
        │             + pós-processamento de asteriscos (regex)
        │             → retorna texto mascarado
        │      ≠200 → Log::error + cai no return da máscara local
        │      exceção → Log::error + cai no return da máscara local
        ▼
informations_requests.mask_request / ombudsman_requests.mask_description
        ▼
Relatórios estatísticos públicos (e-SIC e Ouvidoria)
```

---

## 4. Inventário de classes

| Classe / arquivo | Papel |
|---|---|
| `v5/app/Http/Controllers/Prefecture/GeminiController.php` | Endpoint de **geração** de título+corpo. Herda de `App\Http\Controllers\Admin\Controller` (apesar de ser rota de portal). |
| `v5/app/Http/Requests/GeminiPromptRequest.php` | Validação do `prompt` (`required|string|min:4|max:5000`). `authorize()` → `true`. |
| `v5/app/Http/Traits/DataMaskTrait.php` | Contém `maskTextWithAi()` (caminho B) e a base determinística `maskTextDeterministic()` / `maskSensitiveDictionary()`. |
| `v5/app/Http/Controllers/Portal/InformationController.php` | Consumidor do caminho B no e-SIC (`MaskRequestWithGeminiAi`, linhas 112 e 320). |
| `v5/app/Http/Controllers/Portal/OmbudsmanController.php` | Consumidor do caminho B na Ouvidoria (`maskTextWithAi`, linha 180). |
| `v5/app/Http/Model/Prefecture/Configuration.php` | Model da tabela `configurations` (namespace `App\Prefecture` via classmap de `app/`). Fornece a chave. |
| `v5/app/Http/Controllers/Prefecture/ConfigurationController.php` | CRUD da tela de Configurações (CMS) onde a chave é digitada e salva. |
| `v5/app/Http/Middleware/CheckSiteManutention.php` | Middleware do grupo que contém a rota `gemini.ia`. |
| `v5/app/Http/Controllers/Admin/Controller.php` | Controller base do `GeminiController` (`App\Http\Controllers\Admin\Controller`). |
| `v5/public/assets/js/custom/utilities/helpers/ia-modal.js` | Cliente AJAX do fluxo A. |
| `v5/resources/views/prefecture/contents/components/ia-modal/index.blade.php` | Modal do fluxo A. |
| `v5/database/seeds/AddToConfigurationsGeminiApiKeyValue.php` | Grava a chave **hardcoded** em `configurations.value`. |
| `v5/database/seeds/UpdateConfigurationSeeder.php` | Cria a linha `gemini_api_key` (se ausente). |
| `v5/database/seeds/ConfigurationOrderSeeder.php` | Cria a linha `gemini_api_key` (se ausente), `order = 70`. |
| `v5/database/seeds/ExtrasSeeder.php` | Orquestra os seeders acima (`$this->call(AddToConfigurationsGeminiApiKeyValue::class)`). |

---

## 5. Contrato HTTP com a API do Gemini

### 5.1 Requisição

| Item | Caminho A (geração) | Caminho B (mascaramento) |
|---|---|---|
| Método | `POST` | `POST` |
| URL | `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=<chave>` | `rtrim(env('GEMINI_API_URL'),'/') ?: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'`, e acrescenta `?key=<chave>` apenas se a URL ainda não tiver `?` |
| Autenticação | **query string** `?key=` | **query string** `?key=` |
| Headers | `Content-Type: application/json` (implícito pelo `json` do Guzzle) | idem |
| Timeout | **não configurado** (Guzzle 5.3.4: `timeout = 0` → sem timeout) | `['timeout' => 10.0]` |
| Payload | `{"contents":[{"parts":[{"text":"<prompt>\n\n<instrucaoAdicional>"}]}]}` | idem, com o *prompt* de anonimização em heredoc |

Não há `generationConfig` (temperature, `maxOutputTokens`), `safetySettings` nem
`systemInstruction` em nenhum dos dois caminhos.

### 5.2 Resposta esperada

```json
{
  "candidates": [
    { "content": { "parts": [ { "text": "Título\n\nCorpo do texto" } ] } }
  ]
}
```

Os dois caminhos leem exatamente `candidates[0].content.parts[0].text`.
`promptFeedback.blockReason` e `finishReason` **não são verificados** — um bloqueio de
segurança do Gemini chega ao operador como "Resposta inesperada da API Gemini." (500).

### 5.3 Payload real enviado pelo caminho A

```php
$response = $client->post($url, [
    'json' => [
        'contents' => [[
            'parts' => [['text' => $promptParaIA]]
        ]]
    ]
]);
```

### 5.4 Instrução fixa do caminho A (`GeminiController.php:15-18`)

> "Por favor, forneça a resposta em texto corrido, sem formatação Markdown (como negrito,
> itálico, títulos ou listas). Separe o título do corpo do texto com uma linha em branco.
> Não inclua instruções, comandos ou frases como 'adicione tal coisa' ou 'faça isso'.
> Apenas forneça o texto final, pronto para uso, baseado nos dados fornecidos. O titulo
> deve ter no máximo 150 caracteres."

### 5.5 Instrução fixa do caminho B (`DataMaskTrait.php:222-231`)

Heredoc `PROMPT_FINAL_COM_DELIMITADOR` com 5 regras: (1) dados sensíveis → asteriscos por
palavra; (2) nomes → manter só o primeiro nome; (3) endereços → asteriscos, manter
números/pontuação; (4) números não capturados → todos os caracteres; (5) preservar
exatamente espaços, pontuação e quebras de linha.

---

## 6. Dependências externas

| Pacote | Versão (v5) | Uso |
|---|---|---|
| `guzzlehttp/guzzle` | `^5.3` (lock: **5.3.4**) | Cliente HTTP das chamadas à API |
| `laravel/framework` | `5.5.*` | `FormRequest`, `response()->json()`, `env()`, `Log` |
| `spatie/laravel-activitylog` | `^2.8` | Registra a edição da configuração — **inclui o valor da chave em claro** |

Nenhuma dependência Composer específica do Google (SDK oficial `google/generative-ai`
**não** é usada): a integração é HTTP cru.

---

## 7. Diagrama de dependência entre módulos

```
                        ┌────────────────────────────┐
                        │  Módulo de Notícias /      │
                        │  Publicações (CMS)         │
                        └──────────┬─────────────────┘
                                   │ view + js
                        ┌──────────▼─────────────────┐
   rotas (portal) ────► │ GeminiController@preencher │──► Google Gemini
                        └──────────┬─────────────────┘
                                   │ lê
                        ┌──────────▼─────────────────┐
                        │ configurations.gemini_api_key│
                        └──────────▲─────────────────┘
                                   │ lê
   e-SIC / Ouvidoria ───►┌─────────┴────────────────┐
   (portal)              │ DataMaskTrait::maskTextWithAi │──► Google Gemini
                         └──────────┬─────────────────┘
                                    │ fallback
                                    ▼
                         maskTextDeterministic() (regex local)
```

---

## 8. Referências

- `v5/app/Http/Controllers/Prefecture/GeminiController.php:11-89`
- `v5/app/Http/Traits/DataMaskTrait.php:213-287`
- `v5/routes/web.php:202-203`
- `v5/app/Http/Kernel.php:30-44` (grupo `web`: sem `throttle`)
