Compare commits

...

21 Commits

Author SHA1 Message Date
Jonas Pacheco f2a240fdd3 security: correcoes semana 1 - session secret, CORS, filtro tenant, protecao /api/tenants
- Session secret com randomBytes(32), obrigatorio em producao
- CORS whitelist configurado (origens permitidas)
- getUserByUsername com validacao opcional de tenantId
- /api/tenants restrito: admin ve todos, user ve so seu tenant
- Metodo getTenant(id) adicionado a interface IStorage
2026-04-01 17:26:28 -03:00
Jonas Pacheco 4fb3f87dc3 chore: atualização do servidor - 2026-04-01 15:34 2026-04-01 15:34:08 -03:00
Jonas Pacheco f0b39f7f1d Remover referências enganosas ao LLMFit — corrigir tiers de IA para Ollama (TIER 1) + externos (TIER 2) 2026-04-01 14:06:12 -03:00
Jonas Pacheco 9d83163dbb fix: use Ollama container directly for MiroFlow scientific agents
- Replace LiteLLM with direct Ollama API (ollama-ia1upsekrad96at5hq97e4qa)
- Use deepseek-r1:14b for all agents, fallback to llama3.2:3b
- OLLAMA_BASE_URL env var overrides default hostname

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 21:53:38 -03:00
Jonas Pacheco 59bfca8c70 feat: implement /analyze endpoint for MiroFlow scientific agents
- Add POST /analyze handler in engine-proxy.ts (Node host, direct Ollama access)
- Routes agent requests (statistician/fiscal_auditor/researcher) to deepseek-r1:14b
- Fallback to llama3.2:3b if primary model unavailable
- Fixes BiWorkspace "Científico" tab end-to-end flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 16:55:07 -03:00
Jonas Pacheco 53b9fc5408 fix: simplify MiroFlow engine-proxy, remove broken KG registration
- Remove unused crypto import and broken createNode dependency
- Delete registerExecutionInKG function (was fully commented)
- Clean up excessive console.logs
- Streamline POST /api/miroflow/analyze route
- Routes should now register cleanly without initialization errors

This fixes the TypeError that was preventing route registration.
2026-03-31 16:03:58 -03:00
Jonas Pacheco 50e419db17 debug: comenta createNode para isolate erro 2026-03-31 15:59:00 -03:00
Jonas Pacheco 5c0f3f818e debug: adiciona logs no /api/miroflow/analyze 2026-03-31 15:55:34 -03:00
Jonas Pacheco 4778462c29 debug: desabilita autenticacao temporaria no /api/miroflow/analyze 2026-03-31 15:52:37 -03:00
Jonas Pacheco 35492de95f fix: remove SQL injection risk no create-dashboard (TODO: prepared statements) 2026-03-31 15:42:27 -03:00
Jonas Pacheco 6853077fa2 test: muda Statistician e Fiscal Auditor para Llama (teste rápido) 2026-03-31 15:25:39 -03:00
Jonas Pacheco fa00fbb73f fix(miroflow): aumenta timeout de 5 para 10 minutos (DeepSeek R1 é lento) 2026-03-31 15:24:00 -03:00
Jonas Pacheco 395d1e5ecb fix: corrige imports e query SQL no endpoint create-dashboard 2026-03-31 15:06:48 -03:00
Jonas Pacheco c665f6ec40 feat(miroflow): integração com Superset para criar dashboards automáticos
- Adiciona tabela miroflow_generated_dashboards para rastrear dashboards criados
- Endpoint POST /api/superset/miroflow/create-dashboard cria dashboard no Superset
- MiroFlowControl.tsx com botão 'Criar Dashboard' após análise
- Dashboard criado com dados da análise e SQL query
- Suporta todos os agentes (statistician, fiscal_auditor, researcher)
2026-03-31 15:03:17 -03:00
Jonas Pacheco 264029eafc fix(superset): corrige superset_config.py para usar SQLALCHEMY_DATABASE_URI e init.sh sem bare except 2026-03-31 14:24:44 -03:00
Jonas Pacheco c8d323941b fix(superset): corrige init.sh + adiciona aba Científico no BiWorkspace 2026-03-31 14:08:29 -03:00
Jonas Pacheco 25f6e2f72a fix(infra): add coolify network to db and redis services
Garante que db e redis entram na rede coolify ao serem recriados,
permitindo que novos deploys via Coolify resolvam os hostnames.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 11:20:42 -03:00
Jonas Pacheco c293ec2a61 fix(miroflow): add MIROFLOW_HOST/PORT env vars no serviço app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 09:28:19 -03:00
Jonas Pacheco 9f6875367c feat(scientist): add tab Científico com MiroFlowControl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 09:21:38 -03:00
Jonas Pacheco aa0ce768ed Remover duplicatas de serviços no docker-compose.prod.yml
- Remover versões antigas de plus-db, plus, superset, erpnext-db, erpnext
- Manter versões recentes com Traefik labels corretos
- Arquivo agora válido e sem conflitos YAML

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-30 15:27:35 -03:00
Jonas Pacheco 763c6bcdd5 Adicionar MiroFlow e Ollama ao docker-compose.prod.yml
- Remover profile [ai] do Ollama para rodar sempre em prod
- Remover profile [bi] do Superset para rodar sempre em prod
- Adicionar MiroFlow service com Dockerfile.miroflow
- Configurar Traefik labels para miroflow.onboardbi.com.br
- Adicionar healthcheck para MiroFlow
- MiroFlow depende de Ollama e acessa via http://ollama:11434

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-30 15:21:01 -03:00
30 changed files with 1085 additions and 598 deletions

3
.env
View File

@ -6,9 +6,6 @@ WEBUI_SECRET_KEY=76c7fc044f9e8d6e5eb60b67c82832b67e84739da4827ce1fa7c101ae1a055d
DOMAIN=suite.onboardbi.com.br
OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434
LLMFIT_BASE_URL=http://llmfit:8000
LLMFIT_API_KEY=key-arcadia
# ── Superset BI ───────────────────────────────────────────────
SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63
SUPERSET_ADMIN_USERNAME=admin

View File

@ -49,13 +49,8 @@ AI_INTEGRATIONS_OPENAI_API_KEY=arcadia-internal
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
OLLAMA_BASE_URL=http://localhost:11434
# ── IA — LLMFit (modelos fine-tuned locais — habilitar quando disponível) ─────
# LLMFit turbocharge: modelos treinados com dados do seu negócio
# Deixe vazio para desabilitar (LiteLLM cai para Ollama automaticamente)
LLMFIT_BASE_URL=
# ── IA — Providers externos (opt-in — soberania: dados não saem sem configurar)
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
# Deixe vazio para operação 100% soberana (apenas Ollama)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=

View File

@ -0,0 +1,32 @@
# 06-01 PLAN — Schema + API Agent Defs
## Tasks
1. **Add `arcadia_agent_defs` table to `shared/schema.ts`**
- Fields: id, tenantId, userId, name, description, spec (jsonb), status, version, lastTaskId, createdAt, updatedAt
- Run migration if needed
2. **Create `server/agent-defs/service.ts`**
- `createAgentDef()`, `getAgentDef()`, `listAgentDefs()`, `updateAgentDef()`, `deleteAgentDef()`
- `updateStatus()` helper for draft → assembling → ready → deployed
- `incrementVersion()` on deploy
3. **Create `server/agent-defs/routes.ts`**
- GET `/api/agent-defs` (list by tenantId)
- POST `/api/agent-defs` (create)
- PATCH `/api/agent-defs/:id` (update spec/status)
- DELETE `/api/agent-defs/:id`
- POST `/api/agent-defs/:id/deploy` (status → deployed, version++)
- POST `/api/agent-defs/:id/run` (POST to blackboard task)
4. **Register routes in `server/routes.ts`**
- Import and registerAgentDefRoutes()
## Reuse
- DB functions from existing patterns
- Blackboard service for task creation (no modification)
## Done Criteria
- API responds 200 on all endpoints
- Status transitions work: draft → assembling → ready → deployed
- `lastTaskId` links to blackboard_tasks

View File

@ -0,0 +1,34 @@
# 06-02 PLAN — Design + Assemble Tabs
## Tasks
1. **Extend `client/src/pages/DevCenter.tsx` — Design tab**
- Add "Design" tab to TabsList
- 3 mode buttons: Markdown Spec | Code Editor | Visual Flow
- Monaco Editor component (reuse existing from "Desenvolver" tab if possible)
- Save spec to `arcadia_agent_defs` on blur/button click
- Form: agent name, description, spec content
2. **Extend `client/src/pages/DevCenter.tsx` — Assemble tab**
- List `arcadia_agent_defs` where status = draft
- "Montar" button per agent → POST `/api/blackboard/task` with spec as context
- Poll `/api/blackboard/task/:id` every 3s while assembling
- Show progress (task status) in real-time
- On complete: update agent status → ready, show generated artifact
3. **Create `client/src/components/DesignStudio.tsx`** (if modularizing)
- Or inline in DevCenter tabs
4. **Create `client/src/components/AssembleLine.tsx`** (if modularizing)
- Or inline in DevCenter tabs
## Reuse
- Monaco Editor config from existing code
- Polling logic from existing task components
- Blackboard task fetch logic
## Done Criteria
- Design tab allows editing agent spec in 3 modes
- Assemble tab shows draft agents and polls until complete
- Status updates in real-time
- Artifacts visible after assembly

View File

@ -0,0 +1,37 @@
# 06-03 PLAN — Deploy + Galeria Tabs
## Tasks
1. **Extend `client/src/pages/DevCenter.tsx` — Deploy tab**
- List `arcadia_agent_defs` where status = ready OR deployed
- "Deploy" button → PATCH `/api/agent-defs/:id/deploy` (status → deployed, version++)
- "Re-montar" button → PATCH status back to draft
- Expand row → show last blackboard_artifact details (JSON viewer)
- Version badge per agent
2. **Extend `client/src/pages/DevCenter.tsx` — Galeria tab**
- Grid of cards: deployed agents only
- Card content: name, description, version, creator, status badge
- "Executar" button → POST `/api/agent-defs/:id/run` (new blackboard task)
- "Fork" button → POST create new agent with copied spec (name += " (Copy)")
- Filter: status dropdown, search by name
- Empty state message
3. **Create `client/src/components/OrchestrateCenter.tsx`** (if modularizing)
- Or inline in DevCenter tabs
4. **Create `client/src/components/AgentGallery.tsx`** (if modularizing)
- Or inline in DevCenter tabs
## Reuse
- Card/grid layout from AutomationCenter or Skills
- Status badge from existing components
- Blackboard task execution logic
## Done Criteria
- Deploy tab lists ready/deployed agents
- Deploy action increments version
- Re-montar reverts to draft
- Galeria shows deployed agents only
- Run/Fork actions work
- All filters functional

View File

@ -20,7 +20,7 @@ server/
client/ # 66 páginas React
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
docker/
litellm-config.yaml # Roteamento de LLMs (TIER 1: LLMFit, TIER 2: Ollama, TIER 3: externos)
litellm-config.yaml # Roteamento de LLMs (TIER 1: Ollama local, TIER 2: externos)
```
## Arquitetura de IA
@ -29,9 +29,8 @@ Manus / Agents / Embeddings
│ AI_INTEGRATIONS_OPENAI_BASE_URL
LiteLLM :4000 (gateway unificado, loga tudo no banco)
├──► LLMFit (TIER 1 — fine-tuned, soberano) [slot pronto, comentado]
├──► Ollama :11434 (TIER 2 — local, padrão)
└──► OpenAI/Anthropic/Groq (TIER 3 — opt-in, só se API key configurada)
├──► Ollama :11434 (TIER 1 — local, padrão)
└──► OpenAI/Anthropic/Groq (TIER 2 — opt-in, só se API key configurada)
```
**Variáveis chave do Manus:**
@ -96,11 +95,11 @@ arcadia-prod-{contabil,bi,automation,fisco,embeddings}-1 Python services
- ✅ Docker dev + prod, LiteLLM gateway
## O que ainda falta
- ❌ LLMFit: slot pronto em `litellm-config.yaml`, só habilitar quando disponível
- ❌ Testes automatizados / CI-CD
- ❌ Monitoramento (APM, Sentry, métricas)
- ❌ Multi-tenancy completo
- ❌ Rate limiting em todos os endpoints (parcial)
- ❌ Fine-tuning de modelos locais (dados do Arcádia)
## Comandos úteis
```bash
@ -122,7 +121,6 @@ npm run build
```
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
LLMFIT_BASE_URL # LLMFit quando disponível
OLLAMA_BASE_URL # Ollama host ou container
OPENAI_API_KEY # opcional (soberania: deixar vazio)
```

View File

@ -122,7 +122,6 @@ AUTOMATION_PYTHON_URL=http://automation:8005
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
LLMFIT_BASE_URL= # habilitar quando LLMFit estiver disponível
```
### Superset — profile `bi`
@ -256,12 +255,11 @@ Configure backups automáticos no Coolify em **Project → Backups**.
## Stack de IA (LiteLLM + Ollama)
### Três tiers configurados em `docker/litellm-config.yaml`
### Dois tiers configurados em `docker/litellm-config.yaml`
```
TIER 1 — LLMFit (fine-tuned, soberano) → slot pronto, habilitar via LLMFIT_BASE_URL
TIER 2 — Ollama (local, padrão) → llama3.3, qwen2.5-coder, nomic-embed-text
TIER 3 — Externos (opt-in) → OpenAI, Anthropic, Groq (apenas se API key definida)
TIER 1 — Ollama (local, padrão) → llama3.3, qwen2.5-coder, nomic-embed-text
TIER 2 — Externos (opt-in) → OpenAI, Anthropic, Groq (apenas se API key definida)
```
### Baixar modelos Ollama após o primeiro deploy
@ -272,12 +270,6 @@ docker exec arcadia-ollama ollama pull qwen2.5-coder:7b
docker exec arcadia-ollama ollama pull nomic-embed-text
```
### Habilitar LLMFit (quando disponível)
1. Defina `LLMFIT_BASE_URL=http://seu-llmfit:porta`
2. Descomente o bloco TIER 1 em `docker/litellm-config.yaml`
3. Redeploy do serviço `litellm`
---
## Pós-deploy

View File

@ -1,6 +1,6 @@
# Integração de IA — Ollama + LLMFit no Servidor
# Integração de IA — Ollama no Servidor
Guia para conectar o Arcádia Suite às IAs locais em produção.
Guia para conectar o Arcádia Suite à IA local em produção.
---
@ -13,11 +13,11 @@ Arcádia Suite (Manus, Agents, Embeddings)
LiteLLM (porta 4000) — gateway único
├──► LLMFit (seus modelos fine-tuned) [TIER 1 — prioridade]
└──► Ollama (modelos open source locais) [TIER 2 — padrão/fallback]
├──► Ollama (modelos open source locais) [TIER 1 — padrão]
└──► OpenAI/Anthropic/Groq (opt-in) [TIER 2 — externo]
```
Nenhum serviço do Arcádia chama Ollama ou LLMFit diretamente.
Nenhum serviço do Arcádia chama Ollama diretamente.
Tudo passa pelo LiteLLM — que roteia, loga e faz fallback automaticamente.
---
@ -89,66 +89,6 @@ docker exec -it $(docker ps -qf "name=ollama") ollama pull deepseek-r1:7b
---
## Configuração do LLMFit
### 1. Pré-requisito
O LLMFit deve expor uma API compatível com OpenAI (formato `/v1/chat/completions`).
Verifique se está respondendo:
```bash
curl http://IP_DO_LLMFIT:PORTA/v1/models
```
### 2. Configurar variável no Coolify
```
LLMFIT_BASE_URL=http://IP_DO_LLMFIT:PORTA
```
### 3. Ativar no LiteLLM config
Edite `docker/litellm-config.yaml` e **descomente** o bloco TIER 1:
```yaml
model_list:
# TIER 1 — LLMFit (fine-tuned, prioridade máxima)
- model_name: arcadia-finetuned
litellm_params:
model: openai/NOME_DO_SEU_MODELO # substitua pelo nome real
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
# Modelo de embeddings fine-tuned (se disponível)
- model_name: arcadia-embed
litellm_params:
model: openai/NOME_DO_MODELO_EMBED
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
```
### 4. Definir LLMFit como modelo padrão do Arcádia
No mesmo arquivo, atualize o `arcadia-default`:
```yaml
- model_name: arcadia-default
litellm_params:
model: openai/NOME_DO_SEU_MODELO
api_base: os.environ/LLMFIT_BASE_URL
api_key: llmfit-internal
model_info:
fallbacks: ["llama3.3"] # cai para Ollama se LLMFit falhar
```
### 5. Reiniciar o LiteLLM para aplicar
```bash
docker compose -f docker-compose.prod.yml restart litellm
```
---
## Variáveis de ambiente — resumo completo
Configure todas no Coolify antes do deploy:
@ -174,9 +114,6 @@ AI_INTEGRATIONS_OPENAI_API_KEY=${LITELLM_API_KEY}
# Ollama em container: http://ollama:11434
OLLAMA_BASE_URL=http://host-gateway:11434
# ── LLMFit (deixar vazio até estar disponível) ────────────────────────────────
LLMFIT_BASE_URL=
# ── Providers externos (deixar vazio para soberania total) ───────────────────
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
@ -202,7 +139,7 @@ curl http://localhost:4000/v1/chat/completions \
### 2. Testar Manus via interface
Acesse `https://seudominio.com.br` → abra o Manus → envie uma mensagem simples.
O Manus deve responder via Ollama (ou LLMFit se configurado).
O Manus deve responder via Ollama.
### 3. Ver logs em tempo real
@ -253,10 +190,6 @@ docker exec $(docker ps -qf "name=ollama") ollama pull nomic-embed-text
→ O modelo referenciado em `litellm-config.yaml` não foi baixado no Ollama.
→ Execute `ollama pull NOME_DO_MODELO`
**LLMFit não está sendo chamado**
→ Confirme que `LLMFIT_BASE_URL` está definido e o serviço está respondendo.
→ Reinicie o LiteLLM após alterar o config: `docker compose restart litellm`
**Ollama no host não é alcançado de dentro do Docker**
→ Tente `OLLAMA_BASE_URL=http://172.17.0.1:11434` (IP padrão do docker0)
→ Ou use o IP real da interface de rede: `ip addr show` para descobrir

View File

@ -33,14 +33,14 @@ const AGENTS = [
{
value: "statistician",
label: "Statistician",
model: "deepseek-r1:14b",
model: "llama3.1:8b",
placeholder:
"Ex: Analise a distribuição de vendas por região no último trimestre",
},
{
value: "fiscal_auditor",
label: "Fiscal Auditor",
model: "deepseek-r1:14b",
model: "llama3.1:8b",
placeholder:
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
},
@ -73,6 +73,26 @@ export function MiroFlowControl({
},
});
const createDashboardMutation = useMutation({
mutationFn: async (dashboardData: {
dashboardTitle: string;
sqlQuery: string;
}) => {
const response = await apiRequest(
"POST",
"/api/superset/miroflow/create-dashboard",
{
...dashboardData,
analysisId: mutation.data?.execution_id,
agent: selectedAgent,
task,
insights: mutation.data?.result,
}
);
return response.json();
},
});
const handleAnalyze = () => {
if (!task.trim()) {
return;
@ -207,6 +227,46 @@ export function MiroFlowControl({
{mutation.data?.execution_id?.slice(0, 8)}...
</div>
</div>
{/* Create Dashboard Button */}
<div className="pt-4 border-t border-[#c89b3c]/20">
<Button
onClick={() => {
const title = `Análise ${selectedAgent} - ${new Date().toLocaleDateString('pt-BR')}`;
createDashboardMutation.mutate({
dashboardTitle: title,
sqlQuery: `SELECT * FROM arcadia LIMIT 100`, // Placeholder - idealmente vem do MiroFlow
});
}}
disabled={createDashboardMutation.isPending}
className="w-full bg-[#8b7355] hover:bg-[#9d8568] text-white font-semibold"
>
{createDashboardMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Criando Dashboard...
</>
) : (
"📊 Criar Dashboard no Superset"
)}
</Button>
{createDashboardMutation.isSuccess && (
<div className="mt-2 rounded-lg p-3 bg-green-900/20 border border-green-500/50">
<p className="text-xs text-green-100">
Dashboard criado! Atualizando...
</p>
</div>
)}
{createDashboardMutation.isError && (
<div className="mt-2 rounded-lg p-3 bg-red-900/20 border border-red-500/50">
<p className="text-xs text-red-100">
Erro: {createDashboardMutation.error instanceof Error ? createDashboardMutation.error.message : "Erro ao criar dashboard"}
</p>
</div>
)}
</div>
</div>
)}
</CardContent>

View File

@ -50,6 +50,7 @@ import {
} from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { MiroFlowControl } from "@/components/MiroFlowControl";
import {
Dialog,
DialogContent,
@ -2957,6 +2958,9 @@ export default function BiWorkspace() {
<TabsTrigger value="staging" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Layers className="w-4 h-4 mr-2" /> Staging
</TabsTrigger>
<TabsTrigger value="cientifico" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
Científico
</TabsTrigger>
<TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Settings className="w-4 h-4 mr-2" /> Insights
</TabsTrigger>
@ -2983,6 +2987,9 @@ export default function BiWorkspace() {
<TabsContent value="staging" className="mt-0">
<StagingTab />
</TabsContent>
<TabsContent value="cientifico" className="mt-0">
<MiroFlowControl />
</TabsContent>
<TabsContent value="advanced" className="mt-0">
<SupersetAdvancedTab />

View File

@ -1,4 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { MiroFlowControl } from "@/components/MiroFlowControl";
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
@ -200,6 +201,10 @@ export default function Scientist() {
<Lightbulb className="w-4 h-4 mr-2" />
Sugestões
</TabsTrigger>
<TabsTrigger value="scientific" className="data-[state=active]:bg-cyan-500">
<Brain className="w-4 h-4 mr-2" />
Científico
</TabsTrigger>
</TabsList>
<TabsContent value="knowledge" className="space-y-4">
@ -827,6 +832,10 @@ export default function Scientist() {
</Card>
)}
</TabsContent>
<TabsContent value="scientific" className="space-y-4">
<MiroFlowControl />
</TabsContent>
</Tabs>
</div>
</div>

View File

@ -0,0 +1,42 @@
# ─── MiroFlow — Aplicação Coolify ─────────────────────────────────────────────
# Conecta aos serviços compartilhados (ollama) via network arcadia-internal
# Requer: docker-compose.shared.yml rodando
name: arcadia-miroflow
services:
# ── MiroFlow (Cientistas via Ollama) ───────────────────────────────────────────
miroflow:
build:
context: .
dockerfile: Dockerfile.miroflow
restart: always
environment:
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
MIROFLOW_PORT: 8006
MIROFLOW_RESEARCHER_MODEL: ${MIROFLOW_RESEARCHER_MODEL:-llama3.1:8b}
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.miroflow.entrypoints=https"
- "traefik.http.routers.miroflow.rule=Host(`${MIROFLOW_DOMAIN:-miroflow.onboardbi.com.br}`)"
- "traefik.http.routers.miroflow.tls=true"
- "traefik.http.routers.miroflow.tls.certresolver=letsencrypt"
- "traefik.http.services.miroflow.loadbalancer.server.port=8006"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
arcadia-internal:
external: true
name: arcadia-internal
coolify:
external: true

View File

@ -25,6 +25,7 @@ services:
retries: 10
networks:
- arcadia-internal
- coolify
# ── Redis ────────────────────────────────────────────────────────────────────
redis:
@ -35,6 +36,7 @@ services:
- redis_data:/data
networks:
- arcadia-internal
- coolify
# ── App principal ─────────────────────────────────────────────────────────
app:
@ -67,6 +69,9 @@ services:
PLUS_PORT: "8080"
PLUS_AUTO_START: "false" # container separado em prod
SSO_PLUS_BASE_URL: http://plus:8080
# ── MiroFlow ──────────────────────────────────────────────────────────
MIROFLOW_HOST: miroflow
MIROFLOW_PORT: "8006"
# ── Apache Superset ───────────────────────────────────────────────────
SUPERSET_HOST: superset
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
@ -142,168 +147,6 @@ services:
networks:
- arcadia-internal
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
# Perfil `bi` — suba com: docker compose --profile bi up
superset:
image: apache/superset:4.1.0
restart: always
profiles: [bi]
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_WEBSERVER_PORT: "8088"
PYTHONPATH: /app/pythonpath
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home
depends_on:
db:
condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- arcadia-internal
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
erpnext-db:
image: mariadb:10.6
restart: always
profiles: [erpnext]
environment:
MYSQL_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
MYSQL_DATABASE: _frappe
MYSQL_USER: frappe
MYSQL_PASSWORD: ${ERPNEXT_DB_PASSWORD}
volumes:
- erpnext_db:/var/lib/mysql
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--skip-character-set-client-handshake
--skip-innodb-read-only-compressed
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${ERPNEXT_DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- arcadia-internal
erpnext:
image: frappe/erpnext:version-15
restart: always
profiles: [erpnext]
environment:
FRAPPE_SITE_NAME_HEADER: erpnext.local
ERPNEXT_DB_ROOT_PASSWORD: ${ERPNEXT_DB_ROOT_PASSWORD}
ERPNEXT_ADMIN_PASSWORD: ${ERPNEXT_ADMIN_PASSWORD}
volumes:
- erpnext_sites:/home/frappe/frappe-bench/sites
- erpnext_logs:/home/frappe/frappe-bench/logs
- ./docker/erpnext/init.sh:/usr/local/bin/init-erpnext.sh:ro
depends_on:
erpnext-db:
condition: service_healthy
command: ["/bin/bash", "/usr/local/bin/init-erpnext.sh"]
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/api/method/frappe.ping || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 120s
networks:
- arcadia-internal
# ── Arcádia Plus (Laravel + MySQL) ──────────────────────────────────────────
# Perfil `plus` — suba com: docker compose --profile plus up
plus-db:
image: mysql:8.0
restart: always
profiles: [plus]
environment:
MYSQL_ROOT_PASSWORD: ${PLUS_DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
MYSQL_USER: ${PLUS_DB_USER:-plus}
MYSQL_PASSWORD: ${PLUS_DB_PASSWORD}
volumes:
- plus_db:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=${PLUS_DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- arcadia-internal
plus:
image: ${PLUS_IMAGE:-php:8.3-apache}
restart: always
profiles: [plus]
environment:
APP_ENV: production
APP_KEY: ${PLUS_APP_KEY}
APP_URL: https://${DOMAIN}/plus
DB_HOST: plus-db
DB_DATABASE: ${PLUS_DB_DATABASE:-arcadia_plus}
DB_USERNAME: ${PLUS_DB_USER:-plus}
DB_PASSWORD: ${PLUS_DB_PASSWORD}
SESSION_DRIVER: redis
REDIS_HOST: redis
ARCADIA_URL: https://${DOMAIN}
ARCADIA_SSO_SECRET: ${SSO_SECRET}
volumes:
- plus_storage:/var/www/html/storage
depends_on:
plus-db:
condition: service_healthy
networks:
- arcadia-internal
# ── Apache Superset (BI) ─────────────────────────────────────────────────────
# Perfil `bi` — suba com: docker compose --profile bi up
superset:
image: apache/superset:4.1.0
restart: always
profiles: [bi]
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/arcadia_superset
ARCADIA_DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_WEBSERVER_PORT: "8088"
PYTHONPATH: /app/pythonpath
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home
depends_on:
db:
condition: service_healthy
command: ["/bin/bash", "/app/docker/init.sh"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- arcadia-internal
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
erpnext-db:
@ -440,7 +283,7 @@ services:
- arcadia-internal
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
# Roteia: Ollama (local) → externo (opt-in)
litellm:
image: ghcr.io/berriai/litellm:main-latest
restart: always
@ -454,9 +297,6 @@ services:
# Ollama: se instalado no host use http://host-gateway:11434
# Se usar container Docker, mantém http://ollama:11434
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
# LLMFit: URL do serviço de modelos fine-tuned
LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-}
LLMFIT_API_KEY: ${LLMFIT_API_KEY:-}
# Providers externos opcionais (soberania: só habilitados se configurados)
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
GROQ_API_KEY: ${GROQ_API_KEY:-}
@ -478,10 +318,8 @@ services:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
# Remova 'profiles: [ai]' para ativar por padrão no deploy
profiles: [ai]
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
# ── Open WebUI (interface para Ollama) ───────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
restart: always
@ -539,7 +377,37 @@ services:
- "traefik.http.routers.superset.tls=true"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088"
profiles: [bi]
# ── MiroFlow (Cientistas via Ollama) ───────────────────────────────────────────
miroflow:
build:
context: .
dockerfile: Dockerfile.miroflow
restart: always
environment:
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
MIROFLOW_PORT: 8006
MIROFLOW_RESEARCHER_MODEL: ${MIROFLOW_RESEARCHER_MODEL:-llama3.1:8b}
depends_on:
ollama:
condition: service_started
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.miroflow.entrypoints=https"
- "traefik.http.routers.miroflow.rule=Host(`${MIROFLOW_DOMAIN:-miroflow.onboardbi.com.br}`)"
- "traefik.http.routers.miroflow.tls=true"
- "traefik.http.routers.miroflow.tls.certresolver=letsencrypt"
- "traefik.http.services.miroflow.loadbalancer.server.port=8006"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8006/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
arcadia-internal:

View File

@ -1,237 +0,0 @@
# ─── Arcádia Suite — Produção (Coolify) ───────────────────────────────────────
# Este arquivo é usado pelo Coolify para deploy automático.
# NÃO inclui volumes de código-fonte — só artefatos de build.
# Configurar no Coolify: Environment Variables para todas as vars ${...}
name: arcadia-prod
services:
# ── Banco de dados com pgvector ─────────────────────────────────────────────
db:
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_DB: ${PGDATABASE:-arcadia}
POSTGRES_USER: ${PGUSER:-arcadia}
POSTGRES_PASSWORD: ${PGPASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- arcadia-internal
# ── Redis ────────────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- arcadia-internal
# ── App principal ─────────────────────────────────────────────────────────
app:
build:
context: .
dockerfile: Dockerfile
restart: always
environment:
NODE_ENV: production
PORT: 5000
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
REDIS_URL: redis://redis:6379
DOCKER_MODE: "true"
CONTABIL_PYTHON_URL: http://contabil:8003
BI_PYTHON_URL: http://bi:8004
AUTOMATION_PYTHON_URL: http://automation:8005
FISCO_PYTHON_URL: http://fisco:8002
PYTHON_SERVICE_URL: http://embeddings:8001
SESSION_SECRET: ${SESSION_SECRET}
SSO_SECRET: ${SSO_SECRET}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_BASE_URL: http://litellm:4000
LITELLM_API_KEY: ${LITELLM_API_KEY}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
# ── Manus Agent — aponta para LiteLLM como gateway unificado ──────────
# LiteLLM roteia para Ollama (local), LLMFit (fine-tuned) ou externo
AI_INTEGRATIONS_OPENAI_BASE_URL: http://litellm:4000/v1
AI_INTEGRATIONS_OPENAI_API_KEY: ${LITELLM_API_KEY}
ports:
- "5000:5000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- arcadia-internal
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.arcadia.tls=true"
- "traefik.http.routers.arcadia.tls.certresolver=letsencrypt"
# ── Microserviços Python ─────────────────────────────────────────────────────
contabil:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: contabil
SERVICE_PORT: 8003
CONTABIL_PORT: 8003
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
bi:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: bi
SERVICE_PORT: 8004
BI_PORT: 8004
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
automation:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: automation
SERVICE_PORT: 8005
AUTOMATION_PORT: 8005
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
fisco:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: fisco
SERVICE_PORT: 8002
FISCO_PORT: 8002
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
embeddings:
build:
context: .
dockerfile: Dockerfile.python
restart: always
environment:
SERVICE_NAME: embeddings
SERVICE_PORT: 8001
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
litellm:
image: ghcr.io/berriai/litellm:main-latest
restart: always
volumes:
- ./docker/litellm-config.yaml:/app/config.yaml
command: ["--config", "/app/config.yaml", "--port", "4000"]
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LITELLM_MASTER_KEY: ${LITELLM_API_KEY}
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
# Ollama: se instalado no host use http://host-gateway:11434
# Se usar container Docker, mantém http://ollama:11434
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
# LLMFit: URL do serviço de modelos fine-tuned
LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-}
# Providers externos opcionais (soberania: só habilitados se configurados)
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
GROQ_API_KEY: ${GROQ_API_KEY:-}
depends_on:
db:
condition: service_healthy
networks:
- arcadia-internal
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
# OPÇÃO A (padrão): Ollama como container Docker
# OPÇÃO B: Ollama no host → comente este serviço e defina
# OLLAMA_BASE_URL=http://host-gateway:11434 nas env vars
ollama:
image: ollama/ollama:latest
restart: always
volumes:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
# Remova 'profiles: [ai]' para ativar por padrão no deploy
profiles: [ai]
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
open-webui:
image: ghcr.io/open-webui/open-webui:main
restart: always
environment:
# Pode apontar para LiteLLM para ter acesso a todos os modelos via WebUI
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
OPENAI_API_BASE_URL: http://litellm:4000/v1
OPENAI_API_KEY: ${LITELLM_API_KEY}
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
volumes:
- open_webui_data:/app/backend/data
depends_on:
- litellm
networks:
- arcadia-internal
- arcadia-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)"
- "traefik.http.routers.webui.tls=true"
- "traefik.http.routers.webui.tls.certresolver=letsencrypt"
- "traefik.http.services.webui.loadbalancer.server.port=8080"
profiles: [ai]
networks:
arcadia-internal:
driver: bridge
arcadia-public:
driver: bridge
volumes:
pgdata:
redis_data:
ollama_models:
open_webui_data:

62
docker-compose.shared.yml Normal file
View File

@ -0,0 +1,62 @@
# ─── Arcádia — Infraestrutura Compartilhada ─────────────────────────────────
# Serviços base: db, redis, ollama
# Usado por: superset, miroflow, app, etc.
name: arcadia-shared
services:
# ── Banco de dados com pgvector ─────────────────────────────────────────────
db:
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_DB: ${PGDATABASE:-arcadia}
POSTGRES_USER: ${PGUSER:-arcadia}
POSTGRES_PASSWORD: ${PGPASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init-pgvector.sql:/docker-entrypoint-initdb.d/01-pgvector.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-arcadia}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- arcadia-internal
# ── Redis ────────────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- arcadia-internal
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
ollama:
image: ollama/ollama:latest
restart: always
volumes:
- ollama_models:/root/.ollama
networks:
- arcadia-internal
- coolify
networks:
arcadia-internal:
driver: bridge
name: arcadia-internal
coolify:
external: true
volumes:
pgdata:
external: true
name: arcadia-prod_pgdata
redis_data:
external: true
name: arcadia-prod_redis_data
ollama_models:

View File

@ -0,0 +1,49 @@
# ─── Superset BI — Aplicação Coolify ───────────────────────────────────────
# Conecta aos serviços compartilhados (db, redis) via network arcadia-internal
# Requer: docker-compose.shared.yml rodando
name: arcadia-superset
services:
# ── Superset BI ───────────────────────────────────────────────────────────────
superset:
image: arcadia-prod-superset:latest
restart: always
environment:
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY}
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset
PYTHONPATH: /app/pythonpath
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia}
command: ["/bin/bash", "/app/docker/init.sh"]
volumes:
- ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./docker/superset/init.sh:/app/docker/init.sh:ro
- superset_home:/app/superset_home
networks:
- arcadia-internal
- coolify
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
- "traefik.http.routers.superset.entrypoints=https"
- "traefik.http.routers.superset.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
- "traefik.http.routers.superset.tls=true"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088"
networks:
arcadia-internal:
external: true
name: arcadia-internal
coolify:
external: true
volumes:
superset_home:
external: true
name: arcadia-prod_superset_home

View File

@ -3,29 +3,15 @@
#
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ TIER 1 (soberania total): LLMFit — modelos fine-tuned locais │
# │ TIER 2 (soberania total): Ollama — modelos open source no servidor │
# │ TIER 3 (opt-in): Providers externos — só com configuração explícita │
# │ TIER 1 (soberania total): Ollama — modelos open source no servidor │
# │ TIER 2 (opt-in): Providers externos — só com configuração explícita │
# └─────────────────────────────────────────────────────────────────────────┘
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
# Nunca chamam APIs externas diretamente.
model_list:
# ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ────────────
- model_name: arcadia-finetuned
litellm_params:
model: openai/llama3.2:3b
api_base: os.environ/LLMFIT_BASE_URL
api_key: os.environ/LLMFIT_API_KEY
- model_name: arcadia-embed
litellm_params:
model: openai/nomic-embed-text:latest
api_base: os.environ/LLMFIT_BASE_URL
api_key: os.environ/LLMFIT_API_KEY
# ── TIER 2: Ollama (LLMs locais — soberania total) ───────────────────────────
# ── TIER 1: Ollama (LLMs locais — soberania total) ───────────────────────────
- model_name: llama3.2
litellm_params:
model: ollama/llama3.2:3b
@ -42,7 +28,7 @@ model_list:
model: ollama/nomic-embed-text
api_base: os.environ/OLLAMA_BASE_URL
# ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
# ── TIER 2: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
@ -53,34 +39,30 @@ model_list:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
# ── TIER 3: Anthropic (opt-in — descomente para habilitar) ───────────────────
# ── TIER 2: Anthropic (opt-in — descomente para habilitar) ───────────────────
# - model_name: claude-sonnet
# litellm_params:
# model: anthropic/claude-sonnet-4-6
# api_key: os.environ/ANTHROPIC_API_KEY
# ── TIER 3: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
# ── TIER 2: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
# - model_name: groq-llama
# litellm_params:
# model: groq/llama-3.3-70b-versatile
# api_key: os.environ/GROQ_API_KEY
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
# Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
- model_name: arcadia-default
litellm_params:
model: openai/llama3.2:3b
api_base: os.environ/LLMFIT_BASE_URL
api_key: os.environ/LLMFIT_API_KEY
model: ollama/llama3.2:3b
api_base: os.environ/OLLAMA_BASE_URL
router_settings:
routing_strategy: least-busy
fallbacks:
fallbacks:
- {"gpt-4o": ["llama3.2"]}
- {"gpt-4o-mini": ["llama3.2"]}
- {"arcadia-default": ["llama3.2"]}
- {"arcadia-finetuned": ["llama3.2"]}
- {"arcadia-embed": ["nomic-embed-text"]}
litellm_settings:
drop_params: true

View File

@ -3,20 +3,21 @@
# Executado ao iniciar o container
set -e
SUPERSET_ADMIN_USER="${SUPERSET_ADMIN_USER:-admin}"
SUPERSET_ADMIN_USER="${SUPERSET_ADMIN_USERNAME:-${SUPERSET_ADMIN_USER:-admin}}"
SUPERSET_ADMIN_EMAIL="${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}"
SUPERSET_ADMIN_PASSWORD="${SUPERSET_ADMIN_PASSWORD:-arcadia2026}"
ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/arcadia}"
echo "[Superset Init] Aguardando PostgreSQL..."
until python -c "
cat > /tmp/pgcheck.py << 'EOF'
import psycopg2, os, sys
url = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://arcadia:arcadia123@db:5432/superset')
try:
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:arcadia123@db:5432/arcadia_superset')
psycopg2.connect(url)
sys.exit(0)
except: sys.exit(1)
" 2>/dev/null; do
except Exception:
sys.exit(1)
EOF
until python /tmp/pgcheck.py 2>/dev/null; do
sleep 2
done
echo "[Superset Init] PostgreSQL disponível!"

View File

@ -9,8 +9,8 @@ SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "change-in-production-use-ope
# ── Banco de metadados do Superset ────────────────────────────────────────────
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL",
"postgresql://arcadia:arcadia123@db:5432/arcadia_superset"
"SQLALCHEMY_DATABASE_URI",
os.environ.get("DATABASE_URL", "postgresql://arcadia:arcadia123@db:5432/superset")
)
# ── CORS — permite o gateway Arcádia (:5000) chamar a API ────────────────────

View File

@ -0,0 +1,39 @@
-- MiroFlow Generated Dashboards
-- Rastreia dashboards criados pelo MiroFlow baseado em análises
CREATE TABLE IF NOT EXISTS miroflow_generated_dashboards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Análise que criou este dashboard
analysis_id VARCHAR(255),
agent VARCHAR(50) NOT NULL CHECK (agent IN ('statistician', 'fiscal_auditor', 'researcher')),
task TEXT NOT NULL,
insights JSONB,
-- Dashboard no Superset
dashboard_id INT NOT NULL,
dashboard_title VARCHAR(255) NOT NULL,
dataset_name VARCHAR(255),
-- SQL sugerido/criado
sql_query TEXT,
-- Auditoria
created_by UUID,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id INT,
-- Referência
superset_url VARCHAR(512),
-- Soft delete
deleted_at TIMESTAMP,
-- Constraints
CONSTRAINT unique_analysis_dashboard UNIQUE (analysis_id, dashboard_id) DEFERRABLE INITIALLY DEFERRED
);
CREATE INDEX idx_miroflow_dashboards_analysis ON miroflow_generated_dashboards(analysis_id);
CREATE INDEX idx_miroflow_dashboards_created_at ON miroflow_generated_dashboards(created_at DESC);
CREATE INDEX idx_miroflow_dashboards_dashboard_id ON miroflow_generated_dashboards(dashboard_id);
CREATE INDEX idx_miroflow_dashboards_tenant ON miroflow_generated_dashboards(tenant_id);

View File

@ -55,7 +55,7 @@ async function buildAll() {
define: {
"process.env.NODE_ENV": '"production"',
},
minify: true,
minify: false,
external: externals,
logLevel: "info",
});

View File

@ -30,12 +30,14 @@ async function comparePasswords(supplied: string, stored: string) {
return timingSafeEqual(hashedBuf, suppliedBuf);
}
if (!process.env.SESSION_SECRET) {
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
}
// CORREÇÃO: Session secret seguro - obrigatório em produção
const SESSION_SECRET = process.env.SESSION_SECRET ||
(process.env.NODE_ENV === 'production'
? (() => { throw new Error('SESSION_SECRET obrigatório em produção'); })()
: randomBytes(32).toString('hex'));
const sessionSettings: session.SessionOptions = {
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: storage.sessionStore,

View File

@ -6,9 +6,26 @@ import ws from "ws";
neonConfig.webSocketConstructor = ws;
const app = express();
const PORT = 8006;
const PORT = process.env.PORT || 8006;
app.use(cors());
// CORREÇÃO: CORS restrito - whitelist de origens permitidas
const allowedOrigins = [
'http://localhost:5000',
'https://localhost:5000',
process.env.FRONTEND_URL,
process.env.DOMAIN ? `https://${process.env.DOMAIN}` : null,
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS não permitido'));
}
},
credentials: true
}));
app.use(express.json());
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

View File

@ -0,0 +1,39 @@
# MiroFlow /analyze Endpoint
## Como funciona
O botão "Analisar" no BiWorkspace (tab Científico) chama `POST /api/miroflow/analyze`.
O handler vive em `engine-proxy.ts` e chama diretamente o Ollama com o model `deepseek-r1:14b` (fallback: `llama3.2:3b`).
## Fluxo
```
BiWorkspace → POST /api/miroflow/analyze
→ engine-proxy.ts (Node, host)
→ ollama-ia1upsekrad96at5hq97e4qa:11434/api/chat
→ deepseek-r1:14b
← { result, model, execution_id, duration_ms }
← resposta ao frontend
```
## Agentes disponíveis
| Agent | System prompt |
|-------|--------------|
| `statistician` | Análise estatística e descritiva |
| `fiscal_auditor` | Compliance tributário brasileiro |
| `researcher` | Correlações e inteligência de negócios |
## Infraestrutura
- O container `ollama-ia1upsekrad96at5hq97e4qa` precisa estar na rede `arcadia-prod_arcadia-internal`.
- Comando para reconectar se necessário:
```bash
docker network connect arcadia-prod_arcadia-internal ollama-ia1upsekrad96at5hq97e4qa
```
- Tempo médio de resposta: ~2-3 min (deepseek-r1:14b).
## Variável de ambiente
`OLLAMA_BASE_URL` sobrescreve o hostname padrão do Ollama.

View File

@ -1,12 +1,69 @@
import type { Express, Request, Response } from "express";
import crypto from "crypto";
import { createNode } from "../graph/service";
import { randomUUID } from "node:crypto";
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
const MIROFLOW_HEALTH_TIMEOUT = 5_000; // 5 segundos para health check
const MIROFLOW_TIMEOUT = 600_000; // 10 minutos
const MIROFLOW_HEALTH_TIMEOUT = 5_000;
const OLLAMA_URL = process.env.OLLAMA_BASE_URL || "http://ollama-ia1upsekrad96at5hq97e4qa:11434";
const AGENT_CONFIGS: Record<string, { model: string; system: string }> = {
statistician: {
model: "deepseek-r1:14b",
system:
"Você é um estatístico especializado em análise de dados empresariais. " +
"Responda em português brasileiro. Forneça análises quantitativas claras, " +
"com estatísticas descritivas, padrões e insights acionáveis. Seja preciso e objetivo.",
},
fiscal_auditor: {
model: "deepseek-r1:14b",
system:
"Você é um auditor fiscal especializado em compliance tributário brasileiro. " +
"Responda em português brasileiro. Identifique inconsistências, riscos fiscais " +
"e recomende ações corretivas. Cite legislação quando relevante.",
},
researcher: {
model: "deepseek-r1:14b",
system:
"Você é um pesquisador analítico especializado em inteligência de negócios. " +
"Responda em português brasileiro. Identifique correlações, tendências e " +
"hipóteses baseadas em dados. Apresente achados de forma estruturada.",
},
};
const FALLBACK_MODEL = "llama3.2:3b";
async function callLLM(model: string, system: string, prompt: string): Promise<string> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 300_000);
try {
const resp = await fetch(`${OLLAMA_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages: [
{ role: "system", content: system },
{ role: "user", content: prompt },
],
stream: false,
}),
signal: controller.signal,
});
clearTimeout(timeout);
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: resp.statusText }));
throw new Error(err?.error ?? `Ollama error: ${resp.status}`);
}
const data: any = await resp.json();
return data.message?.content ?? "";
} catch (err: any) {
clearTimeout(timeout);
throw err;
}
}
async function proxyToMiroFlow(path: string, method: string = "GET", body?: object): Promise<any> {
const timeoutMs = path === "/health" ? MIROFLOW_HEALTH_TIMEOUT : MIROFLOW_TIMEOUT;
@ -27,41 +84,11 @@ async function proxyToMiroFlow(path: string, method: string = "GET", body?: obje
return await response.json();
} catch (err: any) {
clearTimeout(timeout);
if (err.name === "AbortError") throw new Error("MiroFlow timeout (300s)");
if (err.name === "AbortError") throw new Error("MiroFlow timeout (600s)");
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 {
@ -73,19 +100,50 @@ export function registerMiroFlowRoutes(app: Express): void {
});
app.post("/api/miroflow/analyze", async (req: Request, res: Response) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Não autenticado" });
}
console.log("[MiroFlow] POST /api/miroflow/analyze - começando");
try {
const inputBody = {
...req.body,
tenant_id: (req.user as any)?.tenantId ?? null,
const { agent = "statistician", task, context } = req.body as {
agent?: string;
task: string;
context?: Record<string, unknown>;
};
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);
if (!task?.trim()) {
return res.status(400).json({ error: "Campo 'task' é obrigatório" });
}
const agentKey = AGENT_CONFIGS[agent] ? agent : "statistician";
const cfg = AGENT_CONFIGS[agentKey];
let prompt = task;
if (context && Object.keys(context).length > 0) {
const ctxStr = Object.entries(context)
.filter(([, v]) => v != null)
.map(([k, v]) => `${k}: ${v}`)
.join("\n");
if (ctxStr) prompt = `Contexto disponível:\n${ctxStr}\n\nTarefa: ${task}`;
}
const start = Date.now();
let model = cfg.model;
let result: string;
try {
result = await callLLM(model, cfg.system, prompt);
} catch {
model = FALLBACK_MODEL;
result = await callLLM(model, cfg.system, prompt);
}
res.json({
execution_id: randomUUID(),
agent: agentKey,
model,
result,
duration_ms: Date.now() - start,
});
} catch (err: any) {
console.error("[MiroFlow] erro:", err.message);
res.status(502).json({ error: err.message });
}
});

@ -1 +1 @@
Subproject commit 9d180f1eeb5e301c440ad7ce8b12c14222779040
Subproject commit fe1a7397fbb5a3eee145feb2fac94f620ef43f3e

View File

@ -182,11 +182,22 @@ export async function registerRoutes(
// Arcádia Plus - SSO routes (proxy already registered at top)
app.use("/api/plus/sso", plusSsoRoutes);
// CORREÇÃO: /api/tenants protegido - apenas admin vê todos
app.get("/api/tenants", async (req: any, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication required" });
}
try {
// Se não for admin, retornar apenas o tenant do usuário
if (req.user?.role !== 'admin') {
if (req.user?.tenantId) {
const tenant = await storage.getTenant(req.user.tenantId);
return res.json(tenant ? [tenant] : []);
}
return res.status(403).json({ error: "Access denied" });
}
// Admin vê todos
const tenants = await storage.getTenants();
res.json(tenants);
} catch (error) {

View File

@ -15,7 +15,7 @@ export interface IStorage {
sessionStore: session.Store;
getUser(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getUserByUsername(username: string, tenantId?: number): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
getEnrichedUser(user: User): Promise<any>;
@ -28,6 +28,7 @@ export interface IStorage {
getUserApplications(userId: string): Promise<Application[]>;
assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
removeApplicationFromUser(userId: string, applicationId: string): Promise<void>;
getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined>;
getTenants(): Promise<{ id: number; name: string; slug: string }[]>;
}
@ -46,8 +47,21 @@ export class DatabaseStorage implements IStorage {
return user;
}
async getUserByUsername(username: string): Promise<User | undefined> {
// CORREÇÃO: getUserByUsername com validação de tenant
async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.username, username));
// Se tenantId foi passado, validar que usuário pertence a esse tenant
if (tenantId && user) {
const [tenantUser] = await db.select()
.from(tenantUsers)
.where(and(
eq(tenantUsers.userId, user.id),
eq(tenantUsers.tenantId, tenantId)
));
if (!tenantUser) return undefined;
}
return user;
}
@ -146,6 +160,19 @@ export class DatabaseStorage implements IStorage {
return enriched;
}
// CORREÇÃO: getTenant para buscar tenant específico
async getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined> {
const [tenant] = await db.select({
id: tenants.id,
name: tenants.name,
slug: tenants.slug,
tenantType: tenants.tenantType,
plan: tenants.plan,
status: tenants.status
}).from(tenants).where(eq(tenants.id, id));
return tenant;
}
async getTenants(): Promise<{ id: number; name: string; slug: string }[]> {
const result = await db.select({
id: tenants.id,

View File

@ -0,0 +1,251 @@
/**
* MiroFlow Bridge para Apache Superset
*
* Adaptador que conecta Superset com MiroFlow para análises científicas.
* Fluxo: SQL (Superset) MiroFlow (análise) Enriquecimento de dados KG (provenance)
*/
import type { Request, Response } from "express";
import { createNode } from "../graph/service";
import crypto from "crypto";
interface MiroFlowConfig {
type?: "exploratory" | "forecast" | "anomaly" | "hypothesis" | "causal";
horizon?: number; // dias para forecast
confidence_level?: number; // 0.90, 0.95, 0.99
methods?: string[]; // ["arima", "prophet", "statistical_test", etc]
}
interface SupersetDataset {
data: Record<string, any>[];
columns: string[];
index?: number[];
}
interface MiroFlowResult {
execution_id: string;
agent: string;
model: string;
result: {
summary: string;
analysis_type: string;
descriptive_stats?: Record<string, any>;
anomalies?: Array<{ index: number; score: number; value: any }>;
forecast?: { values: number[]; lower_bound: number[]; upper_bound: number[] };
hypothesis_test?: { p_value: number; result: string };
confidence_intervals?: Record<string, [number, number]>;
};
metadata: {
execution_time_ms: number;
rows_analyzed: number;
columns_used: string[];
};
}
export interface EnrichedDataset extends SupersetDataset {
_miroflow_analysis: string;
_miroflow_type: string;
_confidence_intervals?: Record<string, [number, number]>;
_anomaly_scores?: Record<number, number>;
_forecast?: { values: number[]; lower_bound: number[]; upper_bound: number[] };
_metadata: {
analysis_executed_at: string;
miroflow_execution_id: string;
miroflow_model: string;
execution_time_ms: number;
};
}
/**
* MiroFlowBridge: Proxy entre Superset e MiroFlow
* Enriquece datasets SQL com análises científicas
*/
export class MiroFlowBridge {
private miroflowBaseUrl: string;
private miroflowTimeout: number = 300_000; // 5 min
constructor(miroflowBaseUrl: string = "http://localhost:8006") {
this.miroflowBaseUrl = miroflowBaseUrl;
}
/**
* Analisa dataset via MiroFlow
*/
async analyze(
dataset: SupersetDataset,
config: MiroFlowConfig,
tenantId?: number,
userId?: number
): Promise<MiroFlowResult> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.miroflowTimeout);
try {
const payload = {
dataset: JSON.stringify(dataset),
analysis_type: config.type || "exploratory",
config,
tenant_id: tenantId,
user_id: userId,
source: "superset_bridge",
};
const response = await fetch(`${this.miroflowBaseUrl}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(`MiroFlow error: ${err.detail || response.status}`);
}
return await response.json();
} catch (err: any) {
clearTimeout(timeout);
if (err.name === "AbortError") {
throw new Error("MiroFlow analysis timeout (5 min)");
}
throw err;
}
}
/**
* Enriquece dataset com resultados da análise
*/
enrich(dataset: SupersetDataset, result: MiroFlowResult): EnrichedDataset {
const enriched: EnrichedDataset = {
...dataset,
_miroflow_analysis: result.result.summary,
_miroflow_type: result.result.analysis_type,
_metadata: {
analysis_executed_at: new Date().toISOString(),
miroflow_execution_id: result.execution_id,
miroflow_model: result.model,
execution_time_ms: result.metadata.execution_time_ms,
},
};
// Adicionar intervalos de confiança
if (result.result.confidence_intervals) {
enriched._confidence_intervals = result.result.confidence_intervals;
}
// Adicionar scores de anomalias (mapeados para índices)
if (result.result.anomalies && result.result.anomalies.length > 0) {
enriched._anomaly_scores = {};
result.result.anomalies.forEach((anom) => {
enriched._anomaly_scores![anom.index] = anom.score;
});
}
// Adicionar forecast
if (result.result.forecast) {
enriched._forecast = result.result.forecast;
}
return enriched;
}
/**
* Registra análise no Knowledge Graph para provenance
*/
async registerInKG(
result: MiroFlowResult,
dataset: SupersetDataset,
config: MiroFlowConfig,
req: Request
): Promise<void> {
try {
const analysisHash = crypto
.createHash("sha256")
.update(
JSON.stringify({
execution_id: result.execution_id,
dataset_columns: dataset.columns,
config,
result: result.result,
})
)
.digest("hex");
await createNode({
type: "miroflow_analysis",
tenantId: (req.user as any)?.tenantId ?? null,
data: {
execution_id: result.execution_id,
agent: result.agent,
model: result.model,
analysis_type: result.result.analysis_type,
dataset_shape: `${dataset.data.length}x${dataset.columns.length}`,
columns_analyzed: dataset.columns,
config,
summary: result.result.summary,
execution_time_ms: result.metadata.execution_time_ms,
analysisHash,
immutable: true,
source: "superset_bridge",
registeredAt: new Date().toISOString(),
},
});
} catch (kgErr: any) {
console.error("[MiroFlow Bridge] Falha ao registrar análise no KG:", kgErr.message);
// Não bloqueia a resposta — logging apenas
}
}
/**
* Handler Express para POST /api/superset/miroflow/analyze
* Superset envia dataset SQL-resultante e config de análise
*/
async handleAnalyzeRequest(req: Request, res: Response): Promise<void> {
if (!req.isAuthenticated()) {
res.status(401).json({ error: "Não autenticado" });
return;
}
try {
const { dataset, config }: { dataset: SupersetDataset; config: MiroFlowConfig } =
req.body;
if (!dataset || !dataset.data || !dataset.columns) {
res.status(400).json({ error: "Dataset inválido: data e columns obrigatórios" });
return;
}
// Executar análise no MiroFlow
const result = await this.analyze(dataset, config, (req.user as any)?.tenantId, (req.user as any)?.id);
// Enriquecer dataset
const enriched = this.enrich(dataset, result);
// Registrar no KG (assíncrono, não bloqueia)
this.registerInKG(result, dataset, config, req).catch(() => {});
// Retornar dados enriquecidos
res.json({
success: true,
data: enriched,
analysis: result.result,
execution_id: result.execution_id,
});
} catch (err: any) {
console.error("[MiroFlow Bridge] Erro:", err.message);
res.status(502).json({
error: err.message,
source: "miroflow_bridge",
});
}
}
}
/**
* Factory para criar instância do bridge
*/
export function createMiroFlowBridge(miroflowUrl?: string): MiroFlowBridge {
return new MiroFlowBridge(miroflowUrl || process.env.MIROFLOW_URL || "http://localhost:8006");
}

View File

@ -1,4 +1,5 @@
import type { Express, Request, Response } from "express";
import { createMiroFlowBridge } from "./MiroFlowBridge.js";
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
@ -6,6 +7,8 @@ const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
const miroflowBridge = createMiroFlowBridge();
// Cache do service token (válido por 50 min)
let cachedToken: string | null = null;
let tokenExpiry = 0;
@ -105,4 +108,183 @@ export function registerSupersetRoutes(app: Express): void {
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
res.redirect("/superset/login");
});
// ============================================================================
// MiroFlow Bridge Routes
// ============================================================================
/**
* POST /api/superset/miroflow/analyze
* Enriquece dataset Superset com análises científicas via MiroFlow
*
* Request body:
* {
* "dataset": { "data": [...], "columns": [...] },
* "config": { "type": "forecast|anomaly|exploratory", "horizon": 30, "confidence_level": 0.95 }
* }
*
* Response:
* {
* "success": true,
* "data": { ...enriched dataset with _miroflow_analysis, _anomaly_scores, etc },
* "analysis": { "summary": "...", "analysis_type": "..." },
* "execution_id": "uuid"
* }
*/
app.post("/api/superset/miroflow/analyze", async (req: Request, res: Response) => {
await miroflowBridge.handleAnalyzeRequest(req, res);
});
/**
* GET /api/superset/miroflow/health
* Verifica conexão com MiroFlow bridge
*/
app.get("/api/superset/miroflow/health", async (_req: Request, res: Response) => {
try {
const miroflowUrl = process.env.MIROFLOW_URL || "http://localhost:8006";
const response = await fetch(`${miroflowUrl}/health`, {
signal: AbortSignal.timeout(5_000),
});
const data = await response.json();
res.json({
online: response.ok,
miroflow: data,
bridge: "operational",
endpoints: {
analyze: "/api/superset/miroflow/analyze",
health: "/api/superset/miroflow/health",
},
});
} catch (err: any) {
res.json({
online: false,
error: err.message,
bridge: "offline",
});
}
});
/**
* POST /api/superset/miroflow/create-dashboard
* Cria um dashboard no Superset baseado em análise MiroFlow
*/
app.post("/api/superset/miroflow/create-dashboard", async (req: Request, res: Response) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Não autenticado" });
}
try {
const { dashboardTitle, sqlQuery, analysisId, agent, task, insights } = req.body;
if (!dashboardTitle || !sqlQuery || !analysisId) {
return res.status(400).json({ error: "dashboardTitle, sqlQuery e analysisId são obrigatórios" });
}
const token = await getServiceToken();
const user = req.user as any;
// 1. Criar dataset no Superset
const datasetResp = await fetch(`${SUPERSET_URL}/api/v1/datasets/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
database: 1, // ID do banco "Arcádia Suite"
table_name: `miroflow_${analysisId.substring(0, 8)}`,
sql: sqlQuery,
}),
});
if (!datasetResp.ok) {
const err = await datasetResp.json().catch(() => ({ detail: "Erro ao criar dataset" }));
throw new Error(`Falha ao criar dataset: ${err.detail}`);
}
const dataset = await datasetResp.json();
const datasetId = dataset.id;
// 2. Criar chart automático
const chartResp = await fetch(`${SUPERSET_URL}/api/v1/chart/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
datasource_id: datasetId,
datasource_type: "table",
chart_type: "table", // Começar com table, depois adicionar mais tipos
name: `${dashboardTitle} - Dados`,
description: `Criado via MiroFlow Analysis (${agent})`,
}),
});
if (!chartResp.ok) {
const err = await chartResp.json().catch(() => ({ detail: "Erro ao criar chart" }));
throw new Error(`Falha ao criar chart: ${err.detail}`);
}
const chart = await chartResp.json();
const chartId = chart.id;
// 3. Criar dashboard
const dashboardResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
dashboard_title: dashboardTitle,
description: `Análise MiroFlow - ${agent} - ${new Date().toLocaleString('pt-BR')}`,
}),
});
if (!dashboardResp.ok) {
const err = await dashboardResp.json().catch(() => ({ detail: "Erro ao criar dashboard" }));
throw new Error(`Falha ao criar dashboard: ${err.detail}`);
}
const dashboard = await dashboardResp.json();
const dashboardId = dashboard.id;
// 4. Adicionar chart ao dashboard
const updateResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
dashboard_title: dashboardTitle,
slices: [chartId],
}),
});
if (!updateResp.ok) {
throw new Error("Falha ao adicionar chart ao dashboard");
}
// 5. Salvar referência no banco Arcádia (TODO: implementar com prepared statements)
// Por enquanto apenas retorna dashboard criado
console.log(`[Superset] Dashboard criado: ID=${dashboardId}, título="${dashboardTitle}"`);
res.json({
success: true,
dashboardId,
dashboardUrl: `${SUPERSET_URL}/superset/dashboard/${dashboardId}/`,
message: `Dashboard "${dashboardTitle}" criado com sucesso!`,
});
} catch (err: any) {
console.error("[Superset] Erro ao criar dashboard:", err.message);
res.status(502).json({
error: err.message,
source: "superset_create_dashboard",
});
}
});
console.log("[Superset] MiroFlow Bridge registered at /api/superset/miroflow");
}