332 lines
13 KiB
Markdown
332 lines
13 KiB
Markdown
---
|
|
id: "03-02"
|
|
phase: 03-miroflow-embutido
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["03-01"]
|
|
files_modified:
|
|
- server/miroflow/routes.ts
|
|
- server/miroflow/engine-proxy.ts
|
|
- server/routes.ts
|
|
autonomous: true
|
|
requirements:
|
|
- REQ-3.5
|
|
- REQ-3.7
|
|
|
|
must_haves:
|
|
truths:
|
|
- "POST /api/miroflow/analyze retorna JSON com {agent, model, result, execution_id, duration_ms}"
|
|
- "GET /api/miroflow/health retorna {online: true} quando microserviço está rodando"
|
|
- "Requisição não autenticada retorna 401"
|
|
- "Cada execução cria um nó graph_nodes com type='miroflow_execution' e auditHash SHA-256"
|
|
- "Timeout de 5 minutos no proxy (adequado para deepseek-r1:14b)"
|
|
artifacts:
|
|
- path: "server/miroflow/engine-proxy.ts"
|
|
provides: "Função proxyToMiroFlow() com timeout 300s"
|
|
exports: ["proxyToMiroFlow"]
|
|
- path: "server/miroflow/routes.ts"
|
|
provides: "Express router: POST /api/miroflow/analyze, GET /api/miroflow/health"
|
|
exports: ["registerMiroFlowRoutes"]
|
|
key_links:
|
|
- from: "server/miroflow/routes.ts"
|
|
to: "server/python/miroflow_service.py"
|
|
via: "fetch() para http://localhost:8006/analyze"
|
|
pattern: "MIROFLOW_PORT.*8006"
|
|
- from: "server/miroflow/routes.ts"
|
|
to: "server/graph/service.ts"
|
|
via: "createNode({type: 'miroflow_execution', ...})"
|
|
pattern: "createNode.*miroflow_execution"
|
|
- from: "server/routes.ts"
|
|
to: "server/miroflow/routes.ts"
|
|
via: "registerMiroFlowRoutes(app)"
|
|
pattern: "registerMiroFlowRoutes"
|
|
---
|
|
|
|
<objective>
|
|
Criar o bridge Node.js → Python para o MiroFlow: proxy TypeScript (engine-proxy.ts), rotas Express (routes.ts) e registro imutável de execuções no KG via graph_nodes.
|
|
|
|
Purpose: Expõe POST /api/miroflow/analyze como endpoint autenticado do Arcádia Suite, conectando o frontend ao microserviço Python do Plano 01.
|
|
Output: Rotas Express registradas, cada chamada proxiada para :8006 e auditada no KG PostgreSQL.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/03-miroflow-embutido/03-01-SUMMARY.md
|
|
@.planning/phases/03-miroflow-embutido/03-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- Padrão de proxy existente — server/bi/engine-proxy.ts -->
|
|
// TIMEOUT padrão atual: 30000ms — MiroFlow PRECISA de 300000ms (5min)
|
|
async function proxyToEngine(path: string, options: RequestInit = {}): Promise<any> {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), BI_ENGINE_TIMEOUT);
|
|
const response = await fetch(`${BI_ENGINE_URL}${path}`, { ...options, signal: controller.signal });
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
|
throw new Error(error.detail || `BI Engine error: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
|
|
export function registerBiEngineRoutes(app: Express): void {
|
|
app.post("/api/bi-engine/analyze", async (req, res) => {
|
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
|
...
|
|
});
|
|
}
|
|
|
|
<!-- KG service — server/graph/service.ts -->
|
|
export async function createNode(data: InsertGraphNode): Promise<GraphNode>
|
|
// InsertGraphNode: { type: string, tenantId?: number, data: object }
|
|
// graph_nodes tabela PostgreSQL — NÃO usar Neo4j direto
|
|
|
|
<!-- Registro de routes em server/routes.ts (linha ~105) -->
|
|
import { registerBiEngineRoutes } from "./bi/engine-proxy"; // padrão a seguir
|
|
registerBiEngineRoutes(app); // linha ~105
|
|
|
|
<!-- AnalyzeResponse do microserviço Python (03-01) -->
|
|
{
|
|
agent: string, // "statistician" | "fiscal_auditor" | "researcher"
|
|
model: string, // "deepseek-r1:14b" | "llama3.1:8b"
|
|
result: string,
|
|
execution_id: string, // UUID v4
|
|
duration_ms: number
|
|
}
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Criar server/miroflow/engine-proxy.ts e server/miroflow/routes.ts</name>
|
|
<files>server/miroflow/engine-proxy.ts, server/miroflow/routes.ts</files>
|
|
|
|
<read_first>
|
|
- /opt/arcadia_merged/server/bi/engine-proxy.ts (padrão completo de proxy a seguir)
|
|
- /opt/arcadia_merged/server/graph/service.ts (createNode signature, linhas 41-49)
|
|
- /opt/arcadia_merged/server/graph/routes.ts (confirmar que /api/graph/nodes aceita POST)
|
|
- /opt/arcadia_merged/server/auth.ts (confirmar req.isAuthenticated() e req.user shape)
|
|
</read_first>
|
|
|
|
<behavior>
|
|
- proxyToMiroFlow("/health", "GET") faz fetch para MIROFLOW_URL/health com timeout 5s
|
|
- proxyToMiroFlow("/analyze", "POST", body) faz fetch para MIROFLOW_URL/analyze com timeout 300000ms
|
|
- POST /api/miroflow/analyze sem autenticação retorna 401 {"error": "Não autenticado"}
|
|
- POST /api/miroflow/analyze autenticado encaminha body + tenant_id para :8006/analyze
|
|
- Após resposta bem-sucedida, registerExecutionInKG cria nó com type="miroflow_execution"
|
|
- auditHash é SHA-256 do JSON.stringify({execution_id, agent, model, input, output})
|
|
- GET /api/miroflow/health não requer autenticação, retorna {online: bool, url: string}
|
|
</behavior>
|
|
|
|
<action>
|
|
Criar diretório server/miroflow/ se não existir.
|
|
|
|
Criar server/miroflow/engine-proxy.ts:
|
|
```typescript
|
|
import type { Express, Request, Response } from "express";
|
|
import crypto from "crypto";
|
|
import { createNode } from "../graph/service";
|
|
|
|
const MIROFLOW_HOST = process.env.MIROFLOW_HOST || "localhost";
|
|
const MIROFLOW_PORT = parseInt(process.env.MIROFLOW_PORT || "8006", 10);
|
|
const MIROFLOW_URL = `http://${MIROFLOW_HOST}:${MIROFLOW_PORT}`;
|
|
const MIROFLOW_TIMEOUT = 300_000; // 5 minutos — deepseek-r1:14b pode levar 60-120s
|
|
|
|
async function proxyToMiroFlow(path: string, method: string = "GET", body?: object): Promise<any> {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), MIROFLOW_TIMEOUT);
|
|
try {
|
|
const response = await fetch(`${MIROFLOW_URL}${path}`, {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeout);
|
|
if (!response.ok) {
|
|
const err = await response.json().catch(() => ({ detail: response.statusText }));
|
|
throw new Error(err.detail || `MiroFlow error: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (err: any) {
|
|
clearTimeout(timeout);
|
|
if (err.name === "AbortError") throw new Error("MiroFlow timeout (300s)");
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function registerExecutionInKG(req: Request, execData: any, inputBody: any): Promise<void> {
|
|
try {
|
|
const auditHash = crypto
|
|
.createHash("sha256")
|
|
.update(JSON.stringify({
|
|
execution_id: execData.execution_id,
|
|
agent: execData.agent,
|
|
model: execData.model,
|
|
input: inputBody,
|
|
output: execData.result,
|
|
}))
|
|
.digest("hex");
|
|
|
|
await createNode({
|
|
type: "miroflow_execution",
|
|
tenantId: (req.user as any)?.tenantId ?? null,
|
|
data: {
|
|
...execData,
|
|
input: inputBody,
|
|
auditHash,
|
|
immutable: true,
|
|
registeredAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
} catch (kgErr: any) {
|
|
// KG failure não deve bloquear a resposta ao cliente
|
|
console.error("[MiroFlow] Falha ao registrar no KG:", kgErr.message);
|
|
}
|
|
}
|
|
|
|
export function registerMiroFlowRoutes(app: Express): void {
|
|
app.get("/api/miroflow/health", async (_req: Request, res: Response) => {
|
|
try {
|
|
const data = await proxyToMiroFlow("/health", "GET");
|
|
res.json({ online: true, url: MIROFLOW_URL, ...data });
|
|
} catch {
|
|
res.json({ online: false, url: MIROFLOW_URL });
|
|
}
|
|
});
|
|
|
|
app.post("/api/miroflow/analyze", async (req: Request, res: Response) => {
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ error: "Não autenticado" });
|
|
}
|
|
try {
|
|
const inputBody = {
|
|
...req.body,
|
|
tenant_id: (req.user as any)?.tenantId ?? null,
|
|
};
|
|
const data = await proxyToMiroFlow("/analyze", "POST", inputBody);
|
|
// Registrar no KG de forma assíncrona (não bloqueia a resposta)
|
|
registerExecutionInKG(req, data, req.body).catch(() => {});
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
res.status(502).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
console.log(`[MiroFlow Proxy] Rotas registradas -> ${MIROFLOW_URL}`);
|
|
}
|
|
```
|
|
|
|
Criar server/miroflow/routes.ts como re-export limpo (manter compatibilidade com o loader de módulos):
|
|
```typescript
|
|
export { registerMiroFlowRoutes } from "./engine-proxy";
|
|
```
|
|
|
|
IMPORTANTE:
|
|
- Timeout de 300_000ms (5 minutos) — NÃO usar o padrão de 30s do bi/engine-proxy.ts
|
|
- registerExecutionInKG usa createNode do graph/service.ts, NÃO faz fetch para /api/graph/nodes
|
|
- Falha no KG não retorna erro ao cliente (try/catch com log)
|
|
- auditHash cobre: execution_id + agent + model + input + output
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>cd /opt/arcadia_merged && npx tsc --noEmit --project tsconfig.json 2>&1 | grep -i "miroflow" | head -10 || echo "TypeScript OK (nenhum erro em miroflow)"</automated>
|
|
</verify>
|
|
|
|
<acceptance_criteria>
|
|
- server/miroflow/engine-proxy.ts existe e compila sem erros TypeScript
|
|
- server/miroflow/routes.ts existe (re-export ou implementação direta)
|
|
- MIROFLOW_TIMEOUT = 300_000 está definido (não 30000)
|
|
- Função registerExecutionInKG usa createNode importado de "../graph/service"
|
|
- auditHash calculado com crypto.createHash("sha256")
|
|
- POST /api/miroflow/analyze faz verificação req.isAuthenticated() antes de proxiar
|
|
- grep -n "300_000\|300000" server/miroflow/engine-proxy.ts retorna resultado
|
|
- grep -n "miroflow_execution" server/miroflow/engine-proxy.ts retorna resultado
|
|
</acceptance_criteria>
|
|
|
|
<done>Arquivos TypeScript criados sem erros de compilação. Funções exportadas: registerMiroFlowRoutes. Timeout de 5min configurado. KG logging com SHA-256 implementado.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Registrar MiroFlow em server/routes.ts</name>
|
|
<files>server/routes.ts</files>
|
|
|
|
<read_first>
|
|
- /opt/arcadia_merged/server/routes.ts (ARQUIVO INTEIRO — verificar imports e onde registerBiEngineRoutes é chamado para inserir MiroFlow no mesmo padrão)
|
|
</read_first>
|
|
|
|
<action>
|
|
Adicionar import e registro das rotas MiroFlow em server/routes.ts, seguindo o padrão exato de registerBiEngineRoutes:
|
|
|
|
1. Adicionar import após a linha que importa registerBiEngineRoutes (linha ~19):
|
|
```typescript
|
|
import { registerMiroFlowRoutes } from "./miroflow/engine-proxy";
|
|
```
|
|
|
|
2. Adicionar chamada após registerBiEngineRoutes(app) (linha ~105):
|
|
```typescript
|
|
registerMiroFlowRoutes(app);
|
|
```
|
|
|
|
NÃO alterar nenhuma outra linha do arquivo. NÃO mover, reordenar ou reformatar código existente.
|
|
NÃO modificar configurações do Superset ou rotas existentes.
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>cd /opt/arcadia_merged && grep -n "registerMiroFlowRoutes" server/routes.ts && npx tsc --noEmit --project tsconfig.json 2>&1 | grep -c "error" || echo "0 erros"</automated>
|
|
</verify>
|
|
|
|
<acceptance_criteria>
|
|
- `grep "registerMiroFlowRoutes" server/routes.ts` retorna 2 linhas (import + chamada)
|
|
- `npx tsc --noEmit` não retorna erros relacionados a miroflow
|
|
- A linha de import está próxima de registerBiEngineRoutes (mesma região do arquivo)
|
|
- A chamada registerMiroFlowRoutes(app) está próxima de registerBiEngineRoutes(app)
|
|
- Nenhuma linha existente do arquivo foi modificada (verificar com git diff)
|
|
</acceptance_criteria>
|
|
|
|
<done>server/routes.ts registra as rotas MiroFlow. POST /api/miroflow/analyze e GET /api/miroflow/health estarão disponíveis quando o servidor Node iniciar.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
# Compilação TypeScript limpa:
|
|
cd /opt/arcadia_merged && npx tsc --noEmit 2>&1 | grep -i "error" | head -5
|
|
|
|
# Confirmar registro no KG:
|
|
grep -n "miroflow_execution\|auditHash\|createNode" /opt/arcadia_merged/server/miroflow/engine-proxy.ts
|
|
|
|
# Confirmar timeout correto:
|
|
grep -n "300" /opt/arcadia_merged/server/miroflow/engine-proxy.ts
|
|
|
|
# Confirmar rotas registradas:
|
|
grep -n "registerMiroFlowRoutes" /opt/arcadia_merged/server/routes.ts
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- server/miroflow/engine-proxy.ts e server/miroflow/routes.ts existem
|
|
- TypeScript compila sem erros nos novos arquivos
|
|
- server/routes.ts inclui import e chamada registerMiroFlowRoutes
|
|
- Timeout de 300s configurado no proxy
|
|
- auditHash SHA-256 calculado e salvo via createNode
|
|
- GET /api/miroflow/health acessível sem autenticação
|
|
- POST /api/miroflow/analyze requer autenticação (401 sem sessão)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Após conclusão, criar `.planning/phases/03-miroflow-embutido/03-02-SUMMARY.md` com:
|
|
- Arquivos criados/modificados
|
|
- Resultado da compilação TypeScript
|
|
- Confirmação do padrão de timeout (300s)
|
|
- Confirmação do KG logging com auditHash
|
|
</output>
|