docs: add AI module documentation and AGENTS.md

Comprehensive documentation for the AI module covering architecture,
API design, data model, RAG pipeline, configuration, integration points,
and frontend integration guide. Includes IDA5 extraction points reference
and AGENTS.md with project conventions and coding standards.
This commit is contained in:
Rodrigo Verdiani 2026-06-08 20:13:21 -03:00
parent 198d9f333b
commit 90ee74b3b9
9 changed files with 3472 additions and 0 deletions

189
AGENTS.md Normal file
View File

@ -0,0 +1,189 @@
# AGENTS.md — decompile-ai
## Project Overview
**decompile-ai** is a backend service for binary analysis automation. It accepts uploaded binaries, runs them through multiple analysis engines (IDA Pro, Ghidra, DiE) inside Docker containers, and provides AI-powered chat over the decompiled code via Ollama + RAG (pgvector embeddings).
- **Java 25**, **Spring Boot 4.0.6**, **Maven** wrapper (`./mvnw`)
- **PostgreSQL** + **Flyway** migrations; **H2** in-memory for tests
- **RabbitMQ** for async job dispatch
- **Spring Modulith** for module boundaries, event publication, and architecture verification
- **Spring AI** (Ollama) for chat and embedding generation
- **Spotless** (Google Java Format) for code formatting
## Build & Run Commands
```bash
# Compile
./mvnw compile
# Run tests
./mvnw test
# Run a specific test class
./mvnw test -Dtest=ChatServiceTest
# Package
./mvnw package -DskipTests
# Run the app locally (needs PostgreSQL + RabbitMQ)
./mvnw spring-boot:run
# Format code
./mvnw spotless:apply
# Check formatting (CI-style)
./mvnw spotless:check
```
## Project Structure
```
src/main/java/ai/decompile/
├── DecompileAiApplication.java # Spring Boot entry point
├── WebConfig.java # CORS configuration
├── workspace/ # Workspace, Project, Binary CRUD
│ ├── controller/
│ ├── event/ # Domain events (BinaryUploadedEvent)
│ ├── model/{dto,entity,repository,specification}/
│ └── service/
├── analysis/ # Static analysis results (functions, xrefs, labels)
│ ├── controller/
│ ├── event/ # StaticAnalysisRequestedEvent, StaticAnalysisCompletedEvent
│ ├── model/{dto,entity,repository}/
│ └── service/
├── engine/ # Analysis engines: IDA v5, Ghidra (Docker-managed)
│ ├── config/
│ ├── model/
│ └── service/
├── die/ # DiE (Detect It Easy) file type detection (Docker)
├── docker/ # Docker container management via docker-java
├── job/ # Async job queue: create, dispatch, execute, track status
│ ├── controller/
│ ├── messaging/ # RabbitMQ publisher + listener
│ ├── model/{dto,entity,enums,repository,specification}/
│ └── service/handler/ # JobType handlers (AnalyzeFile, StaticAnalysis, Embedding)
├── ai/ # AI chat + RAG embeddings (Spring AI + Ollama)
│ ├── config/
│ ├── controller/
│ ├── model/{dto,entity,jdbc,repository}/
│ └── service/
└── common/ # Shared config and exceptions (NotFoundException, ConflictException)
└── exception/
```
## Coding Conventions
### Null Checking
Always use `java.util.Objects.isNull()` and `java.util.Objects.nonNull()`**never** use raw `== null` or `!= null`.
```java
// Correct
if (Objects.isNull(value)) { ... }
if (Objects.nonNull(value)) { ... }
// Wrong
if (value == null) { ... }
if (value != null) { ... }
```
### If Statements
Always use braces, even for single-line bodies. Always include a blank line after the closing `}` of an `if` block (before the next statement).
```java
// Correct
if (condition) {
doSomething();
}
nextThing();
// Wrong
if (condition) doSomething();
// Wrong (no blank line after if)
if (condition) {
doSomething();
}
nextThing();
```
### Classes and Records
No blank line between the class/record opening brace and the first member.
```java
// Correct
public class Job {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// Correct
public record ChatRequest(@NotBlank String content) {}
// Wrong (blank line after brace)
public class Job {
@Id
private UUID id;
```
### Type Inference (var)
Use `var` for local variables whenever the type is obvious from the right-hand side.
```java
// Correct
var session = chatService.createSession(binaryId, projectId, model);
var messages = new ArrayList<Message>();
var binary = binaryService.getBinary(binaryId);
// Acceptable (type not obvious from RHS, or lambda target typing needed)
StreamingChatModel streamingModel = resolveStreamingChatModel(session.getChatModel());
```
### Dependency Injection
Constructor injection via Lombok `@RequiredArgsConstructor`. Declare dependencies as `private final`.
```java
@Service
@RequiredArgsConstructor
public class MyService {
private final DependencyA depA;
private final DependencyB depB;
}
```
### Transactions
- `@Transactional` on write operations
- `@Transactional(readOnly = true)` on read-only queries
- `@Transactional(propagation = Propagation.REQUIRES_NEW)` for independent sub-transactions
### Testing
- **JUnit 5** + **Mockito** (unit tests)
- **MockMvc** + H2 (integration tests)
- Unit tests: `@ExtendWith(MockitoExtension.class)`, `@Mock` / `@InjectMocks`
- Integration tests: `@SpringBootTest`, `@ActiveProfiles("test")`
- Test class naming: `*Test.java` (unit), `*IntegrationTest.java` (integration)
- Static imports for assertions: `assertEquals(...)`, `assertThrows(...)`, `assertTrue(...)`
### Logging
Lombok `@Log4j2` on all non-trivial classes. Use parameterized messages:
```java
log.info("Processing binary={} with engine={}", binaryId, engineName);
log.error("Failed to stream chat for session {}", sessionId, error);
```
### Domain Events (Spring Modulith)
Event listeners use `@ApplicationModuleListener`. Events are published via `ApplicationEventPublisher.publishEvent()`.
### Flyway Migrations
- Files in `src/main/resources/db/migration/`, naming: `V001__description.sql`
- DDL changes go through Flyway; Hibernate `ddl-auto=validate` only
## Design Principles
- **DRY** — Extract repeated logic. The refactoring of `ChatController.sendMessage()` is the canonical example: duplicated error formatting was extracted into reusable `event()` and `errorEvent()` helpers.
- **SOLID** — Each module (workspace, analysis, engine, job, ai) has a single responsibility. Dependencies between modules are explicitly declared in `package-info.java`.
- **Clean Code** — Small methods, meaningful names, no comments explaining *what* (the code does that), only comments explaining *why* when non-obvious.
- **Module boundaries** — Cross-module communication via domain events, not direct service calls. Use `@NamedInterface` for fine-grained intra-module access control.

404
docs/ai/api-design.md Normal file
View File

@ -0,0 +1,404 @@
# API Design — Módulo AI
## Endpoints
Todos os endpoints são prefixados com o contexto da aplicação e seguem as convenções REST do projeto (OpenAPI 3.1 via SpringDoc).
| Método | Path | Descrição | Tag |
|---|---|---|---|
| `POST` | `/binaries/{binaryId}/chat/sessions` | Criar nova sessão de chat | Chat |
| `GET` | `/binaries/{binaryId}/chat/sessions` | Listar sessões do binário | Chat |
| `GET` | `/chat/sessions/{sessionId}` | Detalhes da sessão | Chat |
| `DELETE` | `/chat/sessions/{sessionId}` | Deletar sessão | Chat |
| `GET` | `/chat/sessions/{sessionId}/messages` | Histórico de mensagens | Chat |
| `POST` | `/chat/sessions/{sessionId}/messages` | Enviar mensagem (SSE stream) | Chat |
---
## 1. Criar Sessão
### `POST /binaries/{binaryId}/chat/sessions`
Cria uma nova sessão de chat vinculada a um binário.
**Query Parameters**:
| Parâmetro | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
| `model` | String | Não | `app.ai.default-chat-provider` | Modelo LLM: `gemma4:12b`, `gemma4:e4b`, `deepseek-chat` |
**Request Body**: Vazio
**Response** (`201 Created`):
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"binaryId": "660e8400-e29b-41d4-a716-446655440001",
"projectId": "770e8400-e29b-41d4-a716-446655440002",
"title": "malware.exe — 2026-06-08T15:30:00Z",
"chatModel": "gemma4:12b",
"messageCount": 0,
"createdAt": "2026-06-08T15:30:00Z",
"updatedAt": "2026-06-08T15:30:00Z"
}
```
**Erros**:
| Código | Condição |
|---|---|
| `404` | Binário não encontrado |
| `400` | Modelo não suportado (ex: `?model=invalid-model`) |
**Exemplo curl**:
```bash
curl -X POST "http://localhost:8080/binaries/{binaryId}/chat/sessions?model=deepseek-chat"
```
---
## 2. Listar Sessões
### `GET /binaries/{binaryId}/chat/sessions`
Lista todas as sessões de chat de um binário, ordenadas por data de atualização (mais recente primeiro).
**Response** (`200 OK`):
```json
[
{
"id": "550e8400-...",
"binaryId": "660e8400-...",
"projectId": "770e8400-...",
"title": "Analisando funções de rede",
"chatModel": "deepseek-chat",
"messageCount": 12,
"createdAt": "2026-06-08T15:30:00Z",
"updatedAt": "2026-06-08T16:45:00Z"
},
{
"id": "550e8401-...",
"binaryId": "660e8400-...",
"projectId": "770e8400-...",
"title": "Investigando crypto",
"chatModel": "gemma4:12b",
"messageCount": 5,
"createdAt": "2026-06-08T14:00:00Z",
"updatedAt": "2026-06-08T14:20:00Z"
}
]
```
**Exemplo curl**:
```bash
curl "http://localhost:8080/binaries/{binaryId}/chat/sessions"
```
---
## 3. Detalhes da Sessão
### `GET /chat/sessions/{sessionId}`
Retorna os detalhes de uma sessão específica.
**Response** (`200 OK`):
```json
{
"id": "550e8400-...",
"binaryId": "660e8400-...",
"projectId": "770e8400-...",
"title": "Analisando funções de rede",
"chatModel": "deepseek-chat",
"messageCount": 12,
"createdAt": "2026-06-08T15:30:00Z",
"updatedAt": "2026-06-08T16:45:00Z"
}
```
**Erros**: `404` se sessão não encontrada.
---
## 4. Deletar Sessão
### `DELETE /chat/sessions/{sessionId}`
Deleta a sessão e todas as suas mensagens (cascade).
**Response**: `204 No Content`
**Exemplo curl**:
```bash
curl -X DELETE "http://localhost:8080/chat/sessions/{sessionId}"
```
---
## 5. Histórico de Mensagens
### `GET /chat/sessions/{sessionId}/messages`
Retorna o histórico de mensagens da sessão, ordenado cronologicamente (mais antigo primeiro).
**Response** (`200 OK`):
```json
[
{
"id": "aa0e8400-...",
"sessionId": "550e8400-...",
"role": "USER",
"content": "Quais funções lidam com rede?",
"tokenCount": 12,
"createdAt": "2026-06-08T15:30:30Z"
},
{
"id": "bb0e8400-...",
"sessionId": "550e8400-...",
"role": "ASSISTANT",
"content": "O binário importa funções de rede da WS2_32.dll. As funções sub_401200 e sub_401300 utilizam send() e recv()...",
"tokenCount": 85,
"createdAt": "2026-06-08T15:30:35Z"
}
]
```
**Erros**: `404` se sessão não encontrada.
---
## 6. Enviar Mensagem (SSE Streaming)
### `POST /chat/sessions/{sessionId}/messages`
Envia uma mensagem do usuário e retorna a resposta da LLM em streaming via Server-Sent Events (SSE).
**Headers**:
```
Content-Type: application/json
Accept: text/event-stream
```
**Request Body**:
```json
{
"content": "Quais funções nesse binário lidam com criptografia?"
}
```
**Response**: `200 OK` com `Content-Type: text/event-stream`
### Protocolo SSE
O stream consiste em uma sequência de eventos `data:`:
#### Evento: `chunk`
Fragmento de texto da resposta, enviado assim que gerado pela LLM:
```
data: {"type":"chunk","content":"O binário"}
data: {"type":"chunk","content":" utiliza "}
data: {"type":"chunk","content":"as seguintes"}
data: {"type":"chunk","content":" funções"}
data: {"type":"chunk","content":" relacionadas"}
data: {"type":"chunk","content":" a criptografia:"}
data: {"type":"chunk","content":"\n\n1. **sub_405000**"}
data: {"type":"chunk","content":" em 0x405000"}
...
```
#### Evento: `done`
Indica o fim do stream, com metadados da mensagem:
```
data: {"type":"done","messageId":"cc0e8400-e29b-...","sessionId":"550e8400-...","tokenCount":142}
```
#### Evento: `error`
Indica um erro durante o processamento:
```
data: {"type":"error","code":"EMBEDDING_FAILED","message":"Failed to generate embedding: Ollama connection refused"}
```
Após um evento `error`, a conexão é fechada. Nenhum evento `done` é enviado.
### Tipos de Evento SSE
| `type` | Descrição | Campos |
|---|---|---|
| `chunk` | Fragmento de texto da resposta | `content: String` |
| `done` | Stream concluído com sucesso | `messageId: UUID`, `sessionId: UUID`, `tokenCount: Integer` |
| `error` | Erro durante o processamento | `code: String`, `message: String` |
### Fluxo Interno Durante o SSE
```
1. Salva UserMessage (role=USER) no banco
2. Embedda a query do usuário (qwen3-embedding)
3. Busca similaridade no pgvector (top-K chunks)
4. (Opcional) Expande call graph
5. Constrói system prompt com contexto RAG
6. Monta lista de mensagens: [SystemPrompt, history, UserMessage]
7. Chama StreamingChatClient da LLM
8. Para cada token gerado: envia SSE "chunk"
9. Ao finalizar: salva AssistantMessage (role=ASSISTANT) no banco
10. Envia SSE "done" com messageId + tokenCount
11. Se erro em qualquer passo: envia SSE "error" e fecha conexão
```
### Exemplo curl (com streaming)
```bash
curl -X POST "http://localhost:8080/chat/sessions/{sessionId}/messages" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"content": "Explique a função sub_401000"}'
```
### Implementação no Controller
```java
@PostMapping(value = "/chat/sessions/{sessionId}/messages",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a message and stream the AI response",
tags = {"Chat"})
public Flux<ServerSentEvent<String>> sendMessage(
@PathVariable UUID sessionId,
@Valid @RequestBody ChatRequest request) {
return chatService.chat(sessionId, request.content())
.map(token -> ServerSentEvent.<String>builder()
.data("{\"type\":\"chunk\",\"content\":\"" + escapeJson(token) + "\"}")
.build())
.concatWith(chatService.finalizeMessage(sessionId)
.map(msg -> ServerSentEvent.<String>builder()
.data("{\"type\":\"done\",\"messageId\":\"" + msg.id() + "\","
+ "\"sessionId\":\"" + sessionId + "\","
+ "\"tokenCount\":" + msg.tokenCount() + "}")
.build()))
.onErrorResume(e -> Flux.just(
ServerSentEvent.<String>builder()
.data("{\"type\":\"error\",\"code\":\"INTERNAL\","
+ "\"message\":\"" + escapeJson(e.getMessage()) + "\"}")
.build()));
}
```
---
## DTOs
### `ChatRequest`
```java
public record ChatRequest(
@NotBlank @Size(min = 1, max = 4000)
String content
) {}
```
### `ChatResponse` (evento `done`)
```java
public record ChatResponse(
UUID messageId,
UUID sessionId,
int tokenCount
) {}
```
### `SessionResponse`
```java
public record SessionResponse(
UUID id,
UUID binaryId,
UUID projectId,
String title,
String chatModel,
int messageCount,
Instant createdAt,
Instant updatedAt
) {}
```
### `MessageResponse`
```java
public record MessageResponse(
UUID id,
UUID sessionId,
String role,
String content,
Integer tokenCount,
Instant createdAt
) {}
```
### `StreamChunk` (evento SSE `chunk`)
```java
public record StreamChunk(
String type, // "chunk", "done", "error"
String content // null para "done" e "error"
) {}
```
---
## Tratamento de Erros
| Código HTTP | `type` SSE | Condição |
|---|---|---|
| `400` | — | Request body inválido (content vazio, >4000 chars) |
| `404` | — | Sessão não encontrada |
| `404` | — | Binário não encontrado |
| `503` | `error` | Ollama indisponível (embeddings ou chat) |
| `503` | `error` | DeepSeek API indisponível |
| `500` | `error` | Erro interno (pgvector, RabbitMQ, etc.) |
| `402` | `error` | DeepSeek API: saldo insuficiente / quota excedida |
### Resposta de erro padrão (não-SSE)
```json
{
"status": 404,
"message": "Chat session with id '550e8400-...' not found",
"timestamp": "2026-06-08T15:30:00Z"
}
```
---
## OpenAPI / Swagger
Todos os endpoints são documentados com `@Operation` e aparecem no Swagger UI (`/swagger-ui.html`).
**Tag**: `Chat`
**Schemas**:
- `ChatRequest`
- `SessionResponse`
- `MessageResponse`
- `StreamChunk` (não documentado como schema, pois é SSE)

281
docs/ai/architecture.md Normal file
View File

@ -0,0 +1,281 @@
# 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 324096 |
| **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

438
docs/ai/configuration.md Normal file
View File

@ -0,0 +1,438 @@
# Guia de Configuração — Módulo AI
## Pré-requisitos
| Componente | Como obter | Status |
|---|---|---|
| **Ollama** (host) | `curl -fsSL https://ollama.com/install.sh \| sh` | Local, necessário |
| **qwen3-embedding** | `ollama pull qwen3-embedding:latest` | Modelo de embedding |
| **gemma4:12b** | `ollama pull gemma4:12b` | LLM para chat local |
| **DeepSeek API key** | [platform.deepseek.com](https://platform.deepseek.com) | LLM cloud (opcional) |
| **PostgreSQL + pgvector** | `docker compose up -d postgres` | Já configurado |
| **RabbitMQ** | `docker compose up -d rabbit-mq` | Já configurado |
## 1. Setup do Ollama (Host)
### Instalação
```bash
# Linux
curl -fsSL https://ollama.com/install.sh | sh
# Verificar se está rodando
ollama --version
curl http://localhost:11434/api/tags
```
### Download dos Modelos
```bash
# Embedding (4.7 GB)
ollama pull qwen3-embedding:latest
# Chat local (7.6 GB)
ollama pull gemma4:12b
# Alternativa menor para máquinas com pouca RAM (9.6 GB, MoE)
# ollama pull gemma4:e4b
# Verificar modelos instalados
ollama list
```
**Hardware recomendado**:
| Modelo | RAM mínima | RAM recomendada | GPU VRAM (opcional) |
|---|---|---|---|
| `qwen3-embedding:latest` (8B) | 8 GB | 16 GB | 6 GB |
| `gemma4:12b` | 10 GB | 16 GB | 8 GB |
| `gemma4:e4b` | 6 GB | 12 GB | 6 GB |
### Configuração do Ollama
Por padrão, o Ollama escuta em `localhost:11434`. Se precisar acessar de outro host:
```bash
# Editar /etc/systemd/system/ollama.service ou usar env var
export OLLAMA_HOST=0.0.0.0:11434
```
Para otimizar performance com GPU (se disponível):
```bash
# Verificar se GPU está sendo usada
ollama run gemma4:12b --verbose
# Saída: "total duration: ..." — procure por "eval rate: XX tokens/s"
```
## 2. Arquivo `.env`
Criar na raiz do projeto (`backend/.env`):
```bash
# ============================================================
# Decompile-AI — Environment Variables
# ============================================================
# --- LLM Providers ---
# DeepSeek API Key (obter em https://platform.deepseek.com)
DEEPSEEK_API_KEY=sk-your-api-key-here
# --- Override de modelos (opcional) ---
# Provedor padrão de chat: ollama | deepseek
AI_DEFAULT_CHAT_PROVIDER=ollama
# Modelo de chat Ollama (padrão: gemma4:12b)
# AI_CHAT_MODEL_OLLAMA=gemma4:12b
# Modelo de chat DeepSeek (padrão: deepseek-chat)
# AI_CHAT_MODEL_DEEPSEEK=deepseek-chat
# Modelo de embedding (padrão: qwen3-embedding:latest)
# AI_EMBEDDING_MODEL=qwen3-embedding:latest
# Dimensão do embedding (padrão: 1024, max 4096 para qwen3)
# AI_EMBEDDING_DIMENSION=1024
# --- Database ---
# Sobrescrever credenciais do Postgres se necessário
# SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/decompile_ai
# SPRING_DATASOURCE_USERNAME=decompile_ai
# SPRING_DATASOURCE_PASSWORD=decompile_ai
```
**Importante**: Adicione `.env` ao `.gitignore`:
```bash
echo ".env" >> .gitignore
```
## 3. Carregamento do `.env`
O Spring Boot precisa saber como carregar o `.env`. Opções:
### Opção A: `spring.config.import` (nativo Spring Boot)
Adicionar ao `application.yml`:
```yaml
spring:
config:
import: optional:file:.env[.properties]
```
Isso carrega o `.env` como um arquivo de propriedades. **Limitação**: não suporta substituição de variáveis inline como `${VAR}` dentro de outras variáveis no `.env`, mas funciona bem para definir valores.
### Opção B: Plugin `dotenv` (recomendado para desenvolvimento)
Adicionar dependência ao `pom.xml`:
```xml
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>4.0.0</version>
</dependency>
```
Suporta interpolação de variáveis e convenção `.env` padrão.
### Opção C: Export manual (desenvolvimento)
```bash
set -a && source .env && set +a
./mvnw spring-boot:run
```
## 4. `application.yml` — Configuração do Módulo AI
### Blocos a adicionar ao arquivo existente
```yaml
spring:
# ... configurações existentes (datasource, jpa, flyway, rabbitmq) ...
ai:
ollama:
base-url: http://localhost:11434
embedding:
options:
model: ${AI_EMBEDDING_MODEL:qwen3-embedding:latest}
chat:
options:
model: ${AI_CHAT_MODEL_OLLAMA:gemma4:12b}
temperature: 0.3 # respostas determinísticas para análise técnica
top-p: 0.95
top-k: 64
openai:
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: ${AI_CHAT_MODEL_DEEPSEEK:deepseek-chat}
temperature: 0.3
max-tokens: 4096
app:
# ... configurações existentes (storage, rabbitmq, docker, die, engines) ...
ai:
# Provedor padrão de chat quando a sessão não especifica modelo
default-chat-provider: ${AI_DEFAULT_CHAT_PROVIDER:ollama}
# --- Configurações de Embedding ---
embedding:
# Dimensão do vetor (qwen3-embedding suporta 32-4096)
# DEVE corresponder à coluna vector(N) na migration V008
dimension: ${AI_EMBEDDING_DIMENSION:1024}
# Tamanho máximo do chunk em caracteres
chunk-max-chars: 3000
# Quantas funções processar por batch na API de embedding
batch-size: 20
# --- Configurações de RAG ---
rag:
# Quantos chunks retornar na busca semântica
top-k: 10
# Similaridade mínima para incluir um chunk (0.0 a 1.0)
similarity-threshold: 0.6
# Máximo de tokens de contexto enviados à LLM
max-context-tokens: 8000
# --- Configurações de Chat ---
chat:
# Quantas mensagens de histórico incluir no prompt
max-history-messages: 20
# Temperatura padrão para geração
temperature: 0.3
```
### Configuração completa do `application.yml` (após merge)
```yaml
spring:
threads:
virtual:
enabled: true
application:
name: decompile-ai
config:
import: optional:file:.env[.properties]
datasource:
url: jdbc:postgresql://localhost:5432/decompile_ai
username: decompile_ai
password: decompile_ai
jpa:
hibernate:
ddl-auto: validate
flyway:
enabled: true
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
ai:
ollama:
base-url: http://localhost:11434
embedding:
options:
model: ${AI_EMBEDDING_MODEL:qwen3-embedding:latest}
chat:
options:
model: ${AI_CHAT_MODEL_OLLAMA:gemma4:12b}
temperature: 0.3
top-p: 0.95
top-k: 64
openai:
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: ${AI_CHAT_MODEL_DEEPSEEK:deepseek-chat}
temperature: 0.3
max-tokens: 4096
app:
storage:
upload-dir: ./uploads/binaries
rabbitmq:
exchange: decompile.jobs
queue: decompile.jobs.queue
routing-key: decompile.jobs.created
docker:
enabled: true
die:
image: decompile-ai/diec:latest
timeout-seconds: 60
docker-host: unix:///var/run/docker.sock
engines:
ida5:
image: decompile-ai/ida5:latest
timeout-seconds: 300
ghidra:
image: decompile-ai/ghidra:latest
timeout-seconds: 600
ai:
default-chat-provider: ${AI_DEFAULT_CHAT_PROVIDER:ollama}
embedding:
dimension: ${AI_EMBEDDING_DIMENSION:1024}
chunk-max-chars: 3000
batch-size: 20
rag:
top-k: 10
similarity-threshold: 0.6
max-context-tokens: 8000
chat:
max-history-messages: 20
temperature: 0.3
logging:
level:
org.springframework.modulith: DEBUG
ai.decompile.ai: DEBUG # Logs do módulo AI
org.springframework.ai: INFO # Logs do Spring AI
```
## 5. `docker-compose.yml` (sem alterações)
O `docker-compose.yml` atual já atende todos os requisitos:
```yaml
services:
postgres:
container_name: decompile-ai-postgres
image: pgvector/pgvector:pg18 # PostgreSQL 18 + pgvector nativo
environment:
- POSTGRES_USER=decompile_ai
- POSTGRES_PASSWORD=decompile_ai
- POSTGRES_DB=decompile_ai
ports:
- "5432:5432"
volumes:
- postgres:/var/lib/postgresql
networks:
- decompile-ai-network
rabbit-mq:
container_name: decompile-ai-rabbitmq
image: rabbitmq:4-management
ports:
- "5672:5672"
- "15672:15672"
networks:
- decompile-ai-network
networks:
decompile-ai-network:
driver: bridge
volumes:
postgres:
```
**Não adicionamos Ollama ao docker-compose** porque ele roda no host.
## 6. Startup
### Ordem de inicialização
```bash
# 1. Infraestrutura Docker
docker compose up -d postgres rabbit-mq
# 2. Verificar Ollama no host
ollama list
# Deve mostrar: qwen3-embedding:latest, gemma4:12b
# 3. Configurar .env
cp .env.example .env
# Editar com a DEEPSEEK_API_KEY
# 4. Iniciar o backend
./mvnw spring-boot:run
# 5. Verificar health
curl http://localhost:8080/actuator/health
```
### Verificação de saúde dos componentes AI
```bash
# Ollama
curl http://localhost:11434/api/tags | jq '.models[].name'
# Testar embedding
curl http://localhost:11434/api/embed -d '{
"model": "qwen3-embedding:latest",
"input": "test"
}' | jq '.embeddings[0] | length'
# Deve retornar 1024 (ou dimensão configurada)
# Testar chat local
curl http://localhost:11434/api/chat -d '{
"model": "gemma4:12b",
"messages": [{"role": "user", "content": "Hello"}],
"stream": false
}' | jq '.message.content'
```
## 7. Troubleshooting
### Ollama não responde
```bash
# Verificar se está rodando
systemctl status ollama
# ou
ps aux | grep ollama
# Reiniciar se necessário
systemctl restart ollama
```
### Erro: "model not found"
```bash
# Listar modelos disponíveis
ollama list
# Baixar modelo faltante
ollama pull qwen3-embedding:latest
ollama pull gemma4:12b
```
### Erro: "DeepSeek API key not configured"
- Verificar se `.env` existe e contém `DEEPSEEK_API_KEY=sk-...`
- Verificar se `spring.config.import: optional:file:.env[.properties]` está no `application.yml`
- Alternativa: `export DEEPSEEK_API_KEY=sk-...` no terminal
### Erro: "relation 'vector_store' does not exist"
- Verificar se Flyway rodou a migration V008: `SELECT * FROM flyway_schema_history;`
- Rodar manualmente se necessário: `./mvnw flyway:migrate`
### Erro: "column 'embedding' type vector not supported"
- Verificar se a extensão pgvector está ativada: `SELECT * FROM pg_extension WHERE extname = 'vector';`
- Ativar manualmente: `CREATE EXTENSION IF NOT EXISTS vector;`
### Performance lenta nos embeddings
- Reduzir `batch-size` no `application.yml`
- Verificar se o Ollama está usando GPU: `ollama run qwen3-embedding --verbose` e checar `eval rate`
- Sem GPU, o modelo 8B pode ser lento para centenas de funções (esperar 2-5 minutos)
### Contexto da LLM estourando
- Reduzir `max-context-tokens` em `app.ai.rag`
- Reduzir `max-history-messages` em `app.ai.chat`
- Aumentar `chunk-max-chars` (para incluir menos chunks maiores)

369
docs/ai/data-model.md Normal file
View File

@ -0,0 +1,369 @@
# Modelo de Dados — Módulo AI
## Diagrama Entidade-Relacionamento
```
┌─────────────────────────────────────────────────────────────┐
│ TABELAS EXISTENTES │
│ │
│ binaries ──────┬── static_analysis ──┬── static_function │
│ (workspace) │ ├── static_label │
│ │ └── static_xref │
│ │ │
│ └── jobs │
└─────────────────────────────────────────────────────────────┘
│ FK: binary_id
┌─────────────────────────────────────────────────────────────┐
│ NOVAS TABELAS (V008) │
│ │
│ embedding_chunk ──── chat_session ──── chat_message │
│ │ │ │
│ │ │ FK: binary_id → binaries │
│ │ │ FK: project_id → projects │
│ │ │ │
│ │ FK: binary_id │ │
│ │ → binaries │ │
│ │ │ │
│ │ FK: source_id │ │
│ │ → opcional │ │
│ │ (static_func) │ │
└─────────────────────────────────────────────────────────────┘
```
## Migration V008 — DDL Completo
```sql
-- V008__ai_embedding_and_chat.sql
-- Garantir que a extensão pgvector está ativada
CREATE EXTENSION IF NOT EXISTS vector;
-- ============================================================
-- Tabela: embedding_chunk
-- Armazena chunks de texto embeddados para busca semântica
-- ============================================================
CREATE TABLE embedding_chunk (
id UUID PRIMARY KEY,
-- Referência ao binário (obrigatória)
binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,
-- Tipo de chunk: FUNCTION, METADATA
chunk_type VARCHAR(30) NOT NULL,
-- Tipo da fonte: STATIC_FUNCTION, IMPORT, STRING (extensível)
source_type VARCHAR(30),
-- FK opcional para a entidade fonte (ex: static_function.id)
source_id UUID,
-- Texto completo que foi embeddado
content TEXT NOT NULL,
-- Vetor de embedding (dimensão configurável, default 4096)
-- qwen3-embedding:latest (8B) retorna 4096 dimensões
embedding vector(4096),
-- Metadados flexíveis em JSON:
-- {
-- "function_name": "sub_401000",
-- "address": "0x401000",
-- "callers": ["WinMain", "sub_402300"],
-- "callees": ["CreateFileA", "WriteFile"],
-- "label_count": 2
-- }
metadata JSONB,
-- Contagem estimada de tokens (para controle de contexto)
token_count INTEGER,
-- Timestamp de criação
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice para busca por binário (filtro de escopo)
CREATE INDEX idx_embedding_chunk_binary ON embedding_chunk(binary_id);
-- Índice para lookup por fonte (ex: achar chunk de uma função específica)
CREATE INDEX idx_embedding_chunk_source ON embedding_chunk(source_type, source_id);
-- Índice de busca textual (fallback para queries específicas)
CREATE INDEX idx_embedding_chunk_content ON embedding_chunk
USING gin (content gin_trgm_ops);
-- NOTA: Não há índice vetorial. HNSW e IVFFlat do pgvector limitados a 2000d.
-- Sequential scan é rápido para < 10K vetores de 4096d.
-- ============================================================
-- Tabela: chat_session
-- Representa uma sessão de conversa com o chat IA
-- ============================================================
CREATE TABLE chat_session (
id UUID PRIMARY KEY,
-- Binário em análise (escopo da conversa)
binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,
-- Projeto pai (para navegação/breadcrumb)
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
-- Título da sessão (ex: "Analisando funções de rede")
title VARCHAR(255),
-- Modelo de chat usado nesta sessão
-- Ex: "gemma4:12b", "deepseek-chat", "gemma4:e4b"
chat_model VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_chat_session_binary ON chat_session(binary_id);
-- ============================================================
-- Tabela: chat_message
-- Mensagens individuais dentro de uma sessão
-- ============================================================
CREATE TABLE chat_message (
id UUID PRIMARY KEY,
-- Sessão pai
session_id UUID NOT NULL REFERENCES chat_session(id) ON DELETE CASCADE,
-- Papel do emissor: USER, ASSISTANT, SYSTEM
role VARCHAR(20) NOT NULL,
-- Conteúdo textual da mensagem
content TEXT NOT NULL,
-- Estimativa de tokens (para tracking de uso)
token_count INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice para buscar histórico em ordem cronológica
CREATE INDEX idx_chat_message_session ON chat_message(session_id, created_at);
```
## Entidades JPA
### `EmbeddingChunk`
```java
@Entity
@Table(name = "embedding_chunk")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class EmbeddingChunk {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "binary_id", nullable = false)
private UUID binaryId;
@Column(name = "chunk_type", nullable = false, length = 30)
private String chunkType;
// Valores: "FUNCTION", "METADATA"
@Column(name = "source_type", length = 30)
private String sourceType;
// Valores: "STATIC_FUNCTION", null (para METADATA)
@Column(name = "source_id")
private UUID sourceId;
// FK lógica para static_function.id (sem constraint de FK no banco
// porque pode ser null ou apontar para tabelas diferentes no futuro)
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@JdbcTypeCode(SqlTypes.VECTOR)
@Column(name = "embedding", columnDefinition = "vector(1024)")
private float[] embedding;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "metadata", columnDefinition = "jsonb")
private String metadata;
// JSON string: {"function_name":"...", "address":"...", "callers":[...], ...}
@Column(name = "token_count")
private Integer tokenCount;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
}
```
**Notas sobre o mapeamento**:
- `embedding` usa `@JdbcTypeCode(SqlTypes.VECTOR)` — requer Hibernate 6.2+ (Spring Boot 4.0.6 inclui)
- `metadata` é `String` com `@JdbcTypeCode(SqlTypes.JSON)` — Hibernate converte de/para JSONB automaticamente
- `sourceId` é FK lógica (não constraint no banco) para permitir flexibilidade futura (apontar para outras tabelas além de `static_function`)
### `ChatSession`
```java
@Entity
@Table(name = "chat_session")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class ChatSession {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "binary_id", nullable = false)
private UUID binaryId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "binary_id", insertable = false, updatable = false)
private Binary binary;
@Column(name = "project_id", nullable = false)
private UUID projectId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id", insertable = false, updatable = false)
private Project project;
@Column(length = 255)
private String title;
@Column(name = "chat_model", nullable = false, length = 100)
private String chatModel;
@OneToMany(mappedBy = "session", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<ChatMessage> messages = new ArrayList<>();
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}
```
### `ChatMessage`
```java
@Entity
@Table(name = "chat_message")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "session_id", nullable = false)
private ChatSession session;
@Column(nullable = false, length = 20)
private String role; // "USER", "ASSISTANT", "SYSTEM"
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(name = "token_count")
private Integer tokenCount;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
}
```
## Relacionamentos
```
chat_session (1) ────────── (N) chat_message
│ │
│ FK binary_id → binaries │ FK session_id → chat_session
│ FK project_id → projects │
│ │
└── cascade: ALL, orphanRemoval └── cascade: ALL, orphanRemoval
(deletar sessão deleta (deletar sessão deleta
todas as mensagens) todas as mensagens)
embedding_chunk (N) ────────── (1) binaries
│ FK binary_id → binaries
│ ON DELETE CASCADE (deletar binário deleta todos os chunks)
└── source_id: FK lógica para static_function.id
(não é constraint; usado para join manual)
```
## Índices e Performance
### Índices
| Índice | Coluna(s) | Uso |
|---|---|---|
| `idx_embedding_chunk_binary` | `binary_id` | Filtrar chunks por binário (todas as queries) |
| `idx_embedding_chunk_source` | `source_type, source_id` | Lookup por fonte (ex: achar chunk de uma função específica) |
| `idx_embedding_chunk_content` | `content` (GIN trgm) | Busca textual de fallback (`ILIKE '%term%'`) |
| `idx_chat_session_binary` | `binary_id` | Listar sessões de um binário |
| `idx_chat_message_session` | `session_id, created_at` | Buscar histórico em ordem cronológica |
> **Performance**: Sem índice vetorial (HNSW/IVFFlat limitados a 2000d). Para < 10K vetores de 4096d, sequential scan < 100ms.
## Coluna `metadata` (JSONB)
O campo `metadata` armazena informações estruturadas que não fazem parte do texto embeddado mas são úteis para filtragem e exibição:
```json
{
"function_name": "sub_401000",
"address": "0x401000",
"callers": ["WinMain", "sub_402300"],
"callees": ["CreateFileA", "WriteFile", "CloseHandle"],
"label_count": 2,
"assembly_length": 1247,
"has_decompiled_code": false
}
```
Para chunks do tipo `METADATA`:
```json
{
"filename": "malware.exe",
"format": "PE32",
"architecture": "x86",
"compiler": "MSVC 19.0",
"function_count": 342
}
```
Vantagens do JSONB sobre colunas separadas:
- Flexível: adicionar campos sem migration
- Indexável: `CREATE INDEX ... ON embedding_chunk USING gin (metadata jsonb_path_ops)` se necessário
- Permite queries como: `SELECT * FROM embedding_chunk WHERE metadata->>'function_name' = 'WinMain'`
## Políticas de Deleção
| Operação | Comportamento |
|---|---|
| Deletar binário | `ON DELETE CASCADE` → deleta todos os `embedding_chunk`, `chat_session` e `chat_message` |
| Deletar projeto | `ON DELETE CASCADE` via binário → cascata completa |
| Re-analisar binário | Hard delete de `embedding_chunk` via `deleteByBinaryId()` antes de reindexar |
| Deletar sessão | `orphanRemoval = true` → deleta todas as `chat_message` |
## Relação com o Schema Existente
A migration V008 é aditiva — não altera nenhuma tabela existente. As novas tabelas referenciam:
- `binaries(id)` — FK com CASCADE
- `projects(id)` — FK com CASCADE
O `source_id` em `embedding_chunk` referencia logicamente `static_function.id`, mas **sem constraint de FK** para:
- Permitir valores nulos (chunk METADATA não tem source)
- Permitir referências futuras a outras tabelas (ex: tabela de strings, tabela de imports)

671
docs/ai/frontend-guide.md Normal file
View File

@ -0,0 +1,671 @@
# Guia de Implementação Frontend — Chat IA (RAG)
## Visão Geral
Este documento descreve como integrar o frontend com a API de chat IA do Decompile-AI. O fluxo consiste em:
1. Após upload + análise estática de um binário, embeddings são gerados automaticamente
2. O usuário cria uma sessão de chat vinculada ao binário
3. Mensagens são enviadas e a resposta da LLM é recebida via **Server-Sent Events (SSE)**
4. O backend faz RAG automaticamente: busca contexto relevante no binário e injeta no prompt
## Stack Recomendada (Frontend)
| Tecnologia | Propósito |
|---|---|
| **EventSource** ou **fetch + ReadableStream** | Consumir SSE stream |
| **React** (ou framework similar) | UI components |
| **AbortController** | Cancelar stream em andamento |
| **@microsoft/fetch-event-source** | Lib opcional para SSE com POST + cancelamento |
---
## 1. Endpoints da API
### 1.1 Criar Sessão de Chat
```
POST /binaries/{binaryId}/chat/sessions?projectId={projectId}&model={model}
```
**Query Params**:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `projectId` | UUID | SIM | ID do projeto pai |
| `model` | String | Não | Modelo LLM (`gemma4:12b` padrão) |
**Response** (`201 Created`):
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"binaryId": "660e8400-e29b-41d4-a716-446655440001",
"projectId": "770e8400-e29b-41d4-a716-446655440002",
"title": "malware.exe — Chat",
"chatModel": "gemma4:12b",
"messageCount": 0,
"createdAt": "2026-06-08T15:30:00Z",
"updatedAt": "2026-06-08T15:30:00Z"
}
```
**Erros**:
| Código | Significado |
|---|---|
| `404` | Binário/projeto não encontrado |
| `400` | Modelo indisponível (Ollama não está rodando) |
| `400` | `projectId` ausente |
### 1.2 Listar Sessões do Binário
```
GET /binaries/{binaryId}/chat/sessions
```
**Response** (`200 OK`):
```json
[
{
"id": "550e8400-...",
"binaryId": "660e8400-...",
"projectId": "770e8400-...",
"title": "malware.exe — Chat",
"chatModel": "gemma4:12b",
"messageCount": 5,
"createdAt": "2026-06-08T15:30:00Z",
"updatedAt": "2026-06-08T16:00:00Z"
}
]
```
Ordenado por `updatedAt` decrescente (sessão mais recente primeiro).
### 1.3 Ver Detalhes da Sessão
```
GET /chat/sessions/{sessionId}
```
Mesmo formato de resposta que o item 1.1.
### 1.4 Deletar Sessão
```
DELETE /chat/sessions/{sessionId}
```
**Response**: `204 No Content`.
> Deletar a sessão remove **todas** as mensagens associadas (cascade).
### 1.5 Histórico de Mensagens
```
GET /chat/sessions/{sessionId}/messages
```
**Response** (`200 OK`):
```json
[
{
"id": "aa0e8400-...",
"sessionId": "550e8400-...",
"role": "USER",
"content": "Quais funções lidam com rede?",
"tokenCount": 12,
"createdAt": "2026-06-08T15:30:30Z"
},
{
"id": "bb0e8400-...",
"sessionId": "550e8400-...",
"role": "ASSISTANT",
"content": "O binário importa funções de rede da WS2_32.dll...",
"tokenCount": 85,
"createdAt": "2026-06-08T15:30:35Z"
}
]
```
Ordenado por `createdAt` crescente (mais antigo primeiro).
### 1.6 Enviar Mensagem (SSE Streaming)
```
POST /chat/sessions/{sessionId}/messages
Content-Type: application/json
Accept: text/event-stream
```
**Request Body**:
```json
{
"content": "Quais funções nesse binário lidam com criptografia?"
}
```
| Campo | Tipo | Validação |
|---|---|---|
| `content` | String | 14000 caracteres, obrigatório |
**Response**: `200 OK` com `Content-Type: text/event-stream`
---
## 2. Protocolo SSE (Server-Sent Events)
O stream é uma sequência de eventos `data:` enviados ao longo da conexão HTTP.
### 2.1 Tipos de Evento
| `type` | Quando | Campos |
|---|---|---|
| `chunk` | Token de texto gerado pela LLM | `content: String` |
| `done` | Stream concluído com sucesso | `sessionId: UUID` |
| `error` | Erro durante o processamento | `message: String` |
### 2.2 Exemplo de Stream
```
data: {"type":"chunk","content":"O binário"}
data: {"type":"chunk","content":" utiliza "}
data: {"type":"chunk","content":"as seguintes"}
data: {"type":"chunk","content":" funções"}
data: {"type":"chunk","content":" para criptografia:"}
data: {"type":"chunk","content":"\n\n1. **sub_405000**"}
...
data: {"type":"done","sessionId":"550e8400-e29b-..."}
```
### 2.3 Evento de Erro
```
data: {"type":"error","message":"Ollama is not available. Install ollama and pull gemma4:12b."}
```
Após um evento `error`, a conexão é fechada. Nenhum evento `done` é enviado.
### 2.4 Notas sobre SSE
- A conexão é mantida aberta até o stream terminar (`done` ou `error`)
- Tokens podem chegar com latência variável (a LLM gera em tempo real)
- Cada evento `data:` contém uma linha JSON válida
- O frontend deve acumular os `chunk.content` para montar a resposta completa
- O backend **já salva** a resposta completa no banco ao final — o frontend não precisa reenviar
---
## 3. Fluxo de Implementação
### 3.1 Diagrama de Sequência
```
Frontend Backend Ollama
│ │ │
│ POST /chat/sessions (criar) │ │
├────────────────────────────────►│ │
│◄────────────────────────────────┤ sessionId │
│ │ │
│ POST /chat/sessions/{id}/messages (SSE) │
├────────────────────────────────►│ │
│ │ embed query │
│ ├──────────────────────────────►│
│ │◄──────────────────────────────┤
│ │ │
│ │ pgvector search (RAG) │
│ │ build system prompt │
│ │ │
│ │ stream LLM │
│ ├──────────────────────────────►│
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
│◄──── data: {"type":"done"...}───┤ │
│ │ │
│ │ save assistant message │
│ │ │
│ GET /chat/sessions/{id}/messages (refresh) │
├────────────────────────────────►│ │
│◄────────────────────────────────┤ full history │
```
### 3.2 Estrutura de Componentes (React)
```
BinaryDetailPage
├── ChatPanel ← componente principal do chat
│ ├── ChatSessionList ← barra lateral: lista de sessões
│ │ ├── ChatSessionItem ← cada sessão (título, modelo, msg count)
│ │ └── NewSessionButton ← botão "Nova conversa"
│ │
│ └── ChatWindow ← área principal de conversa
│ ├── ChatMessageList ← scroll area com mensagens
│ │ ├── UserMessage ← bolha do usuário
│ │ └── AssistantMessage ← bolha do assistente (com markdown)
│ │
│ ├── StreamingMessage ← mensagem sendo gerada (cursor piscando)
│ └── ChatInput ← textarea + botão enviar
```
### 3.3 Exemplo de Hook: `useChatSession`
```typescript
interface ChatSession {
id: string;
binaryId: string;
projectId: string;
title: string;
chatModel: string;
messageCount: number;
createdAt: string;
updatedAt: string;
}
interface ChatMessage {
id: string;
sessionId: string;
role: 'USER' | 'ASSISTANT';
content: string;
tokenCount: number;
createdAt: string;
}
function useChatSession(binaryId: string, projectId: string) {
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [streamingContent, setStreamingContent] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const abortRef = useRef<AbortController | null>(null);
// Listar sessões
const loadSessions = async () => {
const res = await fetch(`/binaries/${binaryId}/chat/sessions`);
const data = await res.json();
setSessions(data);
};
// Criar nova sessão
const createSession = async (model: string = 'gemma4:12b') => {
const res = await fetch(
`/binaries/${binaryId}/chat/sessions?projectId=${projectId}&model=${model}`,
{ method: 'POST' }
);
const session = await res.json();
setSessions(prev => [session, ...prev]);
setActiveSessionId(session.id);
setMessages([]);
return session;
};
// Carregar histórico
const loadMessages = async (sessionId: string) => {
const res = await fetch(`/chat/sessions/${sessionId}/messages`);
const data = await res.json();
setMessages(data);
};
// Enviar mensagem (SSE streaming)
const sendMessage = async (content: string) => {
if (!activeSessionId) return;
// Adiciona mensagem do usuário ao estado local (otimista)
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
sessionId: activeSessionId,
role: 'USER',
content,
tokenCount: 0,
createdAt: new Date().toISOString(),
};
setMessages(prev => [...prev, userMsg]);
setStreamingContent('');
setIsStreaming(true);
const controller = new AbortController();
abortRef.current = controller;
try {
const response = await fetch(
`/chat/sessions/${activeSessionId}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify({ content }),
signal: controller.signal,
}
);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let fullContent = '';
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const event = JSON.parse(line.slice(6));
if (event.type === 'chunk') {
fullContent += event.content;
setStreamingContent(fullContent);
} else if (event.type === 'done') {
setIsStreaming(false);
// Recarrega histórico real para pegar IDs do backend
await loadMessages(activeSessionId);
} else if (event.type === 'error') {
setIsStreaming(false);
throw new Error(event.message);
}
}
}
} catch (err: any) {
if (err.name !== 'AbortError') {
setIsStreaming(false);
console.error('Chat error:', err);
}
}
};
// Cancelar stream
const cancelStream = () => {
abortRef.current?.abort();
setIsStreaming(false);
};
// Deletar sessão
const deleteSession = async (sessionId: string) => {
await fetch(`/chat/sessions/${sessionId}`, { method: 'DELETE' });
setSessions(prev => prev.filter(s => s.id !== sessionId));
if (activeSessionId === sessionId) {
setActiveSessionId(null);
setMessages([]);
}
};
// Selecionar sessão
const selectSession = async (sessionId: string) => {
setActiveSessionId(sessionId);
await loadMessages(sessionId);
};
return {
sessions,
activeSessionId,
messages,
streamingContent,
isStreaming,
loadSessions,
createSession,
selectSession,
sendMessage,
cancelStream,
deleteSession,
};
}
```
### 3.4 Componente de Chat (React)
```tsx
function ChatWindow({
messages,
streamingContent,
isStreaming,
onSend,
onCancel,
}: {
messages: ChatMessage[];
streamingContent: string;
isStreaming: boolean;
onSend: (content: string) => void;
onCancel: () => void;
}) {
const [input, setInput] = useState('');
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isStreaming) return;
onSend(input.trim());
setInput('');
};
return (
<div className="chat-window">
<div className="messages">
{messages.map(msg => (
<MessageBubble key={msg.id} role={msg.role} content={msg.content} />
))}
{isStreaming && (
<MessageBubble
role="ASSISTANT"
content={streamingContent}
isStreaming
/>
)}
<div ref={scrollRef} />
</div>
<form onSubmit={handleSubmit} className="chat-input">
<textarea
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Pergunte sobre funções, protocolos, estruturas..."
disabled={isStreaming}
rows={2}
/>
{isStreaming ? (
<button type="button" onClick={onCancel}>Parar</button>
) : (
<button type="submit" disabled={!input.trim()}>Enviar</button>
)}
</form>
</div>
);
}
```
---
## 4. Tratamento de Erros
| Cenário | Código | Ação Recomendada |
|---|---|---|
| **Sessão não encontrada** | 404 | Redirecionar para lista de binários |
| **Binário não analisado** | — | Embeddings não gerados → exibir mensagem "Execute a análise estática primeiro" |
| **Ollama offline** | `error` SSE | Exibir "Ollama não está rodando. Inicie com `ollama serve`" |
| **Modelo não baixado** | `error` SSE | Exibir "Modelo não encontrado. Execute `ollama pull gemma4:12b`" |
| **Timeout do stream** | Conexão fechada | Mostrar mensagem parcial + botão "Tentar novamente" |
| **Conteúdo vazio** | 400 | Validar no frontend: mínimo 1 caractere |
| **Conteúdo > 4000 chars** | 400 | Limitar textarea a 4000 caracteres |
### Exemplo de Error Boundary
```typescript
function getErrorMessage(error: string): string {
if (error.includes('Ollama is not available')) {
return 'Servidor LLM local (Ollama) não está rodando. Execute `ollama serve` no terminal.';
}
if (error.includes('Install ollama and pull')) {
return `Modelo LLM não encontrado. Execute no terminal:\nollama pull gemma4:12b`;
}
return `Erro ao processar a resposta: ${error}`;
}
```
---
## 5. Considerações de UX
### 5.1 Estados da UI
| Estado | O que mostrar |
|---|---|
| **Binário sem análise** | "Execute a análise estática antes de usar o chat." |
| **Sem embeddings** | "Aguarde a geração dos embeddings..." (polling do job `GENERATE_EMBEDDINGS`) |
| **Nenhuma sessão** | Botão "Iniciar conversa" proeminente |
| **Streaming** | Bolha do assistente com cursor piscando (`▊`), botão "Parar" |
| **Stream concluído** | Mensagem completa renderizada com markdown |
| **Erro** | Banner de erro com ação sugerida |
### 5.2 Renderização de Markdown
A resposta da LLM frequentemente contém markdown (negrito, listas, blocos de código). Use uma biblioteca como `react-markdown` com syntax highlighting para assembly:
```tsx
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
function MessageBubble({ content, role, isStreaming }: Props) {
return (
<div className={`bubble ${role.toLowerCase()}`}>
<ReactMarkdown
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter language={match[1]} PreTag="div">
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>{children}</code>
);
},
}}
>
{content}
</ReactMarkdown>
{isStreaming && <span className="cursor"></span>}
</div>
);
}
```
### 5.3 Gerenciamento de Scroll
- Scroll automático para o final ao receber novos tokens
- Se o usuário scrollar para cima manualmente, **não** força scroll (respeita leitura)
- Retoma auto-scroll quando usuário scrolla de volta ao final
```typescript
function useAutoScroll(deps: any[]) {
const containerRef = useRef<HTMLDivElement>(null);
const [userScrolledUp, setUserScrolledUp] = useState(false);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const handleScroll = () => {
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
setUserScrolledUp(!isAtBottom);
};
el.addEventListener('scroll', handleScroll);
return () => el.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (!userScrolledUp) {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' });
}
}, deps);
return containerRef;
}
```
### 5.4 Polling de Status dos Embeddings
Antes de habilitar o chat, verifique se os embeddings foram gerados:
```typescript
async function hasEmbeddings(binaryId: string): Promise<boolean> {
// Verifica se existem chunks para este binário
// Opção 1: endpoint dedicado (a ser adicionado)
// Opção 2: verificar jobs — se existe GENERATE_EMBEDDINGS COMPLETED
const res = await fetch(`/jobs?binaryId=${binaryId}&type=GENERATE_EMBEDDINGS&status=COMPLETED`);
const jobs = await res.json();
return jobs.length > 0;
}
```
---
## 6. Resumo de Integração
```
Preparação:
1. Upload binário → POST /projects/{id}/binaries?engine=IDA5
2. Aguardar análise → polling GET /jobs?binaryId={id}&type=STATIC_ANALYSIS
3. Aguardar embeddings → polling GET /jobs?binaryId={id}&type=GENERATE_EMBEDDINGS
Chat:
4. Criar sessão → POST /binaries/{id}/chat/sessions?projectId={pid}
5. Enviar mensagem → POST /chat/sessions/{sid}/messages (SSE stream)
6. Renderizar tokens → acumular data.type=chunk
7. Finalizar → data.type=done → recarregar histórico
8. Histórico → GET /chat/sessions/{sid}/messages (ao voltar à sessão)
Gerenciamento:
9. Listar sessões → GET /binaries/{id}/chat/sessions
10. Deletar sessão → DELETE /chat/sessions/{sid}
```
---
## 7. Endpoints de Suporte
Estes endpoints **já existem** e são úteis para polling de status:
### Jobs
```
GET /jobs?projectId={projectId}&type=GENERATE_EMBEDDINGS&status=COMPLETED
```
Retorna jobs do tipo `GENERATE_EMBEDDINGS` concluídos. Use para verificar se os embeddings estão prontos.
```
GET /jobs?binaryId={binaryId}&type=GENERATE_EMBEDDINGS
```
Status possíveis: `ENQUEUED`, `STARTED`, `IN_PROGRESS`, `COMPLETED`, `FAILED`.
### Funções (para referência no chat)
```
GET /analysis/{analysisId}/functions?page=0&size=50
GET /functions/{functionId}
GET /functions/{functionId}/callers
GET /functions/{functionId}/callees
```
O frontend pode usar esses endpoints para permitir que o usuário **clique em uma função mencionada na resposta** e veja seus detalhes (assembly, xrefs, labels).

View File

@ -0,0 +1,424 @@
# Pontos de Integração — Módulo AI com Sistema Existente
## Visão Geral
O módulo `ai/` não existe isoladamente — ele se integra profundamente com os módulos existentes (`workspace`, `analysis`, `job`) através de eventos de domínio, dependências Modulith e jobs assíncronos.
Este documento descreve cada ponto de integração, o fluxo de dados e as modificações necessárias nos módulos existentes.
---
## 1. Diagrama de Sequência Completo
```
Usuário Controller AnalysisSvc JobModule AI Module Ollama/LLM
│ │ │ │ │ │
│ POST /upload │ │ │ │ │
├────────────────►│ │ │ │ │
│ │ save │ │ │ │
│ ├─────────────►│ │ │ │
│ │ │ BinaryUp- │ │ │
│ │ │ loadedEvent │ │ │
│ │ ├─────────────►│ │ │
│ │ │ │ ANALYZE_FILE │ │
│ │ │ │ (DiE Docker) │ │
│ │ │ │ │ │
│ POST /analyze │ │ │ │ │
├────────────────►│ │ │ │ │
│ │ │ StaticAnlys │ │ │
│ │ │ RequestedEvt │ │ │
│ │ ├─────────────►│ │ │
│ │ │ │ STATIC_ANALYS │ │
│ │ │ │ (IDA5 Docker) │ │
│ │ │◄─────────────┤ saveResult() │ │
│ │ │ │ │ │
│ │ │ StaticAnlys │ │ │
│ │ │ CompletedEvt │ │ │
│ │ ├─────────────►│ │ │
│ │ │ │ GENERATE_EMB │ │
│ │ │ ├──────────────►│ │
│ │ │ │ │ indexBinary() │
│ │ │ │ ├──────────────►│
│ │ │ │ │◄──────────────┤
│ │ │ │◄──────────────┤ embeddings │
│ │ │ │ │ │
│ │ │ │ (binário indexado) │
│ │ │ │ │ │
│ POST /chat/... │ │ │ │ │
├────────────────►│ │ │ │ │
│ │ │ │ │ chat() │
│ │ │ │ ├──────────────►│
│ │◄─────────────┼──────────────┼───────────────┤ SSE stream │
│◄────────────────┤ │ │ │ │
```
## 2. Eventos
### 2.1 Novo Evento: `StaticAnalysisCompletedEvent`
**Definição**: `analysis/event/StaticAnalysisCompletedEvent.java`
```java
package ai.decompile.analysis.event;
import java.util.UUID;
/**
* Published when a static analysis job completes successfully.
* Triggers the embedding generation pipeline.
*/
public record StaticAnalysisCompletedEvent(
UUID binaryId,
UUID projectId,
UUID workspaceId,
UUID analysisId,
String engine
) {}
```
**Quem publica**: `StaticAnalysisHandler.process()` no módulo `job`
**Quem consome**: `EmbeddingEventSubscriber` no módulo `job`
**Momento da publicação**: Após `analysisService.saveResult()` e `binaryService.markStaticAnalysisDone()` concluírem com sucesso.
### 2.2 Publicação no `StaticAnalysisHandler`
**Arquivo a modificar**: `job/service/handler/StaticAnalysisHandler.java`
**Mudança**: Adicionar publicação do evento após salvar resultados:
```java
// Dentro de process(), após saveResult() e markStaticAnalysisDone():
@Autowired
private ApplicationEventPublisher eventPublisher;
// ... no final de process():
var binary = binaryService.getBinary(job.getBinaryId());
eventPublisher.publishEvent(
new StaticAnalysisCompletedEvent(
binary.getId(),
binary.getProject().getId(),
binary.getProject().getWorkspace().getId(),
analysisResponse.id(),
engineName
)
);
```
> **Alternativa com Spring Modulith**: Usar `@ApplicationModuleListener` exige que o evento seja publicado via `ApplicationEventPublisher` do Spring. O Spring Modulith intercepta e persiste na tabela `event_publication` para garantia de entrega.
### 2.3 Novo Subscriber: `EmbeddingEventSubscriber`
**Arquivo a criar**: `job/service/EmbeddingEventSubscriber.java`
```java
package ai.decompile.job.service;
import ai.decompile.analysis.event.StaticAnalysisCompletedEvent;
import ai.decompile.job.messaging.JobMessagePublisher;
import ai.decompile.job.model.entity.Job;
import ai.decompile.job.model.enums.JobStatus;
import ai.decompile.job.model.enums.JobType;
import ai.decompile.job.model.repository.JobRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Log4j2
@Component
@RequiredArgsConstructor
public class EmbeddingEventSubscriber {
private final JobRepository jobRepository;
private final JobMessagePublisher jobMessagePublisher;
@ApplicationModuleListener
public void on(StaticAnalysisCompletedEvent event) {
log.info(">>> EmbeddingEventSubscriber: Received StaticAnalysisCompletedEvent binary={}", event.binaryId());
var job = createJob(event);
log.info(">>> EmbeddingEventSubscriber: Created GENERATE_EMBEDDINGS job {} for binary {}", job.getId(), event.binaryId());
jobMessagePublisher.publish(job.getId());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
Job createJob(StaticAnalysisCompletedEvent event) {
return jobRepository.save(
Job.builder()
.type(JobType.GENERATE_EMBEDDINGS)
.status(JobStatus.ENQUEUED)
.projectId(event.projectId())
.binaryId(event.binaryId())
.payload("{\"analysisId\":\"" + event.analysisId() + "\"}")
.retryCount(0)
.build()
);
}
}
```
## 3. Jobs
### 3.1 Novo `JobType`
**Arquivo a modificar**: `job/model/enums/JobType.java`
```java
public enum JobType {
ANALYZE_FILE,
STATIC_ANALYSIS,
GENERATE_EMBEDDINGS // <-- novo
}
```
### 3.2 Novo Handler: `EmbeddingGenerationHandler`
**Arquivo a criar**: `job/service/handler/EmbeddingGenerationHandler.java`
```java
package ai.decompile.job.service.handler;
import ai.decompile.ai.service.EmbeddingService;
import ai.decompile.job.model.entity.Job;
import ai.decompile.job.model.enums.JobStatus;
import ai.decompile.job.model.enums.JobType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
@Log4j2
@Component
public class EmbeddingGenerationHandler implements JobHandler {
private final EmbeddingService embeddingService;
private final ObjectMapper objectMapper;
public EmbeddingGenerationHandler(EmbeddingService embeddingService, ObjectMapper objectMapper) {
this.embeddingService = embeddingService;
this.objectMapper = objectMapper;
}
@Override
public void process(Job job) {
log.info("Processing GENERATE_EMBEDDINGS job {} for binary {}", job.getId(), job.getBinaryId());
try {
int chunks = embeddingService.indexBinary(job.getBinaryId());
job.setResult("{\"chunkCount\":" + chunks + "}");
job.setStatus(JobStatus.COMPLETED);
log.info("Job {} completed. Generated {} embedding chunks for binary {}",
job.getId(), chunks, job.getBinaryId());
} catch (Exception e) {
log.error("Job {} failed: {}", job.getId(), e.getMessage(), e);
job.setStatus(JobStatus.FAILED);
job.setErrorMessage(e.getMessage());
}
}
@Override
public JobType supportedType() {
return JobType.GENERATE_EMBEDDINGS;
}
}
```
> **Nota**: `EmbeddingGenerationHandler` não precisa de `@ConditionalOnProperty` como `StaticAnalysisHandler`, pois embeddings não dependem do Docker. Ollama roda no host.
### 3.3 Atualizar `JobHandlerFactory`
Nenhuma mudança necessária — o factory já descobre automaticamente todos os beans `JobHandler` via injeção de lista.
## 4. Dependências Modulith
### 4.1 Atualizar `job/package-info.java`
**Arquivo a modificar**: `job/package-info.java`
```java
@org.springframework.modulith.ApplicationModule(
displayName = "Job",
allowedDependencies = {
"workspace::events",
"workspace::entities",
"workspace::services",
"die::service",
"die::model",
"analysis::events",
"analysis::services",
"analysis::dto",
"engine::services",
"ai::services", // <-- novo: EmbeddingService
"ai::model::entity", // <-- novo: EmbeddingChunk (se necessário)
"common"
})
package ai.decompile.job;
```
### 4.2 `analysis/package-info.java`
Não precisa ser alterado. O evento `StaticAnalysisCompletedEvent` é definido no próprio módulo `analysis` (`analysis.event`), então não há nova dependência.
### 4.3 `ai/package-info.java` (novo)
```java
@org.springframework.modulith.ApplicationModule(
displayName = "AI",
allowedDependencies = {
"workspace::entities", // Binary, Project (para JPA relationships)
"workspace::services", // BinaryService (para buscar filePath, metadados)
"analysis::entities", // StaticFunction, StaticAnalysis, StaticXref, StaticLabel
"analysis::services", // AnalysisService (para buscar funções/xrefs)
"common" // exceções, config
})
package ai.decompile.ai;
```
## 5. Modificações em Arquivos Existentes
### 5.1 `pom.xml`
Adicionar dependências Spring AI (Spring AI BOM, ollama, openai, pgvector-store).
### 5.2 `application.yml`
Adicionar blocos `spring.ai.*` e `app.ai.*` (ver `configuration.md`).
### 5.3 `job/model/enums/JobType.java`
Adicionar `GENERATE_EMBEDDINGS`.
### 5.4 `job/package-info.java`
Adicionar `ai::services` e `ai::model::entity` aos `allowedDependencies`.
### 5.5 `job/service/handler/StaticAnalysisHandler.java`
Adicionar publicação de `StaticAnalysisCompletedEvent` ao final de `process()`.
### 5.6 `.gitignore`
Adicionar `.env`.
## 6. Arquivos Novos (por módulo)
### Módulo `ai/`
```
src/main/java/ai/decompile/ai/
├── package-info.java
├── config/
│ └── AiConfig.java
├── model/
│ ├── entity/
│ │ ├── package-info.java
│ │ ├── EmbeddingChunk.java
│ │ ├── ChatSession.java
│ │ └── ChatMessage.java
│ ├── repository/
│ │ ├── package-info.java
│ │ ├── EmbeddingChunkRepository.java
│ │ ├── ChatSessionRepository.java
│ │ └── ChatMessageRepository.java
│ └── dto/
│ ├── package-info.java
│ ├── ChatRequest.java
│ ├── SessionResponse.java
│ └── MessageResponse.java
├── service/
│ ├── package-info.java
│ ├── EmbeddingService.java
│ ├── RagService.java
│ └── ChatService.java
└── controller/
├── package-info.java
└── ChatController.java
```
### Módulo `analysis/`
```
src/main/java/ai/decompile/analysis/
└── event/
└── StaticAnalysisCompletedEvent.java # novo
```
### Módulo `job/`
```
src/main/java/ai/decompile/job/
├── service/
│ ├── EmbeddingEventSubscriber.java # novo
│ └── handler/
│ └── EmbeddingGenerationHandler.java # novo
└── model/enums/
└── JobType.java # modificado (+ GENERATE_EMBEDDINGS)
```
### Database
```
src/main/resources/db/migration/
└── V008__ai_embedding_and_chat.sql # novo
```
### Raiz do projeto
```
backend/
├── .env # novo
├── .env.example # novo (template sem secrets)
└── .gitignore # modificado (+ .env)
```
## 7. Ordem de Implementação Recomendada
| Ordem | Tarefa | Depende de |
|---|---|---|
| 1 | `V008__ai_embedding_and_chat.sql` | Nada |
| 2 | Dependências no `pom.xml` | Nada |
| 3 | `application.yml` — blocos AI | Nada |
| 4 | `.env` + `.env.example` + `.gitignore` | Nada |
| 5 | Entidades JPA (`EmbeddingChunk`, `ChatSession`, `ChatMessage`) | V008, pom.xml |
| 6 | Repositórios JPA | Entidades |
| 7 | `AiConfig.java` | pom.xml, application.yml |
| 8 | `EmbeddingService` | Repositórios, AiConfig |
| 9 | `RagService` | EmbeddingService, AnalysisService |
| 10 | `ChatService` | RagService, AiConfig |
| 11 | `ChatController` | ChatService |
| 12 | `StaticAnalysisCompletedEvent` | Nada |
| 13 | `EmbeddingEventSubscriber` | Evento, JobService |
| 14 | `EmbeddingGenerationHandler` | EmbeddingService |
| 15 | `JobType.GENERATE_EMBEDDINGS` | Nada |
| 16 | `job/package-info.java` update | Dependências ai |
| 17 | `StaticAnalysisHandler` — publish event | Evento |
| 18 | `ai/package-info.java` | Dependências workspace, analysis |
| 19 | Testes de integração | Tudo acima |
## 8. Verificação de Integridade
Após implementar todos os pontos de integração, verificar:
```bash
# 1. Compilação limpa
./mvnw clean compile
# 2. Migration roda sem erros
./mvnw flyway:migrate
# 3. Testes de módulo Modulith
./mvnw test -Dtest="*Modulith*"
# 4. Fluxo completo (manual):
# - Upload de binário
# - Análise estática
# - Verificar job GENERATE_EMBEDDINGS criado e concluído
# - Verificar registros em embedding_chunk
# - Criar sessão de chat
# - Enviar mensagem e receber SSE stream
```

404
docs/ai/rag-pipeline.md Normal file
View File

@ -0,0 +1,404 @@
# Pipeline RAG (Retrieval-Augmented Generation)
## Visão Geral
O pipeline RAG do Decompile-AI permite que o usuário faça perguntas em linguagem natural sobre um binário analisado. O sistema recupera automaticamente as funções e informações mais relevantes do binário e as injeta no prompt da LLM, que então gera uma resposta contextualizada.
O pipeline opera em duas fases distintas:
1. **Fase de Indexação** — Processa os resultados da análise estática, gera embeddings e os armazena no pgvector
2. **Fase de Query** — A cada pergunta do usuário, recupera contexto relevante e gera a resposta via LLM
---
## Fase 1: Indexação
### 1.1 Gatilho
A indexação é disparada automaticamente quando uma análise estática é concluída com sucesso:
```
StaticAnalysisHandler.process() conclui
└── publica StaticAnalysisCompletedEvent(binaryId, projectId, workspaceId, analysisId, engine)
└── EmbeddingEventSubscriber.on(StaticAnalysisCompletedEvent)
└── cria Job(type=GENERATE_EMBEDDINGS, status=ENQUEUED)
└── RabbitMQ → EmbeddingGenerationHandler.process(job)
```
Também é possível reindexar manualmente (endpoint futuro).
### 1.2 Coleta de Dados
O `EmbeddingGenerationHandler` chama `EmbeddingService.indexBinary(binaryId)`. O serviço:
1. **Deleta embeddings existentes** para o binário (`deleteByBinaryId`) — hard delete, sem soft delete
2. **Busca a análise mais recente** via `AnalysisService.getAnalysis(analysisId)` ou `listAnalyses(binaryId)`
3. **Itera sobre todas as `StaticFunction`** da análise, carregando:
- `name`, `address`, `assembly` (sempre presentes)
- `decompiledCode`, `signature` (quando disponíveis — Ghidra/IDA 7)
- `StaticLabel` associadas à função
- `StaticXref` (callers e callees) — nomes das funções via join
4. **Busca metadados do binário**: formato, arquitetura, compilador (tabela `binaries`)
### 1.3 Chunking
Cada função gera um chunk de texto que será embeddado. O template do chunk:
```
[FUNCTION] {name} at {address}
Binary: {filename} | {architecture} | {compiler} | {format}
Callers: {caller1}, {caller2}, ...
Callees: {callee1}, {callee2}, ...
Labels:
{label1_address} ({label1_name})
{label2_address} ({label2_name})
[ASSEMBLY]
{assembly_code}
```
**Regras de chunking**:
| Regra | Descrição |
|---|---|
| **1 chunk = 1 função** | Unidade semântica natural da análise de binários |
| **Truncagem** | Se o texto montado exceder `chunk-max-chars` (default 3000), o assembly é truncado |
| **Labels inline** | Labels são incluídos como comentários contextuais para enriquecer o embedding |
| **Xrefs como metadados** | Nomes de callers/callees são incluídos como "dicas semânticas" no texto |
| **Chunk de metadados** | Um chunk extra (tipo `METADATA`) com informações gerais do binário é sempre gerado |
**Exemplo de chunk gerado** (para `malware.exe` x86 com MSVC):
```
[FUNCTION] sub_401000 at 0x401000
Binary: malware.exe | x86 | MSVC 19.0 | PE32
Callers: WinMain, sub_402300
Callees: CreateFileA, WriteFile, CloseHandle, sub_401200
Labels:
0x40100A (check_password)
0x401050 (error_msg)
[ASSEMBLY]
push ebp
mov ebp, esp
sub esp, 0x40
push ecx
push edx
mov eax, [ebp+8]
push eax
call ds:CreateFileA
mov [ebp-4], eax
cmp eax, -1
jz short loc_401050
...
```
**Chunk de metadados** (tipo `METADATA`):
```
[METADATA] Binary Overview
Filename: malware.exe
Format: PE32 (Portable Executable 32-bit)
Architecture: x86 (Intel 32-bit)
Compiler: Microsoft Visual C++ 19.0
Entry Point: 0x401200
Function count: 342
Analysis engine: IDA5
```
Esse chunk de metadados responde perguntas como "qual arquitetura?", "com o que foi compilado?", "quantas funções tem?".
### 1.4 Geração de Embeddings
Para cada chunk de texto:
1. O texto é enviado ao **Ollama** via Spring AI `EmbeddingModel.embed(document)`
2. O modelo configurado é `qwen3-embedding:latest` (8B parâmetros, dimensão configurável)
3. A dimensão padrão dos vetores é **1024** (configurável em `app.ai.embedding.dimension`)
4. Os embeddings são gerados em **batch** de até `batch-size` (default 20) chunks por chamada
5. Cada embedding é um `float[1024]` (ou dimensão configurada)
6. O vetor é persistido na tabela `embedding_chunk` junto com o texto original e metadados
**Fluxo detalhado do batch**:
```
funções: [f1, f2, f3, ..., f342]
│ particiona em batches de 20
batch1: [f1..f20] → Ollama embed() → 20 × float[1024] → INSERT batch
batch2: [f21..f40] → Ollama embed() → 20 × float[1024] → INSERT batch
...
batch18: [f341..f342] → Ollama embed() → 2 × float[1024] → INSERT batch
chunk METADATA → Ollama embed() → 1 × float[1024] → INSERT
```
**Tratamento de erros**:
- Se a API Ollama falhar para um batch, o job é marcado como `FAILED`
- O `retry_count` do job é incrementado (política de retry via RabbitMQ dead-letter queue no futuro)
- Nenhum embedding parcial é salvo — a transação faz rollback
### 1.5 Re-indexação
Quando um binário é re-analisado (ex: trocar de engine IDA5 → Ghidra):
1. O `EmbeddingEventSubscriber` cria novo job `GENERATE_EMBEDDINGS`
2. `EmbeddingService.indexBinary()` chama `deleteByBinaryId()` primeiro → **hard delete** de todos os chunks antigos
3. Novos chunks são gerados a partir da análise mais recente
Isso garante que não há embeddings órfãos ou inconsistentes.
---
## Fase 2: Query (Chat)
### 2.1 Visão Geral do Fluxo
```
Usuário: "Quais funções nesse binário lidam com rede?"
ChatController → ChatService.chat(sessionId, message)
├── 1. Busca sessão + histórico recente (últimas N mensagens)
├── 2. ChatService.buildContext(binaryId, query, binary)
│ │
│ ├── 2a. SemanticSearch: embed query → pgvector <=>
│ │ └── top-K chunks por similaridade de cosseno
│ │
│ ├── 2b. TextualFallback (se semantic < topK * 0.4):
│ │ └── Extrai tokens da query → LIKE search no content
│ │
│ └── 2c. CallGraphExpansion:
│ ├── Detecta nomes de função/endereço na query
│ ├── Busca funções via static_function (ILIKE)
│ └── Expande 1-hop (callers + callees) + assembly inline
├── 3. Merge + dedup: requested functions first, then semantic/textual chunks
├── 4. Build system prompt com contexto RAG + histórico
├── 5. LLM (Gemma4:12b) → streaming
│ │
│ └── Flux<String> via StreamingChatModel.stream()
├── 6. Salva UserMessage + AssistantMessage no banco
└── 7. Retorna SSE stream para o frontend
```
### 2.2 Busca Semântica (pgvector)
A busca vetorial é a espinha dorsal do retrieval. Utiliza o operador de distância de cosseno do pgvector:
```sql
SELECT id, content, metadata, token_count,
1 - (embedding <=> CAST(:queryEmbedding AS vector)) AS similarity
FROM embedding_chunk
WHERE binary_id = :binaryId
AND 1 - (embedding <=> CAST(:queryEmbedding AS vector)) > :threshold
ORDER BY embedding <=> CAST(:queryEmbedding AS vector)
LIMIT :limit
```
- **`<=>`** é o operador de cosine distance do pgvector (0 = idêntico, 2 = oposto)
- **`1 - (<=>)`** converte para similaridade (1 = idêntico, -1 = oposto)
- **`threshold`** (default 0.6) filtra resultados pouco relevantes
- **`limit`** (default 10) retorna os top-K chunks
- **`binary_id`** restringe a busca ao binário específico (escopo da sessão)
O índice HNSW (`idx_embedding_chunk_embedding`) acelera a busca para complexidade O(log N).
**Modelo de embedding da query**:
A pergunta do usuário é embeddada usando o **mesmo modelo** da indexação (`qwen3-embedding:latest`), garantindo que query e documentos estejam no mesmo espaço vetorial.
### 2.2.1 Fallback por Busca Textual
Quando a busca semântica retorna poucos resultados (menos de `fallback-min-ratio * topK`, default `0.4 * 10 = 4`), o sistema complementa com **busca textual direta** via `LIKE` no conteúdo dos chunks:
```sql
SELECT * FROM embedding_chunk
WHERE binary_id = :binaryId
AND LOWER(content) LIKE LOWER(CONCAT('%', :term, '%'))
LIMIT :limit
```
**Estratégia**:
1. A query é tokenizada (split por espaços e pontuação)
2. Tokens com ≥ 3 caracteres são usados como termos de busca
3. Para cada termo, busca textual preenche os slots restantes até `topK`
4. Resultados são deduplicados (mesmo chunk pode aparecer em múltiplas buscas)
**Exemplo**: Query *"Quais funções lidam com rede?"*
- Tokens: `Quais`, `funções`, `lidam`, `rede`
- Cada token vira `LIKE '%rede%'` no conteúdo dos chunks
- Funções que mencionam `WS2_32.send`, `socket`, `connect` são recuperadas mesmo que o embedding semântico não as tenha rankeado bem
### 2.3 Expansão por Call Graph (Recuperação Estruturada)
O sistema detecta automaticamente menções a funções ou endereços na query do usuário e recupera o contexto via call graph:
**Detecção**:
1. Tokenização da query + regex para padrões comuns: `sub_XXXX`, `0xXXXXXXXX`, nomes em PascalCase
2. Para cada token candidato, busca em `static_function` via:
```java
List<StaticFunction> findByNameIgnoreCaseContainingAndAnalysisId(name, analysisId);
```
3. Até 5 funções são identificadas (limite para não poluir o contexto)
**Expansão 1-hop**:
- **Callers**: quem chama a função detectada (`xrefRepository.findByCalleeId`)
- **Callees**: quem a função detectada chama (`xrefRepository.findByCallerId`)
- Para funções detectadas (que podem não ter embedding), o assembly é montado **inline no contexto**, truncado a 5000 caracteres
**Prioridade no contexto**:
1. Funções explicitamente mencionadas na query + seu assembly (sempre incluídas, mesmo sem embedding)
2. Callers/callees imediatos (1-hop, assembly truncado a 3000 caracteres)
3. Chunks semânticos (pgvector top-K)
4. Chunks textuais (LIKE fallback)
### 2.4 Construção do System Prompt
O `ChatService.buildSystemPrompt()` monta o prompt do sistema que instrui a LLM:
```
You are an expert reverse engineer and malware analyst.
You are analyzing a binary file with the following characteristics.
Use the provided function disassembly and context to answer the user's questions.
When referencing code, mention function names, addresses, and the binary context.
If the provided context does not contain enough information, acknowledge the limitation
and suggest what additional analysis would help.
[BINARY OVERVIEW]
Filename: malware.exe
Format: PE32 (Portable Executable 32-bit)
Architecture: x86
Compiler: Microsoft Visual C++ 19.0
[REQUESTED FUNCTION ANALYSIS] ← call graph expansion (se houver)
[FUNCTION] sub_401000 at 0x401000
Callers: WinMain, sub_402300
Callees: CreateFileA, WriteFile, CloseHandle
[ASSEMBLY]
push ebp
mov ebp, esp
...
[RELEVANT CODE SECTIONS] ← semantic + textual results
Section 1:
[FUNCTION] sub_401200 at 0x401200
Callers: sub_401000, sub_402500
Callees: WS2_32.send, WS2_32.recv
[ASSEMBLY]
...
[CONVERSATION GUIDELINES]
- Cite function addresses when relevant (e.g., "the function at 0x401000")
- Explain assembly in natural language when useful
- If you identify patterns (loops, conditionals, API calls), highlight them
- If you detect malicious intent, point it out objectively
- Be concise but thorough in your technical analysis
- If the context includes a [REQUESTED FUNCTION ANALYSIS] section with full assembly,
analyze that function in depth rather than saying you don't have access to it
```
- Explique o assembly em linguagem natural quando útil
- Se identificar padrões (ex: loop, condicional, chamada de API), destaque-os
- Se perceber intenção maliciosa, aponte objetivamente
```
### 2.6 Truncagem de Contexto
O contexto total (system prompt + chunks + histórico) deve caber na janela de contexto da LLM:
| Modelo | Janela de Contexto | Max Context Tokens Config |
|---|---|---|
| Gemma4:12b | 256K tokens | 8000 (padrão) |
| DeepSeek-chat | 64K tokens | 8000 (padrão) |
**Algoritmo de truncagem**:
1. **Reserva para system prompt**: ~500 tokens (fixo)
2. **Reserva para histórico**: últimas 20 mensagens, truncadas se necessário (~2000 tokens)
3. **Reserva para resposta**: ~1000 tokens (output)
4. **Disponível para chunks**: `max-context-tokens - 500 - 2000 - 1000 = ~4500 tokens`
5. **Preenche chunks em ordem de relevância** até atingir o limite
6. Chunks que não cabem são descartados (os menos relevantes primeiro)
### 2.7 Histórico de Conversa
O histórico da sessão (`chat_message`) é incluído no prompt para manter contexto multi-turn:
```
Messages enviados à LLM:
[
{ role: SYSTEM, content: "<system prompt com RAG context>" },
{ role: USER, content: "Quantas funções esse binário tem?" },
{ role: ASSISTANT, content: "342 funções." },
{ role: USER, content: "E dessas, quantas importam APIs de rede?" },
// ↑ A LLM usa o contexto do system prompt + histórico para responder
]
```
- Apenas as últimas `max-history-messages` (default 20) são incluídas
- Mensagens muito antigas são truncadas para caber no orçamento de tokens
### 2.8 Resposta Streaming (SSE)
Tokens são enviados ao frontend assim que gerados pela LLM:
```
data: {"type":"chunk","content":"O binário"}
data: {"type":"chunk","content":" importa "}
data: {"type":"chunk","content":"funções de"}
data: {"type":"chunk","content":" rede via"}
data: {"type":"chunk","content":" WS2_32"}
...
data: {"type":"done","messageId":"550e8400-e29b-...","sessionId":"660e8400-..."}
```
### 2.9 Tipos de Query e Estratégias
| Categoria | Exemplo de Query | Estratégia de Retrieval |
|---|---|---|
| **Análise de função** | "O que a função sub_401000 faz?" | Call graph expansion + assembly inline (1-hop) |
| **Busca por nome** | "Explique sub_f5" | Textual fallback (LIKE) + call graph se detectado |
| **Busca semântica** | "Quais funções lidam com criptografia?" | pgvector similarity search (top-10) |
| **Navegação do grafo** | "Quem chama WinMain?" | Call graph (callers diretos via xrefs) |
| **Metadados** | "Qual arquitetura/com pilador?" | Chunk METADATA (sempre incluído) |
| **Strings/Imports** | "Esse binário usa rede?" | pgvector (chunks que mencionam imports) |
| **Classificação** | "Isso parece malware?" | pgvector + METADATA (visão holística) |
| **Controle de fluxo** | "Explique o fluxo a partir de main" | Call graph (multi-hop callees) |
> **Nota**: O fallback textual e a expansão por call graph garantem que queries com nomes específicos de funções (ex: `sub_f5`, `0x401000`) sempre retornem o assembly relevante — mesmo que a busca semântica retorne 0 resultados.
---
## Modelo de Embedding: Qwen3 Embedding
| Propriedade | Valor |
|---|---|
| Modelo | `qwen3-embedding:latest` (8B) |
| Tamanho | 4.7 GB |
| Contexto máximo | 32K tokens |
| Dimensão configurável | 324096 (Spring AI 1.0.1 não expõe o parâmetro; usamos default do modelo: **4096**) |
| Ranking MTEB | #1 (score 70.58) |
| Idiomas | 100+ (incluindo código) |
| Execução | Ollama local (localhost:11434) |
**Escolha da dimensão 4096**: O modelo `qwen3-embedding:latest` (8B) retorna 4096 dimensões por padrão. O Spring AI 1.0.1 não expõe o parâmetro `dimensions` da API do Ollama (é um campo top-level, não dentro de `options`). Para 330 funções a 4096 floats (4 bytes cada), o storage total de vetores é ~5.4 MB. A coluna é `vector(4096)` e a busca usa sequential scan (sem índice — HNSW e IVFFlat do pgvector limitados a 2000d).

View File

@ -0,0 +1,292 @@
# Pontos de Extração — IDA 5, IDA 7 e Ghidra
Documento de referência sobre quais dados de análise estática são extraíveis via script de cada engine, com a utilidade de cada ponto e grau de dificuldade.
---
## Tabela Resumo
| # | Dado | Utilidade | IDA 5 (IDC) | IDA 7+ (IDAPython) | Ghidra (Java/Python) |
|---|------|-----------|:-----------:|:------------------:|:--------------------:|
| 1 | Funções (nome, endereço, assembly) | Navegação básica do binário | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 2 | Code references / Labels | Resolver alvos de jump/call | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 3 | Xrefs de entrada (callers) | Grafo de chamadas reverso | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 4 | Entry point | Saber onde o binário inicia | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 5 | Tipo de xref (CALL vs JUMP vs DATA) | Diferencia saltos de chamadas | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 6 | Flags da função (library, thunk, far) | Classificar tipo de função | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 7 | Segmentos (.text, .data, .rdata) | Mapa de memória do binário | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 8 | Tamanho/fim da função | Delimitar escopo da função | ✅ Fácil | ✅ Fácil | ✅ Fácil |
| 9 | Strings referenciadas | Entender mensagens, nomes, paths | ✅ Médio | ✅ Fácil | ✅ Fácil |
| 10 | Imports / APIs externas (DLLs) | Saber quais libs externas são usadas | ✅ Médio | ✅ Fácil | ✅ Fácil |
| 11 | Frame da função (vars locais, args) | Estrutura da stack frame | ✅ Médio | ✅ Fácil | ✅ Fácil |
| 12 | Comentários do IDA/Ghidra | Anotações automáticas do disassembler | ✅ Médio | ✅ Fácil | ✅ Fácil |
| 13 | Data xrefs (leitura/escrita em globais) | Quem acessa cada variável global | ⚠️ Limitado | ✅ Médio | ✅ Médio |
| 14 | Tipos e assinaturas de funções | Parâmetros, retorno, calling convention | ❌ Não viável | ✅ Médio | ✅ Fácil |
| 15 | Estruturas e enums | Layout de structs usadas pelo binário | ❌ Não viável | ⚠️ Limitado | ✅ Médio |
| 16 | Decompilação (pseudocódigo) | Código de alto nível reconstruído | ❌ Não existe | ✅ Fácil | ✅ Fácil |
| 17 | Call graph / Fluxo de controle (CFG) | Visualização de blocos e branches | ⚠️ Limitado | ✅ Médio | ✅ Médio |
| 18 | Nomes de variáveis globais | Labels simbólicos de dados | ✅ Médio | ✅ Fácil | ✅ Fácil |
---
## 1. Funções (nome, endereço, assembly)
**O que é:** Lista de todas as funções reconhecidas pelo disassembler, com nome, endereço de início e o texto do assembly.
**Para que serve:** É a base de toda navegação. Permite listar funções, buscar por nome, e exibir o código de cada uma.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `NextFunction()`, `GetFunctionName()`, `GetDisasm()`, `ItemSize()` | ✅ **Já implementado** |
| IDA 7 | `ida_funcs.get_funcs()`, `ida_lines.generate_disasm_line()` | ✅ Trivial |
| Ghidra | `currentProgram.getFunctionManager().getFunctions()`, `listing.getInstructions()` | ✅ Trivial |
---
## 2. Code References / Labels
**O que é:** Para cada instrução de jump/call, o endereço de destino e o nome (label) associado a ele.
**Para que serve:** Permite que o frontend resolva labels (`loc_28D`, `sub_402000`) para endereços reais e ofereça navegação por clique.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `Rfirst(ea)`, `Rnext(ea, ref)`, `Name(ref)` | ✅ **Já implementado** |
| IDA 7 | `ida_xref.xrefblk_t()` com `iscode=True` | ✅ Trivial |
| Ghidra | `instruction.getReferencesFrom()`, `ref.getToAddress()` | ✅ Trivial |
---
## 3. Xrefs de entrada (callers)
**O que é:** Lista de funções que chamam/referenciam a função atual.
**Para que serve:** Constrói o grafo reverso de chamadas — "quem me chama". Essencial para entender o fluxo do programa.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `RfirstB(ea)`, `RnextB(ea, xref)` | ✅ **Já implementado** |
| IDA 7 | `ida_xref.xrefs_to(ea)` | ✅ Trivial |
| Ghidra | `function.getCallingFunctions(TaskMonitor.DUMMY)` ou via reference manager | ✅ Trivial |
---
## 4. Entry Point
**O que é:** O endereço onde a execução do binário começa. Pode ser o stub da CRT (`start`) ou diretamente `main`/`WinMain`.
**Para que serve:** Ponto de partida da análise. O analista sabe por onde começar a ler o código. Em modo headless do frontend, pode ser usado como âncora inicial.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `BeginEA()` retorna o entry point; `Name()` dá o label | ✅ Trivial (testado, funciona) |
| IDA 7 | `ida_loader.get_entry_point()` ou `ida_entry.get_entry_ordinals()` | ✅ Trivial |
| Ghidra | `program.getListing().getInstructionAt(program.getImageBase().add(entryOffset))`; entry offset via `program.getExecutableFormat()` ou metadados do importador | ✅ Trivial |
---
## 5. Tipo de Xref (CALL vs JUMP vs DATA)
**O que é:** Classifica cada code reference como `CALL` (chamada de função), `JUMP` (salto condicional/incondicional) ou referência a dados.
**Para que serve:** Permite que o frontend diferencie visualmente chamadas de função de saltos locais. Essencial para coloração de syntax e navegação contextual.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `XrefType()` retorna `fl_CF` (far call), `fl_CN` (near call), `fl_JF` (far jump), `fl_JN` (near jump) | ✅ Fácil (1 enum switch) |
| IDA 7 | `ida_xref.xrefblk_t().type` retorna `fl_CF/fl_CN/fl_JF/fl_JN` | ✅ Fácil |
| Ghidra | `ref.getReferenceType()` retorna `CALL`, `JUMP`, `DATA`, etc. | ✅ Fácil |
---
## 6. Flags da Função
**O que é:** Atributos da função como: é library function? é thunk? usa far calling convention? tem frame pointer?
**Para que serve:** Classificação automática de funções. Ex: marcar funções importadas de DLL como "library", diferenciar funções far/near em binários 16-bit, etc.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `GetFunctionFlags(ea)` retorna bits: `FUNC_LIB`, `FUNC_THUNK`, `FUNC_FAR`, `FUNC_FRAME`, etc. | ✅ Fácil (bitmask) |
| IDA 7 | `ida_funcs.get_func(ea).flags` | ✅ Fácil |
| Ghidra | `function.isThunk()`, `function.isLibrary()`, `function.getCallingConvention()` | ✅ Fácil |
---
## 7. Segmentos (.text, .data, .rdata, etc.)
**O que é:** Mapa de memória do binário — cada segmento com nome, endereço inicial, endereço final e classe (CODE, DATA, BSS, etc.).
**Para que serve:** Contexto para o frontend mostrar em qual região de memória cada função/string está. Permite filtrar funções por segmento (ex: "só .text").
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `SegByName()`, `SegStart()`, `SegEnd()`, `SegName()`, iteração com `FirstSeg()`/`NextSeg()` | ✅ Fácil (loop de segmentos) |
| IDA 7 | `ida_segment.get_segm_list()`, `ida_segment.get_segm_by_name()` | ✅ Fácil |
| Ghidra | `program.getMemory().getBlocks()` | ✅ Fácil |
---
## 8. Tamanho / Fim da Função
**O que é:** Endereço final da função (`FindFuncEnd`), permitindo calcular tamanho em bytes e delimitar o escopo.
**Para que serve:** Já usado internamente no extract.idc. Expor no JSON permite ao frontend mostrar "tamanho: 245 bytes" ou destacar o range da função no mapa de memória.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `FindFuncEnd(ea)` | ✅ **Já usado internamente** |
| IDA 7 | `ida_funcs.get_func(ea).end_ea` | ✅ Trivial |
| Ghidra | `function.getBody().getMaxAddress()` | ✅ Trivial |
---
## 9. Strings
**O que é:** Todas as strings contidas no binário (mensagens, nomes de arquivo, paths, nomes de classes, etc.) com endereço, conteúdo e tipo (ASCII, Unicode).
**Para que serve:** Essencial para entender o que o programa faz. Strings geralmente revelam funcionalidades, mensagens de erro, comandos, paths de arquivo. O frontend pode oferecer busca e filtro por strings.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | Iterar sobre segmentos de dados com `isLoaded(ea)` e `GetString(ea, -1, ASCSTR_C)`. Também é possível buscar "string literals" no segmento `.data`/`.rdata` com `GetStringType()`. | ✅ Médio (iteração manual sobre bytes) |
| IDA 7 | `ida_bytes.get_strlit_contents()`, `ida_strlist.string_info_t()` — API nativa de strings | ✅ Fácil |
| Ghidra | `program.getListing().getDefinedData(true)` filtrado por `StringDataInstance` | ✅ Fácil |
---
## 10. Imports / APIs Externas (DLLs)
**O que é:** Lista de funções externas importadas de DLLs (ex: `KERNEL32.CreateFileA`, `USER32.MessageBoxA`) com nome da DLL, nome da função e endereço na IAT.
**Para que serve:** Saber quais APIs do sistema operacional o binário usa. Permite inferir capacidades (ex: se importa `WS2_32`, usa rede; se importa `CRYPT32`, usa criptografia).
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | Iterar segmentos externos (`SegByName(".idata")`) e usar `Name(ea)` nos endereços da IAT. Alternativa: parse da tabela de imports manualmente. | ✅ Médio (requer lógica de parse) |
| IDA 7 | `ida_nalt.get_import_module_name()`, `idaapi.enum_import_names()` | ✅ Fácil |
| Ghidra | `program.getExternalManager().getExternalLibraryNames()`, `getExternalLibraryFunctions()` | ✅ Fácil |
---
## 11. Frame da Função (variáveis locais, argumentos)
**O que é:** Estrutura da stack frame: variáveis locais (com nome e offset negativo do bp), argumentos (offset positivo), saved registers, tamanho total do frame.
**Para que serve:** Entender a assinatura implícita da função (quantos args, quantas vars locais). O frontend pode exibir uma tabela de variáveis locais junto com o assembly.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `GetFrame(ea)` retorna o id da estrutura, `GetStrucSize(id)`, iterar membros com `GetStrucMember()`/`GetMemberName()` | ✅ Médio (API de estruturas frágil no IDA 5) |
| IDA 7 | `ida_frame.get_frame(ea)`, `ida_struct.get_struc_members()` | ✅ Fácil |
| Ghidra | `function.getStackFrame()`, `stackFrame.getStackVariables()` | ✅ Fácil |
---
## 12. Comentários do Disassembler
**O que é:** Comentários gerados automaticamente pelo disassembler (ex: `; CODE XREF: sub_1000+5A`, `; char` em loads de strings) e comentários manuais do analista.
**Para que serve:** Enriquece a exibição do assembly. Comentários automáticos do IDA/Ghidra dão dicas de tipos e cross-references que ajudam na leitura.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `GetCommentEx(ea, 0)` para comentários regulares, `GetRptCmt(ea)` para comentários repetíveis | ✅ Médio (precisa testar quais flags funcionam) |
| IDA 7 | `ida_bytes.get_cmt(ea, False)`, `ida_bytes.get_cmt(ea, True)` | ✅ Fácil |
| Ghidra | `listing.getComment(CodeUnit.EOL_COMMENT, address)` e `PRE_COMMENT`, `POST_COMMENT` | ✅ Fácil |
---
## 13. Data Xrefs (leitura/escrita em globais)
**O que é:** Referências de instruções para endereços de dados — quem lê e quem escreve cada variável global.
**Para que serve:** Entender o fluxo de dados. Ex: "a variável global `g_score` é escrita por `addPoints()` e lida por `drawHUD()`".
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `Dfirst(ea)` e `Dnext(ea, ref)` retornam data xrefs. API existe mas suporte pode ser limitado. | ⚠️ Limitado (testar viabilidade) |
| IDA 7 | `ida_xref.xrefs_to(ea)` com filtro `data`, ou `ida_xref.xrefblk_t()` | ✅ Médio |
| Ghidra | `referenceManager.getReferencesTo(address)` e filtrar por tipo `DATA` | ✅ Médio |
---
## 14. Tipos e Assinaturas de Funções
**O que é:** Tipo de retorno, parâmetros com nome e tipo, calling convention (`__cdecl`, `__stdcall`, `__fastcall`), tipo `void*`, tamanho de cada parâmetro.
**Para que serve:** Assinatura completa da função. Permite ao frontend mostrar "`int sub_1000(HWND hWnd, LPCSTR lpText)`" em vez de só o nome.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | Suporte a tipos muito limitado. `GetType()` pode retornar string, mas raramente populado. | ❌ **Não viável** |
| IDA 7 | `ida_typeinf.get_tinfo(ea)`, `ida_typeinf.print_tinfo()` | ✅ Médio |
| Ghidra | `function.getReturnType()`, `function.getParameters()`, `function.getSignature()` | ✅ Fácil |
---
## 15. Estruturas e Enums
**O que é:** Layout de structs e enums reconhecidos pelo disassembler, com nomes de campos e tipos.
**Para que serve:** Entender estruturas de dados usadas pelo binário. Ex: mostrar que `[ebp+8]` acessa `struct Player.name`.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `GetStrucId()`, `GetStrucName()` — API existe mas tipos raramente aplicados pelo IDA 5 | ❌ **Não viável** |
| IDA 7 | `ida_struct.get_struc_list()`, `ida_typeinf` para tipos locais | ⚠️ Limitado (depende da análise do IDA) |
| Ghidra | `program.getDataTypeManager().getAllStructures()` | ✅ Médio |
---
## 16. Decompilação (Pseudocódigo)
**O que é:** Código de alto nível reconstruído a partir do assembly — `if/else`, `while`, chamadas de função com nomes de parâmetros.
**Para que serve:** Leitura muito mais rápida do que assembly. É o principal diferencial do Ghidra e IDA 7+ sobre o IDA 5.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | Não tem decompiler (Hex-Rays só a partir do IDA 6.6+) | ❌ **Não existe** |
| IDA 7 | `ida_hexrays.decompile(ea)` retorna AST, `cfunc.print()` gera string | ✅ Fácil (requer licença Hex-Rays) |
| Ghidra | `decompInterface.decompileFunction(function, timeout, monitor)``decompResults.getDecompiledFunction().getC()` | ✅ Fácil (nativo e gratuito) |
---
## 17. Call Graph / Control Flow Graph (CFG)
**O que é:** Grafo direcionado de chamadas entre funções (call graph) e blocos básicos dentro de cada função (CFG) com arestas representando branches condicionais.
**Para que serve:** Visualização gráfica do fluxo do programa. O frontend pode renderizar um grafo interativo de chamadas e blocos.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | `GetFchunkAttr(ea, FUNCATTR_START)` e iteração sobre chunks. `FlowChart` não disponível no IDC 5. | ⚠️ Limitado (possível via chunks) |
| IDA 7 | `ida_gdl.FlowChart(f)` retorna nós e arestas; `ida_gdl.gen_flow_graph()` | ✅ Médio |
| Ghidra | `BasicBlockModel` + iteração sobre `function.getBody()` → blocos, `function.getCalledFunctions()` → call graph | ✅ Médio |
---
## 18. Nomes de Variáveis Globais
**O que é:** Labels/names atribuídos a endereços de dados globais (ex: `g_score`, `aHelloWorld`).
**Para que serve:** Dar significado a acessos de memória. Transforma `mov eax, dword_403000` em `mov eax, g_score`.
| Engine | API | Viabilidade |
|--------|-----|:-----------:|
| IDA 5 | Iterar segmentos de dados e usar `Name(ea)` + `isData(GetFlags(ea))` | ✅ Médio |
| IDA 7 | `ida_name.get_name_list()`, `ida_bytes.is_data()` | ✅ Fácil |
| Ghidra | `program.getSymbolTable().getGlobalSymbols()`, `listing.getDataAt()` | ✅ Fácil |
---
## Prioridades Recomendadas
1. **Entry point + Tipo de xref + Flags** — baixo esforço, alto valor, fecha o básico
2. **Strings + Imports** — esforço médio, altíssimo valor para entendimento do binário
3. **Segmentos + Tamanho da função** — baixo esforço, enriquece o contexto
4. **Decompilação** (Ghidra/IDA 7) — esforço médio, é o grande diferencial vs IDA 5
5. **Frame + Variáveis globais** — enriquece a visualização de cada função
6. **Call graph / CFG** — habilita visualização gráfica no frontend
7. **Tipos + Estruturas** — depende muito da engine, menor prioridade