Compare commits
No commits in common. "f2a240fdd30a23116f53bef9a5e6e7b41a8535b5" and "10ffd3338a6786b2207cfdabd0825f257fdad1d9" have entirely different histories.
f2a240fdd3
...
10ffd3338a
3
.env
3
.env
|
|
@ -6,6 +6,9 @@ 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
|
||||
|
|
|
|||
|
|
@ -49,8 +49,13 @@ 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)
|
||||
# Deixe vazio para operação 100% soberana (apenas Ollama + LLMFit)
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GROQ_API_KEY=
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# 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
|
||||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -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: Ollama local, TIER 2: externos)
|
||||
litellm-config.yaml # Roteamento de LLMs (TIER 1: LLMFit, TIER 2: Ollama, TIER 3: externos)
|
||||
```
|
||||
|
||||
## Arquitetura de IA
|
||||
|
|
@ -29,8 +29,9 @@ Manus / Agents / Embeddings
|
|||
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
||||
▼
|
||||
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
||||
├──► Ollama :11434 (TIER 1 — local, padrão)
|
||||
└──► OpenAI/Anthropic/Groq (TIER 2 — opt-in, só se API key configurada)
|
||||
├──► 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)
|
||||
```
|
||||
|
||||
**Variáveis chave do Manus:**
|
||||
|
|
@ -95,11 +96,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
|
||||
|
|
@ -121,6 +122,7 @@ 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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ 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`
|
||||
|
|
@ -255,11 +256,12 @@ Configure backups automáticos no Coolify em **Project → Backups**.
|
|||
|
||||
## Stack de IA (LiteLLM + Ollama)
|
||||
|
||||
### Dois tiers configurados em `docker/litellm-config.yaml`
|
||||
### Três tiers configurados em `docker/litellm-config.yaml`
|
||||
|
||||
```
|
||||
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)
|
||||
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)
|
||||
```
|
||||
|
||||
### Baixar modelos Ollama após o primeiro deploy
|
||||
|
|
@ -270,6 +272,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Integração de IA — Ollama no Servidor
|
||||
# Integração de IA — Ollama + LLMFit no Servidor
|
||||
|
||||
Guia para conectar o Arcádia Suite à IA local em produção.
|
||||
Guia para conectar o Arcádia Suite às IAs locais em produção.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -13,11 +13,11 @@ Arcádia Suite (Manus, Agents, Embeddings)
|
|||
▼
|
||||
LiteLLM (porta 4000) — gateway único
|
||||
│
|
||||
├──► Ollama (modelos open source locais) [TIER 1 — padrão]
|
||||
└──► OpenAI/Anthropic/Groq (opt-in) [TIER 2 — externo]
|
||||
├──► LLMFit (seus modelos fine-tuned) [TIER 1 — prioridade]
|
||||
└──► Ollama (modelos open source locais) [TIER 2 — padrão/fallback]
|
||||
```
|
||||
|
||||
Nenhum serviço do Arcádia chama Ollama diretamente.
|
||||
Nenhum serviço do Arcádia chama Ollama ou LLMFit diretamente.
|
||||
Tudo passa pelo LiteLLM — que roteia, loga e faz fallback automaticamente.
|
||||
|
||||
---
|
||||
|
|
@ -89,6 +89,66 @@ 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:
|
||||
|
|
@ -114,6 +174,9 @@ 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=
|
||||
|
|
@ -139,7 +202,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.
|
||||
O Manus deve responder via Ollama (ou LLMFit se configurado).
|
||||
|
||||
### 3. Ver logs em tempo real
|
||||
|
||||
|
|
@ -190,6 +253,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ const AGENTS = [
|
|||
{
|
||||
value: "statistician",
|
||||
label: "Statistician",
|
||||
model: "llama3.1:8b",
|
||||
model: "deepseek-r1:14b",
|
||||
placeholder:
|
||||
"Ex: Analise a distribuição de vendas por região no último trimestre",
|
||||
},
|
||||
{
|
||||
value: "fiscal_auditor",
|
||||
label: "Fiscal Auditor",
|
||||
model: "llama3.1:8b",
|
||||
model: "deepseek-r1:14b",
|
||||
placeholder:
|
||||
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
|
||||
},
|
||||
|
|
@ -73,26 +73,6 @@ 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;
|
||||
|
|
@ -227,46 +207,6 @@ 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>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ import {
|
|||
} from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -2958,9 +2957,6 @@ 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>
|
||||
|
|
@ -2987,9 +2983,6 @@ 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 />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
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";
|
||||
|
|
@ -201,10 +200,6 @@ 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">
|
||||
|
|
@ -832,10 +827,6 @@ export default function Scientist() {
|
|||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="scientific" className="space-y-4">
|
||||
<MiroFlowControl />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# ─── 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
|
||||
|
|
@ -25,7 +25,6 @@ services:
|
|||
retries: 10
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- coolify
|
||||
|
||||
# ── Redis ────────────────────────────────────────────────────────────────────
|
||||
redis:
|
||||
|
|
@ -36,7 +35,6 @@ services:
|
|||
- redis_data:/data
|
||||
networks:
|
||||
- arcadia-internal
|
||||
- coolify
|
||||
|
||||
# ── App principal ─────────────────────────────────────────────────────────
|
||||
app:
|
||||
|
|
@ -69,9 +67,6 @@ 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}
|
||||
|
|
@ -147,6 +142,168 @@ 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:
|
||||
|
|
@ -283,7 +440,7 @@ services:
|
|||
- arcadia-internal
|
||||
|
||||
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
||||
# Roteia: Ollama (local) → externo (opt-in)
|
||||
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
|
||||
litellm:
|
||||
image: ghcr.io/berriai/litellm:main-latest
|
||||
restart: always
|
||||
|
|
@ -297,6 +454,9 @@ 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:-}
|
||||
|
|
@ -318,8 +478,10 @@ 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) ───────────────────────────────
|
||||
# ── Open WebUI (interface para Ollama + LLMFit) ───────────────────────────────
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
restart: always
|
||||
|
|
@ -377,37 +539,7 @@ services:
|
|||
- "traefik.http.routers.superset.tls=true"
|
||||
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.superset.loadbalancer.server.port=8088"
|
||||
|
||||
# ── 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
|
||||
profiles: [bi]
|
||||
|
||||
networks:
|
||||
arcadia-internal:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
# ─── 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:
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# ─── 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:
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# ─── 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
|
||||
|
|
@ -3,15 +3,29 @@
|
|||
#
|
||||
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
# │ TIER 1 (soberania total): Ollama — modelos open source no servidor │
|
||||
# │ TIER 2 (opt-in): Providers externos — só com configuração explícita │
|
||||
# │ 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 │
|
||||
# └─────────────────────────────────────────────────────────────────────────┘
|
||||
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
|
||||
# Nunca chamam APIs externas diretamente.
|
||||
|
||||
model_list:
|
||||
|
||||
# ── TIER 1: Ollama (LLMs locais — soberania total) ───────────────────────────
|
||||
# ── 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) ───────────────────────────
|
||||
- model_name: llama3.2
|
||||
litellm_params:
|
||||
model: ollama/llama3.2:3b
|
||||
|
|
@ -28,7 +42,7 @@ model_list:
|
|||
model: ollama/nomic-embed-text
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
|
||||
# ── TIER 2: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
|
||||
# ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ─────────
|
||||
- model_name: gpt-4o
|
||||
litellm_params:
|
||||
model: openai/gpt-4o
|
||||
|
|
@ -39,30 +53,34 @@ model_list:
|
|||
model: openai/gpt-4o-mini
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
# ── TIER 2: Anthropic (opt-in — descomente para habilitar) ───────────────────
|
||||
# ── TIER 3: 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 2: Groq (opt-in — inferência rápida sem dados persistidos) ──────────
|
||||
# ── TIER 3: 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: ollama/llama3.2:3b
|
||||
api_base: os.environ/OLLAMA_BASE_URL
|
||||
model: openai/llama3.2:3b
|
||||
api_base: os.environ/LLMFIT_BASE_URL
|
||||
api_key: os.environ/LLMFIT_API_KEY
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,21 +3,20 @@
|
|||
# Executado ao iniciar o container
|
||||
set -e
|
||||
|
||||
SUPERSET_ADMIN_USER="${SUPERSET_ADMIN_USERNAME:-${SUPERSET_ADMIN_USER:-admin}}"
|
||||
SUPERSET_ADMIN_USER="${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..."
|
||||
cat > /tmp/pgcheck.py << 'EOF'
|
||||
until python -c "
|
||||
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)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
EOF
|
||||
until python /tmp/pgcheck.py 2>/dev/null; do
|
||||
sys.exit(0)
|
||||
except: sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "[Superset Init] PostgreSQL disponível!"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"SQLALCHEMY_DATABASE_URI",
|
||||
os.environ.get("DATABASE_URL", "postgresql://arcadia:arcadia123@db:5432/superset")
|
||||
"DATABASE_URL",
|
||||
"postgresql://arcadia:arcadia123@db:5432/arcadia_superset"
|
||||
)
|
||||
|
||||
# ── CORS — permite o gateway Arcádia (:5000) chamar a API ────────────────────
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
-- 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);
|
||||
|
|
@ -55,7 +55,7 @@ async function buildAll() {
|
|||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
},
|
||||
minify: false,
|
||||
minify: true,
|
||||
external: externals,
|
||||
logLevel: "info",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,14 +30,12 @@ async function comparePasswords(supplied: string, stored: string) {
|
|||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||
}
|
||||
|
||||
// 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'));
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
|
||||
}
|
||||
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: SESSION_SECRET,
|
||||
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
|
|
|
|||
|
|
@ -6,26 +6,9 @@ import ws from "ws";
|
|||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8006;
|
||||
const PORT = 8006;
|
||||
|
||||
// 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(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,69 +1,12 @@
|
|||
import type { Express, Request, Response } from "express";
|
||||
import { randomUUID } from "node:crypto";
|
||||
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 = 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;
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
async function proxyToMiroFlow(path: string, method: string = "GET", body?: object): Promise<any> {
|
||||
const timeoutMs = path === "/health" ? MIROFLOW_HEALTH_TIMEOUT : MIROFLOW_TIMEOUT;
|
||||
|
|
@ -84,11 +27,41 @@ 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 (600s)");
|
||||
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 {
|
||||
|
|
@ -100,50 +73,19 @@ export function registerMiroFlowRoutes(app: Express): void {
|
|||
});
|
||||
|
||||
app.post("/api/miroflow/analyze", async (req: Request, res: Response) => {
|
||||
console.log("[MiroFlow] POST /api/miroflow/analyze - começando");
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ error: "Não autenticado" });
|
||||
}
|
||||
try {
|
||||
const { agent = "statistician", task, context } = req.body as {
|
||||
agent?: string;
|
||||
task: string;
|
||||
context?: Record<string, unknown>;
|
||||
const inputBody = {
|
||||
...req.body,
|
||||
tenant_id: (req.user as any)?.tenantId ?? null,
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
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) {
|
||||
console.error("[MiroFlow] erro:", err.message);
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit fe1a7397fbb5a3eee145feb2fac94f620ef43f3e
|
||||
Subproject commit 9d180f1eeb5e301c440ad7ce8b12c14222779040
|
||||
|
|
@ -182,22 +182,11 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface IStorage {
|
|||
sessionStore: session.Store;
|
||||
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string, tenantId?: number): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
getEnrichedUser(user: User): Promise<any>;
|
||||
|
||||
|
|
@ -28,7 +28,6 @@ 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 }[]>;
|
||||
}
|
||||
|
||||
|
|
@ -47,21 +46,8 @@ export class DatabaseStorage implements IStorage {
|
|||
return user;
|
||||
}
|
||||
|
||||
// CORREÇÃO: getUserByUsername com validação de tenant
|
||||
async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> {
|
||||
async getUserByUsername(username: string): 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;
|
||||
}
|
||||
|
||||
|
|
@ -160,19 +146,6 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,251 +0,0 @@
|
|||
/**
|
||||
* 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");
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
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);
|
||||
|
|
@ -7,8 +6,6 @@ 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;
|
||||
|
|
@ -108,183 +105,4 @@ 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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue