From 90ee74b3b9dcd98a46adf5d56cd03e0cc6c654ad Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Mon, 8 Jun 2026 20:13:21 -0300 Subject: [PATCH] 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. --- AGENTS.md | 189 ++++++++++ docs/ai/api-design.md | 404 ++++++++++++++++++++ docs/ai/architecture.md | 281 ++++++++++++++ docs/ai/configuration.md | 438 +++++++++++++++++++++ docs/ai/data-model.md | 369 ++++++++++++++++++ docs/ai/frontend-guide.md | 671 +++++++++++++++++++++++++++++++++ docs/ai/integration-points.md | 424 +++++++++++++++++++++ docs/ai/rag-pipeline.md | 404 ++++++++++++++++++++ docs/ida5-extraction-points.md | 292 ++++++++++++++ 9 files changed, 3472 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/ai/api-design.md create mode 100644 docs/ai/architecture.md create mode 100644 docs/ai/configuration.md create mode 100644 docs/ai/data-model.md create mode 100644 docs/ai/frontend-guide.md create mode 100644 docs/ai/integration-points.md create mode 100644 docs/ai/rag-pipeline.md create mode 100644 docs/ida5-extraction-points.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0a8eace --- /dev/null +++ b/AGENTS.md @@ -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(); +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. diff --git a/docs/ai/api-design.md b/docs/ai/api-design.md new file mode 100644 index 0000000..f43b552 --- /dev/null +++ b/docs/ai/api-design.md @@ -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> sendMessage( + @PathVariable UUID sessionId, + @Valid @RequestBody ChatRequest request) { + + return chatService.chat(sessionId, request.content()) + .map(token -> ServerSentEvent.builder() + .data("{\"type\":\"chunk\",\"content\":\"" + escapeJson(token) + "\"}") + .build()) + .concatWith(chatService.finalizeMessage(sessionId) + .map(msg -> ServerSentEvent.builder() + .data("{\"type\":\"done\",\"messageId\":\"" + msg.id() + "\"," + + "\"sessionId\":\"" + sessionId + "\"," + + "\"tokenCount\":" + msg.tokenCount() + "}") + .build())) + .onErrorResume(e -> Flux.just( + ServerSentEvent.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) diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md new file mode 100644 index 0000000..f9e0658 --- /dev/null +++ b/docs/ai/architecture.md @@ -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 32–4096 | +| **Vector Store** | pgvector (PostgreSQL 18) | Armazenamento e busca por similaridade de cosseno | +| **Vector Index** | HNSW (Hierarchical Navigable Small World) | Índice para busca vetorial rápida | +| **Message Broker** | RabbitMQ 4 | Processamento assíncrono de jobs de embedding | +| **Arquitetura** | Spring Modulith | Módulo desacoplado com eventos de domínio | + +## Diagrama de Componentes + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ ai.decompile.ai (Módulo AI) │ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ EmbeddingService │ │ ChatService │ │ +│ │ │ │ │ │ +│ │ • embed(text) │ │ • createSession() │ │ +│ │ • embedBatch() │ │ • chat(sessionId, │ │ +│ │ • indexBinary(id) │ │ message) │ │ +│ │ • searchSimilar() │ │ • getHistory() │ │ +│ │ • deleteByBinary() │ │ • deleteSession() │ │ +│ └────────┬─────────────┘ └──────────┬───────────┘ │ +│ │ │ │ +│ ┌────────┴─────────────────────────────┴──────────────────┐ │ +│ │ RagService │ │ +│ │ │ │ +│ │ • retrieveContext(binaryId, query) → List │ │ +│ │ • buildSystemPrompt(chunks, binaryMeta) → String │ │ +│ │ • expandCallGraph(functionId, hops) → List │ │ +│ │ • deduplicateAndRank(results) → List │ │ +│ └────────┬──────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────┴────────────────┬──────────────────────────────┐ │ +│ │ 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` (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>`) +- 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 diff --git a/docs/ai/configuration.md b/docs/ai/configuration.md new file mode 100644 index 0000000..07da206 --- /dev/null +++ b/docs/ai/configuration.md @@ -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 + + me.paulschwarz + spring-dotenv + 4.0.0 + +``` + +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) diff --git a/docs/ai/data-model.md b/docs/ai/data-model.md new file mode 100644 index 0000000..dd8f87f --- /dev/null +++ b/docs/ai/data-model.md @@ -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 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) diff --git a/docs/ai/frontend-guide.md b/docs/ai/frontend-guide.md new file mode 100644 index 0000000..f8311cf --- /dev/null +++ b/docs/ai/frontend-guide.md @@ -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 | 1–4000 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([]); + const [activeSessionId, setActiveSessionId] = useState(null); + const [messages, setMessages] = useState([]); + const [streamingContent, setStreamingContent] = useState(''); + const [isStreaming, setIsStreaming] = useState(false); + const abortRef = useRef(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(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 ( +
+
+ {messages.map(msg => ( + + ))} + {isStreaming && ( + + )} +
+
+ +
+