# Arquitetura do Módulo de IA

## Visão Geral

O módulo `ai/` implementa um sistema de **Retrieval-Augmented Generation (RAG)** que permite ao usuário interagir com uma LLM através de um chat, fazendo perguntas sobre funções, estruturas, protocolos de rede e outros aspectos de um binário que passou por análise estática.

O sistema combina **busca semântica via embeddings** (pgvector) com **navegação estruturada via call graph** (xrefs) para fornecer contexto relevante à LLM durante a geração de respostas.

## Stack Tecnológica

| Componente | Tecnologia | Papel |
|---|---|---|
| **Framework** | Spring Boot 4.0.6 + Spring Modulith 2.0.6 | Base da aplicação, injeção de dependências, eventos |
| **AI Framework** | Spring AI 1.0.x | Abstração de LLM providers, embeddings, chat streaming |
| **Chat Local** | Gemma 4 12B (via Ollama) | LLM para chat executando localmente, 256K contexto |
| **Chat Cloud** | DeepSeek (`deepseek-chat`) | LLM alternativa via API cloud (OpenAI-compatible) |
| **Embeddings** | Qwen3 Embedding 8B (via Ollama) | Geração de embeddings para busca semântica, dims 32–4096 |
| **Vector Store** | pgvector (PostgreSQL 18) | Armazenamento e busca por similaridade de cosseno |
| **Vector Index** | HNSW (Hierarchical Navigable Small World) | Índice para busca vetorial rápida |
| **Message Broker** | RabbitMQ 4 | Processamento assíncrono de jobs de embedding |
| **Arquitetura** | Spring Modulith | Módulo desacoplado com eventos de domínio |

## Diagrama de Componentes

```
┌──────────────────────────────────────────────────────────────────────────┐
│                        ai.decompile.ai (Módulo AI)                        │
│                                                                            │
│  ┌──────────────────────┐    ┌──────────────────────┐                    │
│  │    EmbeddingService   │    │     ChatService       │                    │
│  │                      │    │                      │                    │
│  │  • embed(text)       │    │  • createSession()   │                    │
│  │  • embedBatch()      │    │  • chat(sessionId,   │                    │
│  │  • indexBinary(id)   │    │         message)      │                    │
│  │  • searchSimilar()   │    │  • getHistory()       │                    │
│  │  • deleteByBinary()  │    │  • deleteSession()   │                    │
│  └────────┬─────────────┘    └──────────┬───────────┘                    │
│           │                             │                                 │
│  ┌────────┴─────────────────────────────┴──────────────────┐             │
│  │                     RagService                            │             │
│  │                                                           │             │
│  │  • retrieveContext(binaryId, query) → List<Chunk>        │             │
│  │  • buildSystemPrompt(chunks, binaryMeta) → String        │             │
│  │  • expandCallGraph(functionId, hops) → List<Function>    │             │
│  │  • deduplicateAndRank(results) → List<Chunk>             │             │
│  └────────┬──────────────────────────────────────────────────┘             │
│           │                                                                │
│  ┌────────┴────────────────┬──────────────────────────────┐               │
│  │  EmbeddingChunkRepo     │  ChatSessionRepo             │               │
│  │  • findSimilar(vector,  │  • findByBinaryId()          │               │
│  │    binaryId, topK)      │  • findById()                │               │
│  │  • deleteByBinaryId()   │                              │               │
│  │                          │  ChatMessageRepo             │               │
│  │                          │  • findBySessionId()         │               │
│  └──────────────────────────┴──────────────────────────────┘               │
│                                                                            │
│  ┌──────────────────────────────────────────────────────────┐             │
│  │                  ChatController (REST + SSE)               │             │
│  │                                                            │             │
│  │  POST   /binaries/{id}/chat/sessions    — criar sessão    │             │
│  │  GET    /binaries/{id}/chat/sessions    — listar sessões  │             │
│  │  GET    /chat/sessions/{id}/messages    — histórico       │             │
│  │  POST   /chat/sessions/{id}/messages    — enviar (SSE)   │             │
│  │  DELETE /chat/sessions/{id}             — deletar sessão  │             │
│  └──────────────────────────────────────────────────────────┘             │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                      Infraestrutura Externa                                │
│                                                                            │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐                  │
│  │   Ollama     │   │  DeepSeek    │   │  PostgreSQL  │                  │
│  │  (localhost) │   │  (Cloud API) │   │  (pgvector)  │                  │
│  │              │   │              │   │              │                  │
│  │ qwen3-embed  │   │ deepseek-    │   │ embedding_   │                  │
│  │ gemma4:12b   │   │ chat         │   │ chunk        │                  │
│  └──────────────┘   └──────────────┘   │ chat_session │                  │
│                                        │ chat_message │                  │
│  ┌──────────────┐                      └──────────────┘                  │
│  │  RabbitMQ    │                                                         │
│  │              │                                                         │
│  │ decompile.   │                                                         │
│  │ jobs.queue   │                                                         │
│  └──────────────┘                                                         │
└──────────────────────────────────────────────────────────────────────────┘
```

## Decisões Arquiteturais

### 1. Uso do Spring AI como Camada de Abstração

**Decisão**: Utilizar Spring AI 1.0.x como camada de abstração para providers de LLM e embeddings.

**Justificativa**:
- Fornece interfaces padronizadas (`ChatClient`, `StreamingChatClient`, `EmbeddingModel`)
- Suporte nativo a streaming via `Flux<String>` (Reactor)
- Auto-configuração para Ollama e OpenAI (DeepSeek é compatível com API da OpenAI)
- Integração com ecossistema Spring Boot

**Trade-off**: Spring AI é relativamente novo e pode ter breaking changes entre versões. Mitigamos encapsulando os chamados diretos atrás dos nossos serviços (`EmbeddingService`, `ChatService`).

### 2. Embedding Store Próprio (não Spring AI VectorStore)

**Decisão**: Implementar repositório pgvector próprio (`EmbeddingChunkRepository`) em vez de usar `spring-ai-pgvector-store`.

**Justificativa**:
- Schema altamente específico ao domínio (FK para `binary_id`, `source_id` para `static_function.id`)
- Necessidade de campos de metadados customizados em `jsonb`
- Semântica de deleção específica (hard delete por `binary_id` na re-indexação)
- Suporte a múltiplas estratégias de retrieval: busca semântica (pgvector `<=>`), busca textual (ILIKE fallback), call graph expansion

### 2.1 Hybrid Retrieval

**Decisão**: Pipeline de retrieval com três camadas: semântica → textual fallback → call graph.

**Fluxo**:
1. **Semântica**: pgvector cosine similarity (top-K)
2. **Fallback**: se < 40% do topK preenchido, busca textual via `LIKE` nos chunks
3. **Call Graph**: detecta funções/endereços na query, expande 1-hop via xrefs, inclui assembly inline

**Justificativa**:
- Queries com nomes específicos de função (`sub_f5`, `0x401000`) frequentemente falham na busca semântica (nome curto, sem contexto)
- O fallback textual captura menções diretas ao nome da função nos chunks
- A expansão por call graph garante que a função requisitada SEMPRE apareça no contexto, mesmo sem embedding

### 3. Indexação Assíncrona via Job Separado

**Decisão**: Geração de embeddings como job assíncrono (`GENERATE_EMBEDDINGS`) via RabbitMQ, desacoplado do job de análise estática.

**Justificativa**:
- Análise estática e embedding são operações com perfis de falha diferentes
- Permite re-indexar sem re-analisar (ex: trocar modelo de embedding)
- Permite retry independente se a API de embedding falhar
- Segue o padrão arquitetural já estabelecido (ANALYZE_FILE, STATIC_ANALYSIS)

### 4. Multi-Provider com Seleção por Sessão

**Decisão**: Suporte a múltiplos providers de LLM (Ollama/Gemma4 + DeepSeek) com seleção no momento da criação da sessão.

**Justificativa**:
- Usuário pode escolher entre privacidade/local (Ollama) ou qualidade/cloud (DeepSeek)
- Providers são "pluggable" — adicionar novo provider requer apenas implementar a interface e registrar
- A seleção é persistida na `chat_session.chat_model` e usada em todas as mensagens daquela sessão

### 5. Streaming via Server-Sent Events (SSE)

**Decisão**: Respostas do chat utilizam SSE para streaming de tokens.

**Justificativa**:
- UX muito melhor que esperar resposta completa
- Nativo do Spring WebMVC / Reactor (`Flux<ServerSentEvent<String>>`)
- Compatível com `StreamingChatClient` do Spring AI
- Fácil de consumir no frontend (`EventSource` API)

### 6. Chunking por Função (não por tamanho fixo)

**Decisão**: Cada função estática vira um chunk de embedding. Funções muito grandes são truncadas em `chunk-max-chars` (default 3000).

**Justificativa**:
- Alinha com a unidade semântica natural da análise de binários (a função)
- Permite que metadados de call graph (callers/callees) enriqueçam cada chunk
- Facilita a expansão via navegação de xrefs (saber exatamente qual função foi recuperada)
- Simplifica o tracking (`source_type=STATIC_FUNCTION`, `source_id=function.id`)

## Módulos Spring Modulith

### Dependências do módulo `ai/`

```
ai/
 ├── allowedDependencies:
 │   ├── workspace::entities    (Binary, Project para relações FK)
 │   ├── analysis::entities     (StaticFunction, StaticAnalysis, etc.)
 │   ├── analysis::services     (AnalysisService para buscar funções/xrefs)
 │   └── common                 (exceções, configurações compartilhadas)
```

### Dependências do módulo `job/` (atualizado)

```
job/
 ├── allowedDependencies (novos):
 │   ├── ...
 │   ├── ai::services           (EmbeddingService para o handler de embeddings)
 │   ├── ai::dto                (tipos de dados do módulo AI)
 │   └── ai::events             (eventos publicados pelo módulo AI)
```

### Dependências do módulo `analysis/` (atualizado)

```
analysis/
 ├── allowedDependencies (novos):
 │   ├── ...
 │   └── ai::events             (para publicar StaticAnalysisCompletedEvent)
      └── NOTA: o evento será definido no analysis/event para evitar dependência circular
```

## Fluxo de Eventos Inter-Módulos

```
workspace        analysis           job              ai
   │                │                │                │
   │  BinaryUp-     │                │                │
   │  loadedEvent───┤                │                │
   │                │                │                │
   │                │  StaticAnalysisRequestedEvent   │
   │                ├───────────────►│                │
   │                │                │  STATIC_       │
   │                │                │  ANALYSIS      │
   │                │                │  (handler)     │
   │                │                │                │
   │                │◄───────────────┤  saveResult()  │
   │                │                │                │
   │                │  StaticAnalysisCompletedEvent   │
   │                ├────────────────────────────────►│
   │                │                │                │
   │                │                │  GENERATE_     │
   │                │                │  EMBEDDINGS    │
   │                │                │  (handler)────►│
   │                │                │                │
   │                │                │◄───────────────│
   │                │                │  indexBinary() │
```

> **Nota**: O `StaticAnalysisCompletedEvent` será definido em `analysis.event` e o subscriber residirá em `job.service` (padrão consistente com `StaticAnalysisRequestedEvent` + `StaticAnalysisEventSubscriber`).

## Configuração Multi-Provider

O módulo suporta dois providers de chat e um provider de embeddings, todos abstraídos pelo Spring AI:

```
                    ┌─────────────────────┐
                    │   ChatClientProvider │  (interface própria)
                    │                     │
                    │ getClient(model)─────┤
                    └──────────┬──────────┘
                               │
               ┌───────────────┼───────────────┐
               │                               │
    ┌──────────┴──────────┐     ┌──────────────┴──────────┐
    │ DeepSeekChatClient   │     │ OllamaChatClient        │
    │ (OpenAiChatModel)   │     │ (OllamaChatModel)       │
    │                     │     │                         │
    │ base-url:           │     │ base-url:               │
    │ api.deepseek.com    │     │ localhost:11434         │
    │ model: deepseek-chat│     │ model: gemma4:12b       │
    └─────────────────────┘     └─────────────────────────┘

                    ┌─────────────────────┐
                    │  EmbeddingModel     │  (Spring AI interface)
                    │                     │
                    │  embed(docs)        │
                    └──────────┬──────────┘
                               │
                    ┌──────────┴──────────┐
                    │ OllamaEmbeddingModel │
                    │                      │
                    │ base-url:            │
                    │ localhost:11434      │
                    │ model: qwen3-embedding│
                    └──────────────────────┘
```

## Considerações de Performance

| Aspecto | Estratégia |
|---|---|
| **Embedding em batch** | Funções enviadas em lotes de 20 (`app.ai.embedding.batch-size`) |
| **Busca vetorial** | Índice HNSW com `vector_cosine_ops` — sub-100ms para milhares de vetores |
| **Contexto da LLM** | Truncagem de chunks + histórico para caber em `max-context-tokens` (8000 default) |
| **Streaming** | Tokens fluem via SSE assim que gerados, sem buffer completo |
| **Conexões Ollama** | Pool de conexões HTTP gerenciado pelo Spring AI |
| **Virtual Threads** | Habilitados (`spring.threads.virtual.enabled: true`) — I/O de rede não bloqueia |

## Segurança

- API keys (DeepSeek) armazenadas exclusivamente em `.env`, nunca no repositório
- `.env` listado no `.gitignore`
- Nenhuma chave ou segredo em plaintext no `application.yml`
- Variáveis de ambiente referenciadas como `${DEEPSEEK_API_KEY}` com fallback vazio
