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.
20 KiB
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:
- Após upload + análise estática de um binário, embeddings são gerados automaticamente
- O usuário cria uma sessão de chat vinculada ao binário
- Mensagens são enviadas e a resposta da LLM é recebida via Server-Sent Events (SSE)
- 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):
{
"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):
[
{
"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):
[
{
"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:
{
"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 (
doneouerror) - 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.contentpara 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
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)
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
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:
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
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:
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).