Compare commits
21 Commits
10ffd3338a
...
f2a240fdd3
| Author | SHA1 | Date |
|---|---|---|
|
|
f2a240fdd3 | |
|
|
4fb3f87dc3 | |
|
|
f0b39f7f1d | |
|
|
9d83163dbb | |
|
|
59bfca8c70 | |
|
|
53b9fc5408 | |
|
|
50e419db17 | |
|
|
5c0f3f818e | |
|
|
4778462c29 | |
|
|
35492de95f | |
|
|
6853077fa2 | |
|
|
fa00fbb73f | |
|
|
395d1e5ecb | |
|
|
c665f6ec40 | |
|
|
264029eafc | |
|
|
c8d323941b | |
|
|
25f6e2f72a | |
|
|
c293ec2a61 | |
|
|
9f6875367c | |
|
|
aa0ce768ed | |
|
|
763c6bcdd5 |
3
.env
3
.env
|
|
@ -6,9 +6,6 @@ WEBUI_SECRET_KEY=76c7fc044f9e8d6e5eb60b67c82832b67e84739da4827ce1fa7c101ae1a055d
|
||||||
DOMAIN=suite.onboardbi.com.br
|
DOMAIN=suite.onboardbi.com.br
|
||||||
OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434
|
OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434
|
||||||
|
|
||||||
LLMFIT_BASE_URL=http://llmfit:8000
|
|
||||||
LLMFIT_API_KEY=key-arcadia
|
|
||||||
|
|
||||||
# ── Superset BI ───────────────────────────────────────────────
|
# ── Superset BI ───────────────────────────────────────────────
|
||||||
SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63
|
SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63
|
||||||
SUPERSET_ADMIN_USERNAME=admin
|
SUPERSET_ADMIN_USERNAME=admin
|
||||||
|
|
|
||||||
|
|
@ -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
|
# Se Ollama está em outro servidor: OLLAMA_BASE_URL=http://IP_DO_SERVIDOR:11434
|
||||||
OLLAMA_BASE_URL=http://localhost: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)
|
# ── 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=
|
OPENAI_API_KEY=
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
GROQ_API_KEY=
|
GROQ_API_KEY=
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -20,7 +20,7 @@ server/
|
||||||
client/ # 66 páginas React
|
client/ # 66 páginas React
|
||||||
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
|
shared/schema.ts # Schema do banco (7317 linhas, Drizzle ORM)
|
||||||
docker/
|
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
|
## Arquitetura de IA
|
||||||
|
|
@ -29,9 +29,8 @@ Manus / Agents / Embeddings
|
||||||
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
│ AI_INTEGRATIONS_OPENAI_BASE_URL
|
||||||
▼
|
▼
|
||||||
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
LiteLLM :4000 (gateway unificado, loga tudo no banco)
|
||||||
├──► LLMFit (TIER 1 — fine-tuned, soberano) [slot pronto, comentado]
|
├──► Ollama :11434 (TIER 1 — local, padrão)
|
||||||
├──► Ollama :11434 (TIER 2 — local, padrão)
|
└──► OpenAI/Anthropic/Groq (TIER 2 — opt-in, só se API key configurada)
|
||||||
└──► OpenAI/Anthropic/Groq (TIER 3 — opt-in, só se API key configurada)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Variáveis chave do Manus:**
|
**Variáveis chave do Manus:**
|
||||||
|
|
@ -96,11 +95,11 @@ arcadia-prod-{contabil,bi,automation,fisco,embeddings}-1 Python services
|
||||||
- ✅ Docker dev + prod, LiteLLM gateway
|
- ✅ Docker dev + prod, LiteLLM gateway
|
||||||
|
|
||||||
## O que ainda falta
|
## O que ainda falta
|
||||||
- ❌ LLMFit: slot pronto em `litellm-config.yaml`, só habilitar quando disponível
|
|
||||||
- ❌ Testes automatizados / CI-CD
|
- ❌ Testes automatizados / CI-CD
|
||||||
- ❌ Monitoramento (APM, Sentry, métricas)
|
- ❌ Monitoramento (APM, Sentry, métricas)
|
||||||
- ❌ Multi-tenancy completo
|
- ❌ Multi-tenancy completo
|
||||||
- ❌ Rate limiting em todos os endpoints (parcial)
|
- ❌ Rate limiting em todos os endpoints (parcial)
|
||||||
|
- ❌ Fine-tuning de modelos locais (dados do Arcádia)
|
||||||
|
|
||||||
## Comandos úteis
|
## Comandos úteis
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -122,7 +121,6 @@ npm run build
|
||||||
```
|
```
|
||||||
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
|
SESSION_SECRET, SSO_SECRET # gerar strings seguras em prod
|
||||||
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
|
AI_INTEGRATIONS_OPENAI_BASE_URL # aponta para LiteLLM
|
||||||
LLMFIT_BASE_URL # LLMFit quando disponível
|
|
||||||
OLLAMA_BASE_URL # Ollama host ou container
|
OLLAMA_BASE_URL # Ollama host ou container
|
||||||
OPENAI_API_KEY # opcional (soberania: deixar vazio)
|
OPENAI_API_KEY # opcional (soberania: deixar vazio)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ AUTOMATION_PYTHON_URL=http://automation:8005
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
GROQ_API_KEY=
|
GROQ_API_KEY=
|
||||||
LLMFIT_BASE_URL= # habilitar quando LLMFit estiver disponível
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Superset — profile `bi`
|
### Superset — profile `bi`
|
||||||
|
|
@ -256,12 +255,11 @@ Configure backups automáticos no Coolify em **Project → Backups**.
|
||||||
|
|
||||||
## Stack de IA (LiteLLM + Ollama)
|
## 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 1 — Ollama (local, padrão) → llama3.3, qwen2.5-coder, nomic-embed-text
|
||||||
TIER 2 — 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 3 — Externos (opt-in) → OpenAI, Anthropic, Groq (apenas se API key definida)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Baixar modelos Ollama após o primeiro deploy
|
### 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
|
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
|
## Pós-deploy
|
||||||
|
|
|
||||||
|
|
@ -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
|
LiteLLM (porta 4000) — gateway único
|
||||||
│
|
│
|
||||||
├──► LLMFit (seus modelos fine-tuned) [TIER 1 — prioridade]
|
├──► Ollama (modelos open source locais) [TIER 1 — padrão]
|
||||||
└──► Ollama (modelos open source locais) [TIER 2 — padrão/fallback]
|
└──► 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.
|
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
|
## Variáveis de ambiente — resumo completo
|
||||||
|
|
||||||
Configure todas no Coolify antes do deploy:
|
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 em container: http://ollama:11434
|
||||||
OLLAMA_BASE_URL=http://host-gateway: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) ───────────────────
|
# ── Providers externos (deixar vazio para soberania total) ───────────────────
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
|
|
@ -202,7 +139,7 @@ curl http://localhost:4000/v1/chat/completions \
|
||||||
### 2. Testar Manus via interface
|
### 2. Testar Manus via interface
|
||||||
|
|
||||||
Acesse `https://seudominio.com.br` → abra o Manus → envie uma mensagem simples.
|
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
|
### 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.
|
→ O modelo referenciado em `litellm-config.yaml` não foi baixado no Ollama.
|
||||||
→ Execute `ollama pull NOME_DO_MODELO`
|
→ 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**
|
**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)
|
→ 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
|
→ Ou use o IP real da interface de rede: `ip addr show` para descobrir
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,14 @@ const AGENTS = [
|
||||||
{
|
{
|
||||||
value: "statistician",
|
value: "statistician",
|
||||||
label: "Statistician",
|
label: "Statistician",
|
||||||
model: "deepseek-r1:14b",
|
model: "llama3.1:8b",
|
||||||
placeholder:
|
placeholder:
|
||||||
"Ex: Analise a distribuição de vendas por região no último trimestre",
|
"Ex: Analise a distribuição de vendas por região no último trimestre",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "fiscal_auditor",
|
value: "fiscal_auditor",
|
||||||
label: "Fiscal Auditor",
|
label: "Fiscal Auditor",
|
||||||
model: "deepseek-r1:14b",
|
model: "llama3.1:8b",
|
||||||
placeholder:
|
placeholder:
|
||||||
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
|
"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 = () => {
|
const handleAnalyze = () => {
|
||||||
if (!task.trim()) {
|
if (!task.trim()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -207,6 +227,46 @@ export function MiroFlowControl({
|
||||||
{mutation.data?.execution_id?.slice(0, 8)}...
|
{mutation.data?.execution_id?.slice(0, 8)}...
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
import { SupersetDashboard } from "@/components/SupersetDashboard";
|
||||||
|
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
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">
|
<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
|
<Layers className="w-4 h-4 mr-2" /> Staging
|
||||||
</TabsTrigger>
|
</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">
|
<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
|
<Settings className="w-4 h-4 mr-2" /> Insights
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
@ -2983,6 +2987,9 @@ export default function BiWorkspace() {
|
||||||
<TabsContent value="staging" className="mt-0">
|
<TabsContent value="staging" className="mt-0">
|
||||||
<StagingTab />
|
<StagingTab />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="cientifico" className="mt-0">
|
||||||
|
<MiroFlowControl />
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="advanced" className="mt-0">
|
<TabsContent value="advanced" className="mt-0">
|
||||||
<SupersetAdvancedTab />
|
<SupersetAdvancedTab />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
|
||||||
|
import { MiroFlowControl } from "@/components/MiroFlowControl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -200,6 +201,10 @@ export default function Scientist() {
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
Sugestões
|
Sugestões
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="scientific" className="data-[state=active]:bg-cyan-500">
|
||||||
|
<Brain className="w-4 h-4 mr-2" />
|
||||||
|
Científico
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="knowledge" className="space-y-4">
|
<TabsContent value="knowledge" className="space-y-4">
|
||||||
|
|
@ -827,6 +832,10 @@ export default function Scientist() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="scientific" className="space-y-4">
|
||||||
|
<MiroFlowControl />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -25,6 +25,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
|
- coolify
|
||||||
|
|
||||||
# ── Redis ────────────────────────────────────────────────────────────────────
|
# ── Redis ────────────────────────────────────────────────────────────────────
|
||||||
redis:
|
redis:
|
||||||
|
|
@ -35,6 +36,7 @@ services:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
|
- coolify
|
||||||
|
|
||||||
# ── App principal ─────────────────────────────────────────────────────────
|
# ── App principal ─────────────────────────────────────────────────────────
|
||||||
app:
|
app:
|
||||||
|
|
@ -67,6 +69,9 @@ services:
|
||||||
PLUS_PORT: "8080"
|
PLUS_PORT: "8080"
|
||||||
PLUS_AUTO_START: "false" # container separado em prod
|
PLUS_AUTO_START: "false" # container separado em prod
|
||||||
SSO_PLUS_BASE_URL: http://plus:8080
|
SSO_PLUS_BASE_URL: http://plus:8080
|
||||||
|
# ── MiroFlow ──────────────────────────────────────────────────────────
|
||||||
|
MIROFLOW_HOST: miroflow
|
||||||
|
MIROFLOW_PORT: "8006"
|
||||||
# ── Apache Superset ───────────────────────────────────────────────────
|
# ── Apache Superset ───────────────────────────────────────────────────
|
||||||
SUPERSET_HOST: superset
|
SUPERSET_HOST: superset
|
||||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
||||||
|
|
@ -142,168 +147,6 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- 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) ───────────────────────────────────────────────
|
# ── ERPNext (Frappe Framework) ───────────────────────────────────────────────
|
||||||
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
|
# Perfil `erpnext` — suba com: docker compose --profile erpnext up
|
||||||
erpnext-db:
|
erpnext-db:
|
||||||
|
|
@ -440,7 +283,7 @@ services:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
|
|
||||||
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
# ── LiteLLM (gateway unificado de LLM — soberania dos dados) ─────────────────
|
||||||
# Roteia: LLMFit (fine-tuned) → Ollama (local) → externo (opt-in)
|
# Roteia: Ollama (local) → externo (opt-in)
|
||||||
litellm:
|
litellm:
|
||||||
image: ghcr.io/berriai/litellm:main-latest
|
image: ghcr.io/berriai/litellm:main-latest
|
||||||
restart: always
|
restart: always
|
||||||
|
|
@ -454,9 +297,6 @@ services:
|
||||||
# Ollama: se instalado no host use http://host-gateway:11434
|
# Ollama: se instalado no host use http://host-gateway:11434
|
||||||
# Se usar container Docker, mantém http://ollama:11434
|
# Se usar container Docker, mantém http://ollama:11434
|
||||||
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-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)
|
# Providers externos opcionais (soberania: só habilitados se configurados)
|
||||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
GROQ_API_KEY: ${GROQ_API_KEY:-}
|
||||||
|
|
@ -478,10 +318,8 @@ services:
|
||||||
- ollama_models:/root/.ollama
|
- ollama_models:/root/.ollama
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- 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:
|
open-webui:
|
||||||
image: ghcr.io/open-webui/open-webui:main
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
restart: always
|
restart: always
|
||||||
|
|
@ -539,7 +377,37 @@ services:
|
||||||
- "traefik.http.routers.superset.tls=true"
|
- "traefik.http.routers.superset.tls=true"
|
||||||
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.services.superset.loadbalancer.server.port=8088"
|
- "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:
|
networks:
|
||||||
arcadia-internal:
|
arcadia-internal:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -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
|
||||||
|
|
@ -3,29 +3,15 @@
|
||||||
#
|
#
|
||||||
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
# ESTRATÉGIA DE SOBERANIA DOS DADOS:
|
||||||
# ┌─────────────────────────────────────────────────────────────────────────┐
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
# │ TIER 1 (soberania total): LLMFit — modelos fine-tuned locais │
|
# │ TIER 1 (soberania total): Ollama — modelos open source no servidor │
|
||||||
# │ TIER 2 (soberania total): Ollama — modelos open source no servidor │
|
# │ TIER 2 (opt-in): Providers externos — só com configuração explícita │
|
||||||
# │ 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.
|
# O Manus, Autonomous Agents e todos os serviços chamam APENAS este proxy.
|
||||||
# Nunca chamam APIs externas diretamente.
|
# Nunca chamam APIs externas diretamente.
|
||||||
|
|
||||||
model_list:
|
model_list:
|
||||||
|
|
||||||
# ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ────────────
|
# ── TIER 1: Ollama (LLMs locais — soberania total) ───────────────────────────
|
||||||
- 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
|
- model_name: llama3.2
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: ollama/llama3.2:3b
|
model: ollama/llama3.2:3b
|
||||||
|
|
@ -42,7 +28,7 @@ model_list:
|
||||||
model: ollama/nomic-embed-text
|
model: ollama/nomic-embed-text
|
||||||
api_base: os.environ/OLLAMA_BASE_URL
|
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
|
- model_name: gpt-4o
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
|
|
@ -53,34 +39,30 @@ model_list:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
api_key: os.environ/OPENAI_API_KEY
|
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
|
# - model_name: claude-sonnet
|
||||||
# litellm_params:
|
# litellm_params:
|
||||||
# model: anthropic/claude-sonnet-4-6
|
# model: anthropic/claude-sonnet-4-6
|
||||||
# api_key: os.environ/ANTHROPIC_API_KEY
|
# 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
|
# - model_name: groq-llama
|
||||||
# litellm_params:
|
# litellm_params:
|
||||||
# model: groq/llama-3.3-70b-versatile
|
# model: groq/llama-3.3-70b-versatile
|
||||||
# api_key: os.environ/GROQ_API_KEY
|
# api_key: os.environ/GROQ_API_KEY
|
||||||
|
|
||||||
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
||||||
# Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
|
|
||||||
- model_name: arcadia-default
|
- model_name: arcadia-default
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/llama3.2:3b
|
model: ollama/llama3.2:3b
|
||||||
api_base: os.environ/LLMFIT_BASE_URL
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
api_key: os.environ/LLMFIT_API_KEY
|
|
||||||
|
|
||||||
router_settings:
|
router_settings:
|
||||||
routing_strategy: least-busy
|
routing_strategy: least-busy
|
||||||
fallbacks:
|
fallbacks:
|
||||||
- {"gpt-4o": ["llama3.2"]}
|
- {"gpt-4o": ["llama3.2"]}
|
||||||
- {"gpt-4o-mini": ["llama3.2"]}
|
- {"gpt-4o-mini": ["llama3.2"]}
|
||||||
- {"arcadia-default": ["llama3.2"]}
|
- {"arcadia-default": ["llama3.2"]}
|
||||||
- {"arcadia-finetuned": ["llama3.2"]}
|
|
||||||
- {"arcadia-embed": ["nomic-embed-text"]}
|
|
||||||
|
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
drop_params: true
|
drop_params: true
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,21 @@
|
||||||
# Executado ao iniciar o container
|
# Executado ao iniciar o container
|
||||||
set -e
|
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_EMAIL="${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}"
|
||||||
SUPERSET_ADMIN_PASSWORD="${SUPERSET_ADMIN_PASSWORD:-arcadia2026}"
|
SUPERSET_ADMIN_PASSWORD="${SUPERSET_ADMIN_PASSWORD:-arcadia2026}"
|
||||||
ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/arcadia}"
|
ARCADIA_DB_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/arcadia}"
|
||||||
|
|
||||||
echo "[Superset Init] Aguardando PostgreSQL..."
|
echo "[Superset Init] Aguardando PostgreSQL..."
|
||||||
until python -c "
|
cat > /tmp/pgcheck.py << 'EOF'
|
||||||
import psycopg2, os, sys
|
import psycopg2, os, sys
|
||||||
|
url = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://arcadia:arcadia123@db:5432/superset')
|
||||||
try:
|
try:
|
||||||
url = os.environ.get('DATABASE_URL', 'postgresql://arcadia:arcadia123@db:5432/arcadia_superset')
|
|
||||||
psycopg2.connect(url)
|
psycopg2.connect(url)
|
||||||
sys.exit(0)
|
except Exception:
|
||||||
except: sys.exit(1)
|
sys.exit(1)
|
||||||
" 2>/dev/null; do
|
EOF
|
||||||
|
until python /tmp/pgcheck.py 2>/dev/null; do
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
echo "[Superset Init] PostgreSQL disponível!"
|
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 ────────────────────────────────────────────
|
# ── Banco de metadados do Superset ────────────────────────────────────────────
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
"DATABASE_URL",
|
"SQLALCHEMY_DATABASE_URI",
|
||||||
"postgresql://arcadia:arcadia123@db:5432/arcadia_superset"
|
os.environ.get("DATABASE_URL", "postgresql://arcadia:arcadia123@db:5432/superset")
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── CORS — permite o gateway Arcádia (:5000) chamar a API ────────────────────
|
# ── CORS — permite o gateway Arcádia (:5000) chamar a API ────────────────────
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -55,7 +55,7 @@ async function buildAll() {
|
||||||
define: {
|
define: {
|
||||||
"process.env.NODE_ENV": '"production"',
|
"process.env.NODE_ENV": '"production"',
|
||||||
},
|
},
|
||||||
minify: true,
|
minify: false,
|
||||||
external: externals,
|
external: externals,
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,14 @@ async function comparePasswords(supplied: string, stored: string) {
|
||||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.SESSION_SECRET) {
|
// CORREÇÃO: Session secret seguro - obrigatório em produção
|
||||||
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production.");
|
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 = {
|
const sessionSettings: session.SessionOptions = {
|
||||||
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`,
|
secret: SESSION_SECRET,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
store: storage.sessionStore,
|
store: storage.sessionStore,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,26 @@ import ws from "ws";
|
||||||
neonConfig.webSocketConstructor = ws;
|
neonConfig.webSocketConstructor = ws;
|
||||||
|
|
||||||
const app = express();
|
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());
|
app.use(express.json());
|
||||||
|
|
||||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -1,12 +1,69 @@
|
||||||
import type { Express, Request, Response } from "express";
|
import type { Express, Request, Response } from "express";
|
||||||
import crypto from "crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { createNode } from "../graph/service";
|
|
||||||
|
|
||||||
const MIROFLOW_HOST = process.env.MIROFLOW_HOST || "localhost";
|
const MIROFLOW_HOST = process.env.MIROFLOW_HOST || "localhost";
|
||||||
const MIROFLOW_PORT = parseInt(process.env.MIROFLOW_PORT || "8006", 10);
|
const MIROFLOW_PORT = parseInt(process.env.MIROFLOW_PORT || "8006", 10);
|
||||||
const MIROFLOW_URL = `http://${MIROFLOW_HOST}:${MIROFLOW_PORT}`;
|
const MIROFLOW_URL = `http://${MIROFLOW_HOST}:${MIROFLOW_PORT}`;
|
||||||
const MIROFLOW_TIMEOUT = 300_000; // 5 minutos — deepseek-r1:14b pode levar 60-120s
|
const MIROFLOW_TIMEOUT = 600_000; // 10 minutos
|
||||||
const MIROFLOW_HEALTH_TIMEOUT = 5_000; // 5 segundos para health check
|
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> {
|
async function proxyToMiroFlow(path: string, method: string = "GET", body?: object): Promise<any> {
|
||||||
const timeoutMs = path === "/health" ? MIROFLOW_HEALTH_TIMEOUT : MIROFLOW_TIMEOUT;
|
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();
|
return await response.json();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
if (err.name === "AbortError") throw new Error("MiroFlow timeout (300s)");
|
if (err.name === "AbortError") throw new Error("MiroFlow timeout (600s)");
|
||||||
throw err;
|
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 {
|
export function registerMiroFlowRoutes(app: Express): void {
|
||||||
app.get("/api/miroflow/health", async (_req: Request, res: Response) => {
|
app.get("/api/miroflow/health", async (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -73,19 +100,50 @@ export function registerMiroFlowRoutes(app: Express): void {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/miroflow/analyze", async (req: Request, res: Response) => {
|
app.post("/api/miroflow/analyze", async (req: Request, res: Response) => {
|
||||||
if (!req.isAuthenticated()) {
|
console.log("[MiroFlow] POST /api/miroflow/analyze - começando");
|
||||||
return res.status(401).json({ error: "Não autenticado" });
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const inputBody = {
|
const { agent = "statistician", task, context } = req.body as {
|
||||||
...req.body,
|
agent?: string;
|
||||||
tenant_id: (req.user as any)?.tenantId ?? null,
|
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)
|
if (!task?.trim()) {
|
||||||
registerExecutionInKG(req, data, req.body).catch(() => {});
|
return res.status(400).json({ error: "Campo 'task' é obrigatório" });
|
||||||
res.json(data);
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err: any) {
|
||||||
|
console.error("[MiroFlow] erro:", err.message);
|
||||||
res.status(502).json({ error: err.message });
|
res.status(502).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9d180f1eeb5e301c440ad7ce8b12c14222779040
|
Subproject commit fe1a7397fbb5a3eee145feb2fac94f620ef43f3e
|
||||||
|
|
@ -182,11 +182,22 @@ export async function registerRoutes(
|
||||||
// Arcádia Plus - SSO routes (proxy already registered at top)
|
// Arcádia Plus - SSO routes (proxy already registered at top)
|
||||||
app.use("/api/plus/sso", plusSsoRoutes);
|
app.use("/api/plus/sso", plusSsoRoutes);
|
||||||
|
|
||||||
|
// CORREÇÃO: /api/tenants protegido - apenas admin vê todos
|
||||||
app.get("/api/tenants", async (req: any, res) => {
|
app.get("/api/tenants", async (req: any, res) => {
|
||||||
if (!req.isAuthenticated()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).json({ error: "Authentication required" });
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
}
|
}
|
||||||
try {
|
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();
|
const tenants = await storage.getTenants();
|
||||||
res.json(tenants);
|
res.json(tenants);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface IStorage {
|
||||||
sessionStore: session.Store;
|
sessionStore: session.Store;
|
||||||
|
|
||||||
getUser(id: string): Promise<User | undefined>;
|
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>;
|
createUser(user: InsertUser): Promise<User>;
|
||||||
getEnrichedUser(user: User): Promise<any>;
|
getEnrichedUser(user: User): Promise<any>;
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export interface IStorage {
|
||||||
getUserApplications(userId: string): Promise<Application[]>;
|
getUserApplications(userId: string): Promise<Application[]>;
|
||||||
assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
|
assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
|
||||||
removeApplicationFromUser(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 }[]>;
|
getTenants(): Promise<{ id: number; name: string; slug: string }[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +47,21 @@ export class DatabaseStorage implements IStorage {
|
||||||
return user;
|
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));
|
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;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +160,19 @@ export class DatabaseStorage implements IStorage {
|
||||||
return enriched;
|
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 }[]> {
|
async getTenants(): Promise<{ id: number; name: string; slug: string }[]> {
|
||||||
const result = await db.select({
|
const result = await db.select({
|
||||||
id: tenants.id,
|
id: tenants.id,
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Express, Request, Response } from "express";
|
import type { Express, Request, Response } from "express";
|
||||||
|
import { createMiroFlowBridge } from "./MiroFlowBridge.js";
|
||||||
|
|
||||||
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
||||||
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
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_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
||||||
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
||||||
|
|
||||||
|
const miroflowBridge = createMiroFlowBridge();
|
||||||
|
|
||||||
// Cache do service token (válido por 50 min)
|
// Cache do service token (válido por 50 min)
|
||||||
let cachedToken: string | null = null;
|
let cachedToken: string | null = null;
|
||||||
let tokenExpiry = 0;
|
let tokenExpiry = 0;
|
||||||
|
|
@ -105,4 +108,183 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
res.redirect("/superset/login");
|
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