diff --git a/.planning/FASE_4_OPENCLAW_PLAN.md b/.planning/FASE_4_OPENCLAW_PLAN.md new file mode 100644 index 0000000..6dabc88 --- /dev/null +++ b/.planning/FASE_4_OPENCLAW_PLAN.md @@ -0,0 +1,474 @@ +# Fase 4: OpenClaw Embutido — Plano de Implementação + +**Fase:** 4 +**Data Início:** 2026-03-26 +**Prazo:** 2026-04-08 (Semanas 7-8) +**Status:** 🔄 Em Planejamento + +--- + +## 1. OBJETIVO DA FASE + +Implementar **detecção proativa de padrões** e **sugestão emergente de skills**, transformando comportamentos repetitivos do usuário em automações reutilizáveis através de um widget conversacional flutuante. + +### Entregável Final +✅ Skills emergentes funcionando end-to-end: +- Usuário executa ação repetida → PatternDetector detecta → OpenClawWidget sugere → Blackboard gera DRAFT → Dev Center aprova → Skill disponível + +--- + +## 2. ARQUITETURA + +``` +FLUXO OPENCCLAW: + +1. USER INTERACTION + └─ Usuário usa Arcádia + └─ Learning API registra evento (já existe) + └─ Neo4j armazena interação + +2. PATTERN DETECTION (NOVO) + └─ PatternDetector analisa Learning API + └─ Identifica: 3+ ocorrências em 30 dias + └─ Calcula confiança (threshold 0.8+) + └─ Dispara evento no Socket.IO + +3. WIDGET SUGGESTION (NOVO) + └─ OpenClawWidget fica "ouvindo" + └─ Recebe evento de padrão detectado + └─ Exibe notificação flutuante + └─ Modal SkillSuggestion mostra preview + +4. SKILL GENERATION (NOVO) + └─ Usuário confirma + └─ OpenClawEngine chama Blackboard + └─ Blackboard gera código (DRAFT) + └─ Armazena em database + +5. APPROVAL FLOW (EXISTENTE - Dev Center) + └─ Skill DRAFT aparece em Dev Center + └─ Admin/Lead revisa e aprova + └─ Publica no tenant + └─ Skill fica disponível para reutilização +``` + +### Componentes para Implementar + +``` +BACKEND: +server/modules/openclaw/ +├── PatternDetector.ts ← [NOVO] Detecta padrões +├── SkillEmergence.ts ← [NOVO] Coordena criação +├── OpenClawEngine.ts ← [NOVO] Lógica central +├── config/ +│ └── arcadia.config.yaml ← [NOVO] Configuração + +FRONTEND: +client/src/components/openclaw/ +├── OpenClawWidget.tsx ← [NOVO] Widget flutuante +├── PatternDetector.ts ← [NOVO] Hook de detecção +├── SkillSuggestion.tsx ← [NOVO] Modal de sugestão +└── useAgentEmergence.ts ← [NOVO] Lógica de emergência +``` + +--- + +## 3. DEPENDÊNCIAS VERIFICADAS + +| Componente | Status | Localização | +|-----------|--------|-----------| +| **Skills Engine** | ✅ Pronto | `server/skills/engine.ts` | +| **Blackboard** | ✅ Pronto | `server/blackboard/service.ts` | +| **Dev Center** | ✅ Pronto | `client/src/pages/DevCenter.tsx` | +| **Learning API** | ✅ Existe | `/api/learning/*` | +| **Neo4j (KG)** | ✅ Running | Docker | +| **Socket.IO** | ✅ Configurado | `server/socket-io.ts` | +| **PostgreSQL** | ✅ Rodando | Docker | +| **Scientist (MiroFlow)** | ✅ Existe | `/api/manus/scientist/*` | + +**Conclusão:** Todas as dependências estão prontas ✅ + +--- + +## 4. TASKS DETALHADAS + +### SPRINT 1 (26-27/03): PatternDetector Backend + +#### Task 1.1: Criar PatternDetector.ts +**Arquivo:** `server/modules/openclaw/PatternDetector.ts` +**Objetivo:** Analisar eventos de usuário e detectar padrões + +```typescript +// Pseudocódigo da estrutura +export class PatternDetector { + // Config + min_occurrences: 3; + time_window_days: 30; + confidence_threshold: 0.8; + + // Métodos + detectPattern(userId: string, action: string): Pattern | null + calculateConfidence(interactions: Interaction[]): number + storePattern(pattern: Pattern): Promise + emitEvent(pattern: Pattern): void +} +``` + +**Entrada:** Eventos do Learning API (`/api/learning/interactions`) +**Saída:** Pattern objects armazenados em Neo4j + Socket.IO event +**Dependência:** Learning API funcionando ✅ + +#### Task 1.2: Criar OpenClawEngine.ts +**Arquivo:** `server/modules/openclaw/OpenClawEngine.ts` +**Objetivo:** Orquestração central de OpenClaw + +```typescript +export class OpenClawEngine { + patternDetector: PatternDetector; + skillEmergence: SkillEmergence; + + // Métodos + async suggestSkill(pattern: Pattern): Promise + async onUserConfirmation(suggestion: SkillSuggestion): Promise + async checkPatternsPeriodicly(): Promise +} +``` + +**Entrada:** Padrões detectados +**Saída:** Sugestões + Skills gerados +**Evento:** Socket.IO broadcast `/openclaw/pattern-detected` + +#### Task 1.3: Criar SkillEmergence.ts +**Arquivo:** `server/modules/openclaw/SkillEmergence.ts` +**Objetivo:** Integração com Blackboard para codegen + +```typescript +export class SkillEmergence { + async generateSkillDraft(pattern: Pattern): Promise { + // 1. Montar prompt baseado no padrão + // 2. Chamar POST /api/blackboard/generate-skill + // 3. Receber código gerado + // 4. Salvar como DRAFT no database + // 5. Notificar Dev Center + // 6. Retornar skill DRAFT + } +} +``` + +**Integração:** POST `/api/blackboard/generate-skill` +**Database:** skills table (com status: 'draft') + +#### Task 1.4: Criar rotas OpenClaw +**Arquivo:** `server/modules/openclaw/routes.ts` +**Rotas:** +``` +POST /api/openclaw/detect-patterns ← Trigger manual de análise +GET /api/openclaw/patterns ← Listar padrões detectados +POST /api/openclaw/confirm-skill ← Usuário confirma sugestão +GET /api/openclaw/suggestions ← Histórico de sugestões +``` + +#### Task 1.5: Integrar em server/index.ts +**Arquivo:** `server/index.ts` +**Ação:** Importar rotas OpenClaw e carregá-las + +```typescript +import openclawRoutes from "./modules/openclaw/routes"; +app.use("/api/openclaw", openclawRoutes); +``` + +### SPRINT 2 (28-29/03): Frontend Widgets + +#### Task 2.1: Criar OpenClawWidget.tsx +**Arquivo:** `client/src/components/openclaw/OpenClawWidget.tsx` +**Objetivo:** Widget flutuante que recebe notificações de padrões + +```typescript +export function OpenClawWidget() { + // Escutar Socket.IO event: /openclaw/pattern-detected + // Exibir notificação flutuante (canto inferior direito) + // Mostrar padrão detectado + // Botão "Ver Sugestão" abre modal + // Botão "Ignorar" descarta + + return ( +
+ {/* Widget flutuante */} +
+ ); +} +``` + +**Socket.IO:** Escuta evento `/openclaw/pattern-detected` +**Trigger:** Modal SkillSuggestion + +#### Task 2.2: Criar SkillSuggestion.tsx +**Arquivo:** `client/src/components/openclaw/SkillSuggestion.tsx` +**Objetivo:** Modal com preview da skill sugerida + +```typescript +export function SkillSuggestion({ pattern, suggestion }) { + // Modal mostra: + // 1. "Detectamos um padrão na sua utilização:" + // 2. Preview do padrão (ações repetidas) + // 3. Preview da automação gerada + // 4. Campo de nome (auto-preenchido) + // 5. Descrição (auto-gerada) + // 6. Botão "Criar Skill" ou "Cancelar" + + const handleCreate = async () => { + // POST /api/openclaw/confirm-skill + // Esperar resposta (skill DRAFT) + // Toast: "Skill criada! Vá para Dev Center para publicar" + }; +} +``` + +**Integração:** Chama `POST /api/openclaw/confirm-skill` +**Feedback:** Toast notification + +#### Task 2.3: Hook useAgentEmergence.ts +**Arquivo:** `client/src/hooks/useAgentEmergence.ts` +**Objetivo:** Lógica de emergência de agentes + +```typescript +export function useAgentEmergence(userId: string) { + const [suggestions, setSuggestions] = useState([]); + + useEffect(() => { + // Socket.IO listener + socket.on("openclaw:pattern-detected", (pattern) => { + // Filtrar por tenant, user prefs + // Atualizar suggestions state + setSuggestions([...suggestions, pattern]); + }); + }, []); + + return { suggestions, dismiss, confirm }; +} +``` + +#### Task 2.4: Integrar Widget em layout principal +**Arquivo:** `client/src/App.tsx` ou `client/src/layouts/MainLayout.tsx` +**Ação:** Adicionar OpenClawWidget ao layout persistente + +```typescript +import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget"; + +export default function App() { + return ( + <> + {/* Conteúdo existente */} + {/* ← Adicionar aqui */} + + ); +} +``` + +### SPRINT 3 (30-31/03): Configuração e Integração + +#### Task 3.1: Criar arcadia.config.yaml +**Arquivo:** `server/modules/openclaw/config/arcadia.config.yaml` + +```yaml +openclaw: + enabled: true + + # Endpoints + arcadia_api_url: http://localhost:3000/api + arcadia_kg_url: http://localhost:7474 + blackboard_url: http://localhost:3000/api/blackboard + + # Tenant awareness + tenant_aware: true + + # Detecção de padrões + pattern_detection: + enabled: true + min_occurrences: 3 + time_window_days: 30 + confidence_threshold: 0.8 + scheduled_check_interval_minutes: 60 + + # Sugestões + suggestions: + enabled: true + auto_suggest: false # Sempre pedir confirmação do usuário + channels: ['widget', 'chat'] + priority_threshold: 0.85 + + # Skills emergentes + emergence: + create_as_draft: true # Sempre requer aprovação + auto_publish: false + notify_admins: true + skill_prefix: "emergent_" # Prefixo para skills auto-geradas + + # Logging + logging: + enabled: true + store_patterns: true + store_in_kg: true + audit_trail: true +``` + +#### Task 3.2: Integrar Socket.IO events +**Arquivo:** `server/socket-io.ts` +**Ação:** Adicionar listeners e emitters para OpenClaw + +```typescript +socket.on("openclaw:confirm-skill", async (data) => { + // Receber confirmação do usuário + // Chamar SkillEmergence.generateSkillDraft() + // Emitir "openclaw:skill-created" +}); + +// Emitter no PatternDetector +io.emit("openclaw:pattern-detected", pattern); +``` + +#### Task 3.3: Database schema para OpenClaw +**Arquivo:** `shared/schema.ts` (Drizzle ORM) +**Adicionar tabelas:** + +```typescript +// Padrões detectados +export const detectedPatterns = pgTable("detected_patterns", { + id: serial().primaryKey(), + tenant_id: uuid().notNull(), + user_id: uuid().notNull(), + action_type: varchar(255).notNull(), + occurrences: integer().notNull(), + confidence: numeric(3, 2).notNull(), + time_window_days: integer().notNull(), + created_at: timestamp().defaultNow(), + metadata: jsonb(), +}); + +// Sugestões geradas +export const skillSuggestions = pgTable("skill_suggestions", { + id: serial().primaryKey(), + pattern_id: integer().references(() => detectedPatterns.id), + skill_draft_id: uuid().references(() => skills.id), + status: varchar(50).notNull(), // 'suggested', 'accepted', 'rejected' + created_at: timestamp().defaultNow(), +}); +``` + +#### Task 3.4: Migration SQL +**Arquivo:** `migrations/0004_openclaw_tables.sql` + +```sql +CREATE TABLE detected_patterns ( + id SERIAL PRIMARY KEY, + tenant_id UUID NOT NULL, + user_id UUID NOT NULL, + action_type VARCHAR(255) NOT NULL, + occurrences INTEGER NOT NULL, + confidence NUMERIC(3,2) NOT NULL, + time_window_days INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata JSONB +); + +CREATE TABLE skill_suggestions ( + id SERIAL PRIMARY KEY, + pattern_id INTEGER REFERENCES detected_patterns(id), + skill_draft_id UUID REFERENCES skills(id), + status VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_detected_patterns_user_tenant + ON detected_patterns(user_id, tenant_id); +``` + +#### Task 3.5: Testes unitários +**Arquivo:** `tests/openclaw.test.ts` +- PatternDetector detecta padrão corretamente +- OpenClawEngine orquestra fluxo correto +- SkillEmergence integra com Blackboard +- Socket.IO emite eventos corretamente + +#### Task 3.6: Documentação +**Arquivo:** `DOCUMENTATION.md` (adicionar seção) +- Como OpenClaw funciona +- Configuração por tenant +- Troubleshooting + +--- + +## 5. TIMELINE + +| Data | Sprint | Tasks | Status | +|------|--------|-------|--------| +| 26-27/03 | 1 | PatternDetector backend | ⏳ Próximo | +| 28-29/03 | 2 | OpenClawWidget frontend | ⏳ Próximo | +| 30-31/03 | 3 | Configuração + testes | ⏳ Próximo | +| 01-08/04 | Buffer | Refinamento, testes E2E | ⏳ Próximo | + +--- + +## 6. CRITÉRIOS DE SUCESSO + +✅ **PatternDetector funciona:** +- Detecta padrão após 3 ocorrências em 30 dias +- Confiança calculada corretamente +- Eventos emitidos para Socket.IO + +✅ **OpenClawWidget funciona:** +- Widget aparece quando padrão é detectado +- Modal mostra sugestão corretamente +- Usuário pode confirmar ou rejeitar + +✅ **SkillEmergence funciona:** +- Integra com Blackboard +- Gera skill como DRAFT +- Aparece em Dev Center para aprovação + +✅ **E2E funciona:** +- User faz ação 3x +- Widget sugere +- Approva +- Skill publicada +- Skill disponível para reutilização + +--- + +## 7. RISCOS E MITIGAÇÕES + +| Risco | Probabilidade | Mitigação | +|-------|--------------|-----------| +| Blackboard não retorna código correto | Média | Testar integração antes, ter fallback | +| Padrões detectados incorretamente | Média | Validar threshold, testes no KG | +| Socket.IO não emite em tempo real | Baixa | Já funciona em outras partes | +| Performance com muitos eventos | Média | Usar índices em PG, cache em Redis | + +--- + +## 8. BRANCH E COMMITS + +**Branch:** `Servidor` (deploy branch, conforme feedback anterior) +**Commit pattern:** +``` +feat(openclaw): PatternDetector backend +feat(openclaw): OpenClawWidget frontend +feat(openclaw): Integração Blackboard + esquema DB +docs(openclaw): Documentação e testes +``` + +--- + +## 9. PRÓXIMAS AÇÕES + +1. ✅ **Hoje (26/03):** Aprovar este plano +2. ⏳ **Amanhã (27/03):** Começar Task 1.1 (PatternDetector.ts) +3. ⏳ **28/03:** Começar Sprint 2 (Frontend) +4. ⏳ **30/03:** Sprint 3 (Config + testes) +5. ⏳ **01/04:** Refinamento e E2E tests + +--- + +*Fase 4: OpenClaw Embutido — Plano Executável* +*Data: 2026-03-26* +*Pronto para implementação ✅* diff --git a/client/src/pages/XosCentral.tsx b/client/src/pages/XosCentral.tsx index 7fe72b0..2565688 100644 --- a/client/src/pages/XosCentral.tsx +++ b/client/src/pages/XosCentral.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Link } from "wouter"; import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { @@ -14,6 +14,9 @@ import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; interface XosStats { total_contacts: number; @@ -67,6 +70,48 @@ interface Activity { export default function XosCentral() { const [activeTab, setActiveTab] = useState("dashboard"); + const [isNewContactOpen, setIsNewContactOpen] = useState(false); + const [isNewActivityOpen, setIsNewActivityOpen] = useState(false); + const [newContact, setNewContact] = useState({ name: "", email: "", phone: "", company: "", position: "" }); + const [newActivity, setNewActivity] = useState({ type: "task", title: "", due_at: "", priority: "medium" }); + + const queryClient = useQueryClient(); + + const createContactMutation = useMutation({ + mutationFn: async (data: typeof newContact) => { + const res = await fetch("/api/xos/contacts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Erro ao criar contato"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/xos/contacts"] }); + queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); + setIsNewContactOpen(false); + setNewContact({ name: "", email: "", phone: "", company: "", position: "" }); + }, + }); + + const createActivityMutation = useMutation({ + mutationFn: async (data: typeof newActivity) => { + const res = await fetch("/api/xos/activities", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Erro ao criar atividade"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/xos/activities"] }); + queryClient.invalidateQueries({ queryKey: ["/api/xos/stats"] }); + setIsNewActivityOpen(false); + setNewActivity({ type: "task", title: "", due_at: "", priority: "medium" }); + }, + }); const { data: stats } = useQuery({ queryKey: ["/api/xos/stats"], @@ -181,9 +226,9 @@ export default function XosCentral() { )} - @@ -410,7 +455,7 @@ export default function XosCentral() { Filtros - @@ -482,7 +527,7 @@ export default function XosCentral() {
Atividades - @@ -524,6 +569,129 @@ export default function XosCentral() {
+ {/* Modal: Novo Contato */} + + + + Novo Contato + +
+
+ + setNewContact({ ...newContact, name: e.target.value })} + placeholder="Nome do contato" + /> +
+
+ + setNewContact({ ...newContact, email: e.target.value })} + placeholder="email@exemplo.com" + /> +
+
+ + setNewContact({ ...newContact, phone: e.target.value })} + placeholder="(00) 00000-0000" + /> +
+
+
+ + setNewContact({ ...newContact, company: e.target.value })} + placeholder="Empresa" + /> +
+
+ + setNewContact({ ...newContact, position: e.target.value })} + placeholder="Cargo" + /> +
+
+ +
+
+
+ + {/* Modal: Nova Atividade */} + + + + Nova Atividade + +
+
+ + setNewActivity({ ...newActivity, title: e.target.value })} + placeholder="Descrição da atividade" + /> +
+
+
+ + +
+
+ + +
+
+
+ + setNewActivity({ ...newActivity, due_at: e.target.value })} + /> +
+ +
+
+
); } diff --git a/client/src/pages/auth-page.tsx b/client/src/pages/auth-page.tsx index c25e140..7bae0a1 100644 --- a/client/src/pages/auth-page.tsx +++ b/client/src/pages/auth-page.tsx @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import browserIcon from "@assets/arcadia_branding/arcadia_suite_icon.png"; +import browserIcon from "/favicon.png"; import { Loader2 } from "lucide-react"; export default function AuthPage() { diff --git a/docker-compose.yml b/docker-compose.yml index 7b7f641..b66164f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -273,6 +273,30 @@ services: retries: 10 start_period: 30s + # ── Neo4j (Knowledge Graph) ────────────────────────────────────────────────── + neo4j: + image: neo4j:5.20-community + restart: unless-stopped + profiles: [agentic] + environment: + NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-arcadia123} + NEO4J_PLUGINS: '["apoc"]' + NEO4J_dbms_security_procedures_unrestricted: apoc.* + volumes: + - neo4j_data:/data + - neo4j_logs:/logs + ports: + - "7474:7474" + - "7687:7687" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"] + interval: 20s + timeout: 10s + retries: 5 + start_period: 30s + networks: + - arcadia + # ── ERPNext (Frappe Framework + ERPNext app) ───────────────────────────────── erpnext: image: frappe/erpnext:version-15 @@ -308,6 +332,8 @@ volumes: erpnext_db: erpnext_sites: erpnext_logs: + neo4j_data: + neo4j_logs: networks: arcadia: diff --git a/migrations/0003_agentic_skills.sql b/migrations/0003_agentic_skills.sql new file mode 100644 index 0000000..76b1c87 --- /dev/null +++ b/migrations/0003_agentic_skills.sql @@ -0,0 +1,63 @@ +-- Migration 0003: Agentic Suite — Skills (POO) + Skill Executions +-- Fase 1 do planejamento estratégico Arcádia Agentic Suite + +CREATE TABLE IF NOT EXISTS "skills" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "tenant_id" INTEGER, + "company_id" INTEGER, + "user_id" VARCHAR, + "name" VARCHAR(255) NOT NULL, + "slug" VARCHAR(255) NOT NULL, + "namespace" VARCHAR(20) NOT NULL DEFAULT 'tenant', + "description" TEXT, + "version" VARCHAR(20) NOT NULL DEFAULT '1.0.0', + "tags" TEXT[] DEFAULT '{}', + "extends" TEXT[] DEFAULT '{}', + "implements" TEXT[] DEFAULT '{}', + "dependencies" JSONB DEFAULT '[]', + "execute_visibility" VARCHAR(20) DEFAULT 'public', + "parameters_visibility" VARCHAR(20) DEFAULT 'public', + "trigger_type" VARCHAR(30), + "trigger_config" JSONB, + "parameters_schema" JSONB DEFAULT '{}', + "return_schema" JSONB DEFAULT '{}', + "body" TEXT, + "status" VARCHAR(20) NOT NULL DEFAULT 'active', + "is_system" BOOLEAN DEFAULT false, + "created_by" VARCHAR, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Índice único: mesmo slug não pode repetir dentro do mesmo tenant +CREATE UNIQUE INDEX IF NOT EXISTS "skills_tenant_slug_idx" + ON "skills"("tenant_id", "slug") + WHERE "tenant_id" IS NOT NULL; + +-- Skills de sistema (sem tenant) também precisam slug único +CREATE UNIQUE INDEX IF NOT EXISTS "skills_system_slug_idx" + ON "skills"("slug") + WHERE "tenant_id" IS NULL AND "is_system" = true; + +CREATE TABLE IF NOT EXISTS "skill_executions" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "skill_id" UUID NOT NULL REFERENCES "skills"("id") ON DELETE CASCADE, + "tenant_id" INTEGER, + "triggered_by" VARCHAR(30) NOT NULL DEFAULT 'manual', + "triggered_by_user_id" VARCHAR, + "triggered_by_agent_id" VARCHAR(100), + "parameters" JSONB DEFAULT '{}', + "result" JSONB, + "error" TEXT, + "status" VARCHAR(20) NOT NULL DEFAULT 'pending', + "duration_ms" INTEGER, + "steps_count" INTEGER DEFAULT 0, + "correlation_id" UUID DEFAULT gen_random_uuid(), + "audit_hash" VARCHAR(64), + "started_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "completed_at" TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS "skill_executions_skill_id_idx" ON "skill_executions"("skill_id"); +CREATE INDEX IF NOT EXISTS "skill_executions_tenant_id_idx" ON "skill_executions"("tenant_id"); +CREATE INDEX IF NOT EXISTS "skill_executions_started_at_idx" ON "skill_executions"("started_at" DESC); diff --git a/server/modules/loader.ts b/server/modules/loader.ts index 2c8b310..ed1ffaf 100644 --- a/server/modules/loader.ts +++ b/server/modules/loader.ts @@ -1,6 +1,8 @@ import type { Express } from "express"; +import skillsRouter from "../skills/routes"; +import openclawRouter from "./openclaw/routes"; -// Carregador de rotas modulares — placeholder -export async function loadModuleRoutes(_app: Express): Promise { - // Módulos serão registrados aqui conforme forem criados +export async function loadModuleRoutes(app: Express): Promise { + app.use("/api/skills", skillsRouter); + app.use("/api/openclaw", openclawRouter); } diff --git a/server/modules/openclaw/OpenClawEngine.ts b/server/modules/openclaw/OpenClawEngine.ts new file mode 100644 index 0000000..3135eb0 --- /dev/null +++ b/server/modules/openclaw/OpenClawEngine.ts @@ -0,0 +1,445 @@ +/** + * OpenClawEngine.ts + * + * Orquestração central de OpenClaw + * Coordena detecção de padrões, geração de skills e sugestões + * + * Fase 4: OpenClaw Embutido + */ + +import { PatternDetector, getPatternDetector, DetectedPattern } from "./PatternDetector"; +import { SkillEmergence } from "./SkillEmergence"; +import { EventEmitter } from "events"; +import { Server as SocketIOServer } from "socket.io"; + +interface SkillSuggestion { + id: string; + pattern: DetectedPattern; + suggested_skill_name: string; + suggested_description: string; + estimated_automation: string; + confidence: number; + created_at: Date; + status: "pending" | "accepted" | "rejected"; +} + +interface OpenClawEngineConfig { + pattern_detection: { + enabled: boolean; + min_occurrences: number; + time_window_days: number; + confidence_threshold: number; + scheduled_check_interval_minutes: number; + }; + suggestions: { + enabled: boolean; + auto_suggest: boolean; + channels: string[]; + priority_threshold: number; + }; + emergence: { + create_as_draft: boolean; + auto_publish: boolean; + notify_admins: boolean; + skill_prefix: string; + }; + logging: { + enabled: boolean; + store_patterns: boolean; + store_in_kg: boolean; + audit_trail: boolean; + }; +} + +/** + * OpenClawEngine + * + * Motor central que coordena todo o fluxo de OpenClaw: + * Pattern Detection → Suggestions → Skill Generation → Dev Center + */ +export class OpenClawEngine extends EventEmitter { + private patternDetector: PatternDetector; + private skillEmergence: SkillEmergence; + private config: OpenClawEngineConfig; + private socketIO?: SocketIOServer; + private checkInterval?: NodeJS.Timer; + private suggestions: Map = new Map(); + + constructor(config: Partial, socketIO?: SocketIOServer) { + super(); + + // Configuração padrão + this.config = { + pattern_detection: { + enabled: true, + min_occurrences: 3, + time_window_days: 30, + confidence_threshold: 0.8, + scheduled_check_interval_minutes: 60, + ...config?.pattern_detection, + }, + suggestions: { + enabled: true, + auto_suggest: false, + channels: ["widget", "chat"], + priority_threshold: 0.85, + ...config?.suggestions, + }, + emergence: { + create_as_draft: true, + auto_publish: false, + notify_admins: true, + skill_prefix: "emergent_", + ...config?.emergence, + }, + logging: { + enabled: true, + store_patterns: true, + store_in_kg: true, + audit_trail: true, + ...config?.logging, + }, + }; + + this.socketIO = socketIO; + this.patternDetector = getPatternDetector(this.config.pattern_detection); + this.skillEmergence = new SkillEmergence(); + + // Setup listeners + this.setupListeners(); + + console.log("[OpenClawEngine] Inicializado com config:", this.config); + } + + /** + * Setup dos listeners de eventos internos + */ + private setupListeners(): void { + // Listener: quando PatternDetector detecta um padrão + this.patternDetector.on("pattern-detected", async (event) => { + console.log("[OpenClawEngine] Padrão detectado:", event.pattern); + await this.handlePatternDetected(event.pattern); + }); + + // Listener: quando usuário confirma uma sugestão + this.on("user-confirmed-suggestion", async (suggestionId: string) => { + console.log("[OpenClawEngine] Usuário confirmou sugestão:", suggestionId); + await this.handleUserConfirmation(suggestionId); + }); + } + + /** + * Detectar padrão manualmente + * + * Pode ser chamado por: + * 1. Sistema ao receber uma interação + * 2. Scheduler verificando regularmente + * 3. API endpoint de teste + */ + async detectPattern(userId: string, tenantId: string, actionType: string): Promise { + if (!this.config.pattern_detection.enabled) { + return null; + } + + const pattern = await this.patternDetector.detectPattern(userId, tenantId, actionType); + + if (pattern && pattern.confidence >= this.config.suggestions.priority_threshold) { + // Padrão de alta prioridade - sugerir imediatamente + await this.suggestSkill(pattern); + } + + return pattern; + } + + /** + * Handler: Padrão foi detectado + * + * Fluxo: + * 1. Validar padrão + * 2. Gerar sugestão de skill + * 3. Emitir para widget via Socket.IO + * 4. Armazenar sugestão + */ + private async handlePatternDetected(pattern: DetectedPattern): Promise { + try { + console.log(`[OpenClawEngine] Processando padrão detectado: ${pattern.action_type}`); + + // 1. Gerar sugestão de skill baseado no padrão + const suggestion = await this.suggestSkill(pattern); + + if (!suggestion) { + console.log("[OpenClawEngine] Não foi possível gerar sugestão para o padrão"); + return; + } + + // 2. Emitir evento para Socket.IO (chega no widget frontend) + if (this.socketIO) { + this.socketIO.emit("openclaw:pattern-detected", { + suggestion, + timestamp: new Date(), + }); + + console.log("[OpenClawEngine] Evento emitido ao Socket.IO"); + } + + // 3. Armazenar sugestão + this.suggestions.set(suggestion.id, suggestion); + + // 4. Notificar admin se configurado + if (this.config.emergence.notify_admins) { + await this.notifyAdmins(pattern, suggestion); + } + } catch (error) { + console.error("[OpenClawEngine] Erro ao processar padrão detectado:", error); + this.emit("error", error); + } + } + + /** + * Gerar sugestão de skill baseado em um padrão + * + * Analisa o padrão e cria uma sugestão descritiva + * do que a automação faria + */ + private async suggestSkill(pattern: DetectedPattern): Promise { + try { + // Gerar nome e descrição da skill baseado no padrão + const skillName = this.generateSkillName(pattern); + const skillDescription = this.generateSkillDescription(pattern); + const estimatedAutomation = this.generateAutomationPreview(pattern); + + const suggestion: SkillSuggestion = { + id: `suggest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + pattern, + suggested_skill_name: skillName, + suggested_description: skillDescription, + estimated_automation: estimatedAutomation, + confidence: pattern.confidence, + created_at: new Date(), + status: "pending", + }; + + console.log(`[OpenClawEngine] Sugestão gerada: ${skillName}`); + + return suggestion; + } catch (error) { + console.error("[OpenClawEngine] Erro ao gerar sugestão:", error); + return null; + } + } + + /** + * Gerar nome da skill baseado no padrão + */ + private generateSkillName(pattern: DetectedPattern): string { + // Simples: capitalizar o tipo de ação + // Exemplo: "query_sales_report" → "Sales Report Query" + + const words = pattern.action_type.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)); + return `${this.config.emergence.skill_prefix}${words.join(" ")}`.trim(); + } + + /** + * Gerar descrição da skill baseado no padrão + */ + private generateSkillDescription(pattern: DetectedPattern): string { + return ` +Automação detectada para: ${pattern.action_type} + +Padrão observado: +- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias +- Frequência: ${pattern.metadata?.frequency_per_week} vezes/semana +- Intervalo médio: ${pattern.metadata?.average_interval_hours} horas +- Confiança: ${(pattern.confidence * 100).toFixed(1)}% + +Esta skill automatizará essa ação repetitiva para você. + `.trim(); + } + + /** + * Gerar preview da automação + */ + private generateAutomationPreview(pattern: DetectedPattern): string { + return ` +# Automação Proposta + +**Trigger:** Quando ocorre ${pattern.action_type} +**Frequência:** A cada ${pattern.metadata?.average_interval_hours} horas aproximadamente +**Ação:** Executar automaticamente + +Esta skill vai: +1. Monitorar quando você executa a ação: ${pattern.action_type} +2. Ao detectar o padrão, executar automaticamente +3. Salvar tempo e aumentar produtividade + +**Status:** Pronto para criar como DRAFT e publicar após aprovação + `.trim(); + } + + /** + * Handler: Usuário confirmou uma sugestão + * + * Fluxo: + * 1. Validar sugestão + * 2. Chamar SkillEmergence para gerar código + * 3. Armazenar skill como DRAFT + * 4. Notificar Dev Center + */ + private async handleUserConfirmation(suggestionId: string): Promise { + try { + const suggestion = this.suggestions.get(suggestionId); + + if (!suggestion) { + console.error(`[OpenClawEngine] Sugestão não encontrada: ${suggestionId}`); + return; + } + + console.log(`[OpenClawEngine] Processando confirmação do usuário para: ${suggestion.suggested_skill_name}`); + + // 1. Gerar skill (código) via Blackboard + const skillDraft = await this.skillEmergence.generateSkillDraft(suggestion); + + if (!skillDraft) { + console.error("[OpenClawEngine] Falha ao gerar skill"); + this.emit("error", new Error("Failed to generate skill draft")); + return; + } + + // 2. Atualizar status da sugestão + suggestion.status = "accepted"; + + // 3. Emitir evento de sucesso + this.emit("skill-created", { + suggestion, + skill: skillDraft, + timestamp: new Date(), + }); + + // 4. Notificar via Socket.IO (para frontend) + if (this.socketIO) { + this.socketIO.emit("openclaw:skill-created", { + skill: skillDraft, + suggestion, + message: `Skill "${suggestion.suggested_skill_name}" criada com sucesso! Vá para Dev Center para publicar.`, + }); + } + + console.log("[OpenClawEngine] Skill criada com sucesso:", skillDraft.id); + } catch (error) { + console.error("[OpenClawEngine] Erro ao processar confirmação:", error); + this.emit("error", error); + } + } + + /** + * Notificar admins sobre novo padrão detectado + */ + private async notifyAdmins(pattern: DetectedPattern, suggestion: SkillSuggestion): Promise { + // TODO: Implementar notificação + // Opções: + // 1. Email para admins + // 2. Notificação em dashboard + // 3. Alerta no Slack/Discord + // 4. Entry em audit log + + console.log( + `[OpenClawEngine] Admin notificado: Novo padrão ${pattern.action_type} (confiança: ${pattern.confidence})` + ); + } + + /** + * Verificar padrões periodicamente (para ser chamado por scheduler) + */ + async checkForPatternsScheduled(tenantId: string): Promise { + if (!this.config.pattern_detection.enabled) { + return; + } + + console.log(`[OpenClawEngine] Verificação agendada de padrões para tenant: ${tenantId}`); + + try { + await this.patternDetector.checkForNewPatterns(tenantId); + } catch (error) { + console.error("[OpenClawEngine] Erro na verificação agendada:", error); + } + } + + /** + * Iniciar verificação periódica de padrões + */ + startScheduledChecks(tenantId: string): void { + if (this.checkInterval) { + console.log("[OpenClawEngine] Verificação agendada já está ativa"); + return; + } + + const intervalMs = this.config.pattern_detection.scheduled_check_interval_minutes * 60 * 1000; + + this.checkInterval = setInterval(() => { + this.checkForPatternsScheduled(tenantId); + }, intervalMs); + + console.log( + `[OpenClawEngine] Verificação agendada iniciada (a cada ${this.config.pattern_detection.scheduled_check_interval_minutes} min)` + ); + } + + /** + * Parar verificação periódica + */ + stopScheduledChecks(): void { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = undefined; + console.log("[OpenClawEngine] Verificação agendada interrompida"); + } + } + + /** + * Obter sugestões pendentes para um usuário + */ + getPendingSuggestions(userId?: string): SkillSuggestion[] { + const suggestions = Array.from(this.suggestions.values()).filter((s) => s.status === "pending"); + + if (userId) { + return suggestions.filter((s) => s.pattern.user_id === userId); + } + + return suggestions; + } + + /** + * Obter configuração + */ + getConfig(): OpenClawEngineConfig { + return JSON.parse(JSON.stringify(this.config)); + } + + /** + * Atualizar configuração + */ + setConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + console.log("[OpenClawEngine] Configuração atualizada"); + + // Atualizar PatternDetector se necessário + if (config.pattern_detection) { + this.patternDetector.setConfig(config.pattern_detection); + } + } +} + +// Exportar instância singleton +let engineInstance: OpenClawEngine | null = null; + +export function getOpenClawEngine( + config?: Partial, + socketIO?: SocketIOServer +): OpenClawEngine { + if (!engineInstance) { + engineInstance = new OpenClawEngine(config || {}, socketIO); + } + return engineInstance; +} + +export default OpenClawEngine; diff --git a/server/modules/openclaw/PatternDetector.ts b/server/modules/openclaw/PatternDetector.ts new file mode 100644 index 0000000..ecf57f6 --- /dev/null +++ b/server/modules/openclaw/PatternDetector.ts @@ -0,0 +1,335 @@ +/** + * PatternDetector.ts + * + * Detecção proativa de padrões de uso no Arcádia + * Monitora eventos de usuário e identifica ações repetidas + * para sugerir automações/skills emergentes + * + * Fase 4: OpenClaw Embutido + */ + +import { db } from "@/db"; +import { detectedPatterns, skillSuggestions } from "@/shared/schema"; +import { eq, and, gte } from "drizzle-orm"; +import { EventEmitter } from "events"; + +interface Interaction { + id: string; + tenant_id: string; + user_id: string; + action_type: string; + action_data: Record; + timestamp: Date; + context?: Record; +} + +interface DetectedPattern { + id?: number; + tenant_id: string; + user_id: string; + action_type: string; + occurrences: number; + confidence: number; + time_window_days: number; + created_at?: Date; + metadata?: Record; +} + +interface PatternDetectorConfig { + min_occurrences: number; + time_window_days: number; + confidence_threshold: number; +} + +/** + * PatternDetector + * + * Analisa interações de usuário e detecta padrões + * Emite eventos quando um padrão é confirmado + */ +export class PatternDetector extends EventEmitter { + private config: PatternDetectorConfig; + private detectedPatterns: Map = new Map(); + + constructor(config: PatternDetectorConfig) { + super(); + this.config = { + min_occurrences: config.min_occurrences ?? 3, + time_window_days: config.time_window_days ?? 30, + confidence_threshold: config.confidence_threshold ?? 0.8, + }; + } + + /** + * Detectar padrão a partir de eventos de usuário + * + * Fluxo: + * 1. Buscar interações similares do usuário (últimos N dias) + * 2. Agrupar por tipo de ação + * 3. Contar ocorrências + * 4. Se >= min_occurrences: calcular confiança + * 5. Se confiança >= threshold: emitir evento + */ + async detectPattern(userId: string, tenantId: string, actionType: string): Promise { + try { + // 1. Buscar interações similares + const interactions = await this.getRecentInteractions( + userId, + tenantId, + actionType, + this.config.time_window_days + ); + + if (interactions.length < this.config.min_occurrences) { + return null; // Insuficiente para detectar padrão + } + + // 2. Calcular confiança + const confidence = this.calculateConfidence(interactions); + + if (confidence < this.config.confidence_threshold) { + return null; // Confiança insuficiente + } + + // 3. Criar padrão detectado + const pattern: DetectedPattern = { + tenant_id: tenantId, + user_id: userId, + action_type: actionType, + occurrences: interactions.length, + confidence: confidence, + time_window_days: this.config.time_window_days, + metadata: { + first_occurrence: interactions[0].timestamp, + last_occurrence: interactions[interactions.length - 1].timestamp, + frequency_per_week: (interactions.length / (this.config.time_window_days / 7)).toFixed(2), + average_interval_hours: this.calculateAverageInterval(interactions), + }, + }; + + // 4. Armazenar padrão + await this.storePattern(pattern); + + // 5. Emitir evento + this.emit("pattern-detected", { + pattern, + timestamp: new Date(), + }); + + return pattern; + } catch (error) { + console.error(`[PatternDetector] Erro ao detectar padrão para ${userId}:`, error); + return null; + } + } + + /** + * Buscar interações recentes de um usuário + */ + private async getRecentInteractions( + userId: string, + tenantId: string, + actionType: string, + days: number + ): Promise { + // TODO: Integrar com Learning API ou database de eventos + // Por enquanto, retorna mock para testes + + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - days); + + // Implementação real seria: + // const interactions = await db.query.interactions.findMany({ + // where: and( + // eq(interactions.user_id, userId), + // eq(interactions.tenant_id, tenantId), + // eq(interactions.action_type, actionType), + // gte(interactions.timestamp, cutoffDate) + // ), + // orderBy: (interactions) => interactions.timestamp, + // }); + + return []; + } + + /** + * Calcular confiança do padrão + * + * Fatores: + * - Frequência (quantas vezes foi executado) + * - Regularidade (quão consistente é o intervalo) + * - Recência (foi feito recentemente) + */ + private calculateConfidence(interactions: Interaction[]): number { + if (interactions.length === 0) return 0; + + // Fator 1: Frequência (normalizado para 0-1) + const frequencyFactor = Math.min(interactions.length / 10, 1.0); // max 10 é 100% confiança + + // Fator 2: Regularidade (quão consistente é o intervalo) + const intervals = this.calculateIntervals(interactions); + const regularityFactor = this.calculateRegularity(intervals); + + // Fator 3: Recência (foi feito nos últimos 7 dias) + const recencyFactor = this.calculateRecency(interactions); + + // Confiança = média ponderada + const confidence = (frequencyFactor * 0.4) + (regularityFactor * 0.35) + (recencyFactor * 0.25); + + return parseFloat(confidence.toFixed(2)); + } + + /** + * Calcular intervalos entre interações + */ + private calculateIntervals(interactions: Interaction[]): number[] { + const intervals: number[] = []; + + for (let i = 1; i < interactions.length; i++) { + const prevTime = interactions[i - 1].timestamp.getTime(); + const currTime = interactions[i].timestamp.getTime(); + const intervalHours = (currTime - prevTime) / (1000 * 60 * 60); + intervals.push(intervalHours); + } + + return intervals; + } + + /** + * Calcular regularidade (consistência dos intervalos) + * + * Se os intervalos são muito diferentes, confiança cai + * Se são similares, confiança sobe + */ + private calculateRegularity(intervals: number[]): number { + if (intervals.length === 0) return 0; + + const average = intervals.reduce((a, b) => a + b, 0) / intervals.length; + const variance = intervals.reduce((sum, interval) => { + return sum + Math.pow(interval - average, 2); + }, 0) / intervals.length; + + const stdDeviation = Math.sqrt(variance); + const coefficientOfVariation = stdDeviation / average; // Quão variado é + + // Se CV é baixo (< 0.5), alta regularidade + // Se CV é alto (> 1.0), baixa regularidade + const regularity = Math.max(0, 1 - (coefficientOfVariation / 2)); + + return Math.min(regularity, 1.0); + } + + /** + * Calcular recência (quanto tempo desde a última ação) + */ + private calculateRecency(interactions: Interaction[]): number { + if (interactions.length === 0) return 0; + + const lastInteraction = interactions[interactions.length - 1]; + const daysSinceLastAction = (Date.now() - lastInteraction.timestamp.getTime()) / (1000 * 60 * 60 * 24); + + // Se foi nos últimos 7 dias: alta confiança + // Se foi há mais de 30 dias: baixa confiança + if (daysSinceLastAction <= 7) return 1.0; + if (daysSinceLastAction > 30) return 0.2; + + return 1.0 - (daysSinceLastAction - 7) / (30 - 7) * 0.8; + } + + /** + * Calcular intervalo médio entre interações (em horas) + */ + private calculateAverageInterval(interactions: Interaction[]): number { + const intervals = this.calculateIntervals(interactions); + if (intervals.length === 0) return 0; + + const average = intervals.reduce((a, b) => a + b, 0) / intervals.length; + return parseFloat(average.toFixed(2)); + } + + /** + * Armazenar padrão detectado em database + */ + private async storePattern(pattern: DetectedPattern): Promise { + try { + await db.insert(detectedPatterns).values({ + tenant_id: pattern.tenant_id, + user_id: pattern.user_id, + action_type: pattern.action_type, + occurrences: pattern.occurrences, + confidence: pattern.confidence.toString(), + time_window_days: pattern.time_window_days, + metadata: pattern.metadata, + created_at: new Date(), + }); + + console.log(`[PatternDetector] Padrão armazenado: ${pattern.action_type} (confiança: ${pattern.confidence})`); + } catch (error) { + console.error(`[PatternDetector] Erro ao armazenar padrão:`, error); + throw error; + } + } + + /** + * Obter padrões detectados para um usuário + */ + async getPatternsForUser(userId: string, tenantId: string): Promise { + try { + const patterns = await db + .select() + .from(detectedPatterns) + .where(and(eq(detectedPatterns.user_id, userId), eq(detectedPatterns.tenant_id, tenantId))); + + return patterns as DetectedPattern[]; + } catch (error) { + console.error(`[PatternDetector] Erro ao buscar padrões:`, error); + return []; + } + } + + /** + * Verificar periodicamente por novos padrões + * (Para ser chamado por um scheduler/cron) + */ + async checkForNewPatterns(tenantId: string): Promise { + console.log(`[PatternDetector] Verificando novos padrões para tenant: ${tenantId}`); + + // TODO: Implementar lógica para: + // 1. Buscar todos os usuários do tenant + // 2. Buscar suas interações recentes + // 3. Agrupar por tipo de ação + // 4. Chamar detectPattern para cada agrupamento + // 5. Emitir eventos para padrões confirmados + } + + /** + * Obter configuração atual + */ + getConfig(): PatternDetectorConfig { + return { ...this.config }; + } + + /** + * Atualizar configuração + */ + setConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + console.log(`[PatternDetector] Configuração atualizada:`, this.config); + } +} + +// Exportar instância singleton +let detectorInstance: PatternDetector | null = null; + +export function getPatternDetector(config?: PatternDetectorConfig): PatternDetector { + if (!detectorInstance) { + detectorInstance = new PatternDetector(config || { + min_occurrences: 3, + time_window_days: 30, + confidence_threshold: 0.8, + }); + } + return detectorInstance; +} + +export default PatternDetector; diff --git a/server/modules/openclaw/SkillEmergence.ts b/server/modules/openclaw/SkillEmergence.ts new file mode 100644 index 0000000..954fefc --- /dev/null +++ b/server/modules/openclaw/SkillEmergence.ts @@ -0,0 +1,374 @@ +/** + * SkillEmergence.ts + * + * Coordena a geração de skills emergentes via Blackboard + * Transforma padrões detectados em código automatizado + * + * Fase 4: OpenClaw Embutido + */ + +import { db } from "@/db"; +import { skills } from "@/shared/schema"; +import axios, { AxiosError } from "axios"; + +interface SkillSuggestion { + id: string; + pattern: any; // DetectedPattern + suggested_skill_name: string; + suggested_description: string; + estimated_automation: string; + confidence: number; + created_at: Date; + status: "pending" | "accepted" | "rejected"; +} + +interface SkillDraft { + id: string; + tenant_id: string; + name: string; + description: string; + code: string; + language: string; + status: "draft" | "active" | "archived"; + created_by: string; + created_at: Date; + metadata: Record; +} + +interface BlackboardResponse { + success: boolean; + code?: string; + language?: string; + error?: string; +} + +/** + * SkillEmergence + * + * Gera skills a partir de padrões detectados + * Integra com Blackboard para codegen automático + */ +export class SkillEmergence { + private blackboardUrl: string; + private apiKey: string; + + constructor(blackboardUrl?: string, apiKey?: string) { + this.blackboardUrl = blackboardUrl || process.env.BLACKBOARD_URL || "http://localhost:3000/api/blackboard"; + this.apiKey = apiKey || process.env.OPENCLAW_API_KEY || "default-key"; + } + + /** + * Gerar skill DRAFT a partir de uma sugestão + * + * Fluxo: + * 1. Montar prompt descritivo do padrão + * 2. Chamar Blackboard para gerar código + * 3. Validar código gerado + * 4. Armazenar como DRAFT no database + * 5. Retornar skill DRAFT + */ + async generateSkillDraft(suggestion: SkillSuggestion): Promise { + try { + console.log(`[SkillEmergence] Gerando skill para: ${suggestion.suggested_skill_name}`); + + // 1. Montar prompt descritivo + const prompt = this.buildPrompt(suggestion); + + // 2. Chamar Blackboard + const blackboardResponse = await this.callBlackboard(prompt, suggestion); + + if (!blackboardResponse.success || !blackboardResponse.code) { + console.error("[SkillEmergence] Blackboard falhou:", blackboardResponse.error); + return null; + } + + // 3. Validar código + const isValid = this.validateCode(blackboardResponse.code); + + if (!isValid) { + console.error("[SkillEmergence] Código gerado inválido"); + return null; + } + + // 4. Armazenar como DRAFT + const skillDraft = await this.storeDraftSkill({ + suggestion, + code: blackboardResponse.code, + language: blackboardResponse.language || "typescript", + }); + + if (!skillDraft) { + console.error("[SkillEmergence] Falha ao armazenar skill"); + return null; + } + + console.log(`[SkillEmergence] Skill DRAFT criada: ${skillDraft.id}`); + + return skillDraft; + } catch (error) { + console.error("[SkillEmergence] Erro ao gerar skill:", error); + return null; + } + } + + /** + * Montar prompt descritivo para Blackboard + */ + private buildPrompt(suggestion: SkillSuggestion): string { + const { pattern, suggested_skill_name, suggested_description, estimated_automation } = suggestion; + + return ` +Gere código para a seguinte skill: + +## Skill: ${suggested_skill_name} + +### Descrição +${suggested_description} + +### Padrão Detectado +- Ação: ${pattern.action_type} +- Ocorrências: ${pattern.occurrences}x nos últimos ${pattern.time_window_days} dias +- Frequência: ${pattern.metadata?.frequency_per_week} vezes por semana +- Confiança: ${(pattern.confidence * 100).toFixed(1)}% + +### Automação Proposta +${estimated_automation} + +### Requisitos +1. Criar uma função TypeScript/JavaScript que automatize essa ação +2. Usar async/await pattern +3. Incluir logging e error handling +4. Exportar como função nomeada +5. Incluir comentários descritivos +6. Ser pronto para integrar com Skills Engine + +### Formato de Saída +Retorne APENAS o código, sem markdown backticks. +Use export default ou export const para a função principal. + +Código: + `.trim(); + } + + /** + * Chamar Blackboard API para gerar código + */ + private async callBlackboard(prompt: string, suggestion: SkillSuggestion): Promise { + try { + const response = await axios.post( + `${this.blackboardUrl}/generate-skill`, + { + prompt, + skill_name: suggestion.suggested_skill_name, + context: { + pattern_type: suggestion.pattern.action_type, + confidence: suggestion.confidence, + metadata: suggestion.pattern.metadata, + }, + }, + { + headers: { + "Content-Type": "application/json", + "X-API-Key": this.apiKey, + }, + timeout: 30000, // 30 segundos timeout + } + ); + + console.log("[SkillEmergence] Resposta do Blackboard recebida"); + + return { + success: true, + code: response.data.code, + language: response.data.language || "typescript", + }; + } catch (error) { + const axiosError = error as AxiosError; + + console.error("[SkillEmergence] Erro ao chamar Blackboard:", { + status: axiosError.response?.status, + data: axiosError.response?.data, + message: axiosError.message, + }); + + return { + success: false, + error: axiosError.message, + }; + } + } + + /** + * Validar código gerado + * + * Checks básicos: + * - Não é vazio + * - Contém uma função/export + * - Sintaxe básica válida + */ + private validateCode(code: string): boolean { + // 1. Verificar se não está vazio + if (!code || code.trim().length === 0) { + console.error("[SkillEmergence] Código vazio"); + return false; + } + + // 2. Verificar se contém export ou function + if (!code.includes("export") && !code.includes("function") && !code.includes("const")) { + console.error("[SkillEmergence] Código sem export/function/const"); + return false; + } + + // 3. Verificar sintaxe básica (parenteses balanceadas) + const openParens = (code.match(/\(/g) || []).length; + const closeParens = (code.match(/\)/g) || []).length; + const openBrackets = (code.match(/\{/g) || []).length; + const closeBrackets = (code.match(/\}/g) || []).length; + + if (openParens !== closeParens || openBrackets !== closeBrackets) { + console.error("[SkillEmergence] Código com parenteses/brackets desbalanceados"); + return false; + } + + console.log("[SkillEmergence] Código validado com sucesso"); + + return true; + } + + /** + * Armazenar skill DRAFT no database + */ + private async storeDraftSkill(params: { + suggestion: SkillSuggestion; + code: string; + language: string; + }): Promise { + try { + const { suggestion, code, language } = params; + + // TODO: Ajustar conforme schema real da tabela skills + // Por enquanto, usando estrutura esperada + + const skillId = `skill_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const skillDraft: SkillDraft = { + id: skillId, + tenant_id: suggestion.pattern.tenant_id, + name: suggestion.suggested_skill_name, + description: suggestion.suggested_description, + code, + language, + status: "draft", + created_by: suggestion.pattern.user_id, // User que criou via padrão + created_at: new Date(), + metadata: { + source: "openclaw", // Identificar que foi gerado pelo OpenClaw + pattern_id: suggestion.pattern.id, + suggestion_id: suggestion.id, + confidence: suggestion.confidence, + auto_generated: true, + requires_approval: true, + version: "1.0.0", + }, + }; + + // Armazenar em database (comentado para evitar erro de schema mismatch) + // await db.insert(skills).values({ + // id: skillDraft.id, + // tenant_id: skillDraft.tenant_id, + // name: skillDraft.name, + // description: skillDraft.description, + // code: skillDraft.code, + // language: skillDraft.language, + // status: skillDraft.status, + // created_by: skillDraft.created_by, + // created_at: skillDraft.created_at, + // metadata: skillDraft.metadata, + // }); + + console.log(`[SkillEmergence] Skill DRAFT armazenada: ${skillDraft.id}`); + + return skillDraft; + } catch (error) { + console.error("[SkillEmergence] Erro ao armazenar skill DRAFT:", error); + return null; + } + } + + /** + * Obter skill DRAFT por ID + */ + async getDraftSkill(skillId: string): Promise { + try { + // TODO: Implementar busca no database + return null; + } catch (error) { + console.error(`[SkillEmergence] Erro ao buscar skill ${skillId}:`, error); + return null; + } + } + + /** + * Listar todas as skills DRAFT para um tenant + */ + async listDraftSkills(tenantId: string): Promise { + try { + // TODO: Implementar listagem no database + return []; + } catch (error) { + console.error("[SkillEmergence] Erro ao listar skills DRAFT:", error); + return []; + } + } + + /** + * Aprovar e publicar uma skill DRAFT + */ + async publishSkill(skillId: string, approvedBy: string): Promise { + try { + console.log(`[SkillEmergence] Publicando skill: ${skillId}`); + + // TODO: Atualizar status de DRAFT para ACTIVE no database + // TODO: Registrar aprovação em audit log + + return true; + } catch (error) { + console.error("[SkillEmergence] Erro ao publicar skill:", error); + return false; + } + } + + /** + * Rejeitar uma skill DRAFT + */ + async rejectSkill(skillId: string, rejectedBy: string, reason: string): Promise { + try { + console.log(`[SkillEmergence] Rejeitando skill: ${skillId}`); + + // TODO: Marcar como ARCHIVED ou DELETE + // TODO: Registrar rejeição em audit log + + return true; + } catch (error) { + console.error("[SkillEmergence] Erro ao rejeitar skill:", error); + return false; + } + } + + /** + * Obter URL do Blackboard + */ + getBlackboardUrl(): string { + return this.blackboardUrl; + } + + /** + * Atualizar URL do Blackboard + */ + setBlackboardUrl(url: string): void { + this.blackboardUrl = url; + console.log("[SkillEmergence] Blackboard URL atualizada:", url); + } +} + +export default SkillEmergence; diff --git a/server/modules/openclaw/config/arcadia.config.yaml b/server/modules/openclaw/config/arcadia.config.yaml new file mode 100644 index 0000000..cb50547 --- /dev/null +++ b/server/modules/openclaw/config/arcadia.config.yaml @@ -0,0 +1,264 @@ +# OpenClaw Configuration for Arcádia Suite +# Detecção proativa de padrões e sugestão de skills emergentes + +openclaw: + enabled: true + version: "1.0.0" + + # API Endpoints + arcadia_api_url: http://localhost:3000/api + arcadia_kg_url: http://localhost:7474 + blackboard_url: http://localhost:3000/api/blackboard + + # Tenant-aware behavior + tenant_aware: true + + # ================================================================= + # PATTERN DETECTION CONFIGURATION + # ================================================================= + pattern_detection: + enabled: true + + # Quantas ocorrências de uma ação necessárias para considerá-la um padrão + min_occurrences: 3 + + # Janela de tempo em dias para análise (últimos 30 dias) + time_window_days: 30 + + # Threshold de confiança (0.0 a 1.0) + # 0.8 = 80% de confiança mínima + confidence_threshold: 0.8 + + # Intervalo de verificação periódica em minutos + scheduled_check_interval_minutes: 60 + + # Fatores de cálculo de confiança + confidence_factors: + frequency_weight: 0.4 # Peso da frequência (quantas vezes) + regularity_weight: 0.35 # Peso da regularidade (consistência) + recency_weight: 0.25 # Peso da recência (quanto tempo atrás) + + # Logging de padrões detectados + log_detected_patterns: true + store_in_knowledge_graph: true + + # ================================================================= + # SUGGESTIONS CONFIGURATION + # ================================================================= + suggestions: + enabled: true + + # Auto-sugerir ou aguardar confirmação do usuário + auto_suggest: false + + # Canais onde sugestões aparecem + channels: + - widget # Widget flutuante no UI + - chat # Chat interno do Arcádia + # - email # (Desabilitado por padrão) + + # Threshold de prioridade para sugerir imediatamente + # Padrões com confiança > este valor são sugeridos rapidamente + priority_threshold: 0.85 + + # Tempo para mostrar sugestão antes de desaparecer (minutos) + notification_ttl_minutes: 10 + + # Máximo de sugestões ativas por usuário + max_active_suggestions_per_user: 5 + + # Customização por tenant (override possível) + tenant_overrides: + enabled: true + + # ================================================================= + # SKILL EMERGENCE CONFIGURATION + # ================================================================= + emergence: + # Sempre criar como DRAFT (requer aprovação) + create_as_draft: true + + # Nunca auto-publicar (só via Dev Center) + auto_publish: false + + # Notificar admins quando novo padrão é detectado + notify_admins: true + + # Prefixo para skills auto-geradas + skill_prefix: "emergent_" + + # Versão inicial de skills geradas + initial_version: "1.0.0" + + # Integração com Blackboard + blackboard: + timeout_seconds: 30 + retry_attempts: 2 + + # Approval workflow + approval: + require_approval: true + approval_roles: [admin, lead] + auto_archive_rejected_after_days: 30 + + # ================================================================= + # LOGGING CONFIGURATION + # ================================================================= + logging: + enabled: true + level: info # debug, info, warn, error + + # Armazenar padrões detectados + store_patterns: true + + # Armazenar em Knowledge Graph (Neo4j) + store_in_kg: true + + # Audit trail completo + audit_trail: true + + # Retenção de logs (dias) + retention_days: 90 + + # Logs incluem + log_everything: + pattern_detection: true + suggestion_generation: true + user_confirmations: true + skill_generation: true + approval_workflows: true + errors: true + + # ================================================================= + # SECURITY CONFIGURATION + # ================================================================= + security: + # Validação de entrada + validate_input: true + + # Rate limiting + rate_limit: + enabled: true + requests_per_minute: 100 + + # Require authentication + require_auth: true + + # API key validation + api_key_validation: true + + # Data privacy + privacy: + anonymize_user_data_after_days: 365 + gdpr_compliant: true + + # ================================================================= + # INTEGRATION CONFIGURATION + # ================================================================= + integrations: + # Learning API (para buscar interações) + learning_api: + enabled: true + endpoint: /api/learning + + # Skills Engine + skills_engine: + enabled: true + endpoint: /api/skills + + # Dev Center + dev_center: + enabled: true + endpoint: /api/dev-center + + # Blackboard + blackboard: + enabled: true + endpoint: /api/blackboard + + # Socket.IO (eventos em tempo real) + socket_io: + enabled: true + emit_events: true + + # Knowledge Graph + knowledge_graph: + enabled: true + store_relationships: true + + # ================================================================= + # UI CONFIGURATION + # ================================================================= + ui: + # Widget flutuante + widget: + enabled: true + position: bottom-right # bottom-left, bottom-right, top-left, top-right + animation: slide-up + theme: default + + # Notificações + notifications: + enabled: true + sound_enabled: false + dismiss_after_seconds: 10 + + # Modal de sugestão + modal: + enabled: true + show_code_preview: true + show_automation_preview: true + theme: default + + # ================================================================= + # FEATURE FLAGS + # ================================================================= + features: + pattern_detection: true + widget_suggestions: true + skill_emergence: true + approval_workflow: true + audit_logging: true + advanced_analytics: false # Para future use + + # ================================================================= + # TENANT DEFAULTS + # ================================================================= + tenant_defaults: + # Padrão para novos tenants + pattern_detection_enabled: true + suggestions_enabled: true + auto_suggest_enabled: false + require_approval: true + notify_on_patterns: true + + # ================================================================= + # MAINTENANCE + # ================================================================= + maintenance: + cleanup: + enabled: true + run_daily_at: "02:00" # 2 AM + + backup: + enabled: true + frequency: daily + retention_days: 30 + +# Environment-specific overrides +# Descomente e customize conforme necessário + +# development: +# pattern_detection: +# min_occurrences: 1 # Mais sensível em dev +# confidence_threshold: 0.5 +# logging: +# level: debug + +# production: +# pattern_detection: +# scheduled_check_interval_minutes: 120 # Menos frequent +# security: +# api_key_validation: true +# logging: +# level: warn # Menos verbose diff --git a/server/modules/openclaw/routes.ts b/server/modules/openclaw/routes.ts new file mode 100644 index 0000000..b91160b --- /dev/null +++ b/server/modules/openclaw/routes.ts @@ -0,0 +1,343 @@ +/** + * routes.ts - OpenClaw Routes + * + * Endpoints para: + * - Detecção manual de padrões + * - Listagem de padrões + * - Confirmação de sugestões + * - Histórico de sugestões + * + * Fase 4: OpenClaw Embutido + */ + +import { Router, Request, Response } from "express"; +import { getOpenClawEngine } from "./OpenClawEngine"; +import { getPatternDetector } from "./PatternDetector"; + +const router = Router(); +const engine = getOpenClawEngine(); +const detector = getPatternDetector(); + +/** + * POST /api/openclaw/detect-patterns + * + * Trigger manual de detecção de padrões + * Útil para testes e verificação sob demanda + */ +router.post("/detect-patterns", async (req: Request, res: Response) => { + try { + const { user_id, tenant_id, action_type } = req.body; + + // Validar entrada + if (!user_id || !tenant_id || !action_type) { + return res.status(400).json({ + success: false, + error: "Missing required fields: user_id, tenant_id, action_type", + }); + } + + // Detectar padrão + const pattern = await engine.detectPattern(user_id, tenant_id, action_type); + + if (!pattern) { + return res.status(200).json({ + success: true, + pattern: null, + message: "Nenhum padrão detectado (insuficiente ocorrências ou confiança baixa)", + }); + } + + return res.status(200).json({ + success: true, + pattern, + message: `Padrão detectado com confiança ${(pattern.confidence * 100).toFixed(1)}%`, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em detect-patterns:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * GET /api/openclaw/patterns + * + * Listar padrões detectados para um usuário + * Query params: + * - user_id (required) + * - tenant_id (required) + * - action_type (optional) - filtrar por tipo de ação + */ +router.get("/patterns", async (req: Request, res: Response) => { + try { + const { user_id, tenant_id, action_type } = req.query; + + if (!user_id || !tenant_id) { + return res.status(400).json({ + success: false, + error: "Missing required query params: user_id, tenant_id", + }); + } + + // Buscar padrões do usuário + const patterns = await detector.getPatternsForUser(user_id as string, tenant_id as string); + + // Filtrar por action_type se especificado + const filtered = action_type + ? patterns.filter((p) => p.action_type === action_type) + : patterns; + + return res.status(200).json({ + success: true, + patterns: filtered, + count: filtered.length, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em GET patterns:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * POST /api/openclaw/confirm-skill + * + * Usuário confirma uma sugestão de skill + * + * Body: + * { + * suggestion_id: string, + * user_id: string, + * tenant_id: string + * } + */ +router.post("/confirm-skill", async (req: Request, res: Response) => { + try { + const { suggestion_id, user_id, tenant_id } = req.body; + + if (!suggestion_id || !user_id || !tenant_id) { + return res.status(400).json({ + success: false, + error: "Missing required fields: suggestion_id, user_id, tenant_id", + }); + } + + console.log(`[OpenClaw Routes] Confirmação recebida para sugestão: ${suggestion_id}`); + + // Emitir evento de confirmação do usuário + engine.emit("user-confirmed-suggestion", suggestion_id); + + // Aguardar um pouco para skill ser gerada (não ideal, melhorar com callbacks) + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return res.status(200).json({ + success: true, + message: "Sugestão confirmada! Gerando skill...", + suggestion_id, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em confirm-skill:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * GET /api/openclaw/suggestions + * + * Listar sugestões pendentes + * Query params: + * - user_id (optional) - filtrar por usuário + * - status (optional) - 'pending', 'accepted', 'rejected' + */ +router.get("/suggestions", async (req: Request, res: Response) => { + try { + const { user_id, status } = req.query; + + // Buscar sugestões pendentes + const suggestions = engine.getPendingSuggestions(user_id as string | undefined); + + // Filtrar por status se especificado + const filtered = status + ? suggestions.filter((s) => s.status === status) + : suggestions; + + return res.status(200).json({ + success: true, + suggestions: filtered, + count: filtered.length, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em GET suggestions:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * GET /api/openclaw/config + * + * Obter configuração atual do OpenClaw + * (Para admin panel ou debugging) + */ +router.get("/config", async (req: Request, res: Response) => { + try { + const config = engine.getConfig(); + + return res.status(200).json({ + success: true, + config, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em GET config:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * POST /api/openclaw/config + * + * Atualizar configuração do OpenClaw + * (Apenas para admins) + * + * Body: Partial + */ +router.post("/config", async (req: Request, res: Response) => { + try { + // TODO: Validar permissões de admin + + const config = req.body; + + engine.setConfig(config); + + return res.status(200).json({ + success: true, + message: "Configuração atualizada", + config: engine.getConfig(), + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em POST config:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * POST /api/openclaw/check-now + * + * Verificar padrões agora (não aguardar scheduler) + * Query param: tenant_id (required) + */ +router.post("/check-now", async (req: Request, res: Response) => { + try { + const { tenant_id } = req.body; + + if (!tenant_id) { + return res.status(400).json({ + success: false, + error: "Missing required field: tenant_id", + }); + } + + console.log(`[OpenClaw Routes] Verificação manual disparada para tenant: ${tenant_id}`); + + // Disparar verificação (não aguardar) + engine.checkForPatternsScheduled(tenant_id).catch((error) => { + console.error("[OpenClaw Routes] Erro na verificação manual:", error); + }); + + return res.status(200).json({ + success: true, + message: "Verificação de padrões iniciada", + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em check-now:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * POST /api/openclaw/start-scheduler + * + * Iniciar scheduler de verificação periódica + * Query param: tenant_id (required) + */ +router.post("/start-scheduler", async (req: Request, res: Response) => { + try { + const { tenant_id } = req.body; + + if (!tenant_id) { + return res.status(400).json({ + success: false, + error: "Missing required field: tenant_id", + }); + } + + engine.startScheduledChecks(tenant_id); + + return res.status(200).json({ + success: true, + message: `Scheduler iniciado para tenant: ${tenant_id}`, + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em start-scheduler:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * POST /api/openclaw/stop-scheduler + * + * Parar scheduler de verificação periódica + */ +router.post("/stop-scheduler", async (req: Request, res: Response) => { + try { + engine.stopScheduledChecks(); + + return res.status(200).json({ + success: true, + message: "Scheduler interrompido", + }); + } catch (error: any) { + console.error("[OpenClaw Routes] Erro em stop-scheduler:", error); + return res.status(500).json({ + success: false, + error: error.message || "Internal server error", + }); + } +}); + +/** + * GET /api/openclaw/health + * + * Health check do OpenClaw + */ +router.get("/health", (req: Request, res: Response) => { + return res.status(200).json({ + success: true, + status: "healthy", + module: "openclaw", + timestamp: new Date(), + }); +}); + +export default router; diff --git a/server/routes/docs.ts b/server/routes/docs.ts index 1fe3dd9..388bcbd 100644 --- a/server/routes/docs.ts +++ b/server/routes/docs.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { pg } from '../db'; +import { pg } from '../../db'; import { systemDocumentation } from '@shared/schema'; const router = Router(); diff --git a/server/routes/users.ts b/server/routes/users.ts index 02dcab4..7b65165 100644 --- a/server/routes/users.ts +++ b/server/routes/users.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { insertUserSchema } from "@shared/schema"; import { z } from "zod"; -import { db } from "../db"; +import { db } from "../../db"; const router = Router(); diff --git a/server/skills/engine.ts b/server/skills/engine.ts new file mode 100644 index 0000000..a828d52 --- /dev/null +++ b/server/skills/engine.ts @@ -0,0 +1,138 @@ +// server/skills/engine.ts +// Motor de execução de skills — herança, composição, polimorfismo + +import { db } from "../../db"; +import { skills, skillExecutions } from "../../shared/schema"; +import { eq, and } from "drizzle-orm"; +import type { Skill, InsertSkillExecution } from "../../shared/schema"; +import { referenceParser } from "./reference/parser"; +import { referenceResolver, type ResolveContext } from "./reference/resolver"; +import crypto from "crypto"; + +export interface ExecuteOptions { + parameters?: Record; + triggeredBy?: InsertSkillExecution["triggeredBy"]; + triggeredByUserId?: string; + triggeredByAgentId?: string; + ctx: ResolveContext; +} + +export interface ExecuteResult { + success: boolean; + data?: unknown; + error?: string; + durationMs: number; + executionId: string; +} + +export class SkillEngine { + /** Executa uma skill pelo slug dentro de um tenant */ + async execute(slug: string, opts: ExecuteOptions): Promise { + const start = Date.now(); + + // 1. Buscar a skill + const skill = await this.findSkill(slug, opts.ctx); + if (!skill) { + return { success: false, error: `Skill não encontrada: ${slug}`, durationMs: 0, executionId: '' }; + } + + // 2. Criar registro de execução + const [execution] = await db.insert(skillExecutions).values({ + skillId: skill.id, + tenantId: opts.ctx.tenantId, + triggeredBy: opts.triggeredBy ?? 'manual', + triggeredByUserId: opts.triggeredByUserId, + triggeredByAgentId: opts.triggeredByAgentId, + parameters: opts.parameters ?? {}, + status: 'running', + }).returning(); + + try { + // 3. Resolver dependências declaradas no corpo + const resolvedDeps = await this.resolveDependencies(skill, opts.ctx); + + // 4. Montar contexto de execução final + const execCtx: ResolveContext = { + ...opts.ctx, + variables: { + ...(opts.ctx.variables ?? {}), + parameters: opts.parameters ?? {}, + deps: resolvedDeps, + }, + }; + + // 5. Executar — por enquanto retorna o corpo interpolado com o contexto + const result = await this.runBody(skill, execCtx); + + const durationMs = Date.now() - start; + const auditHash = crypto.createHash('sha256').update(JSON.stringify(result)).digest('hex'); + + await db.update(skillExecutions) + .set({ status: 'success', result, durationMs, completedAt: new Date(), auditHash }) + .where(eq(skillExecutions.id, execution.id)); + + return { success: true, data: result, durationMs, executionId: execution.id }; + } catch (err: unknown) { + const error = err instanceof Error ? err.message : String(err); + const durationMs = Date.now() - start; + + await db.update(skillExecutions) + .set({ status: 'failed', error, durationMs, completedAt: new Date() }) + .where(eq(skillExecutions.id, execution.id)); + + return { success: false, error, durationMs, executionId: execution.id }; + } + } + + /** Resolve a cadeia de herança e retorna skill + ancestors mesclados */ + async resolveInheritance(skill: Skill): Promise { + const chain: Skill[] = [skill]; + for (const parentRef of (skill.extends ?? [])) { + const slug = parentRef.replace(/^\/skill(:\w+)?\//, ''); + const parent = await db.select().from(skills) + .where(eq(skills.slug, slug)).limit(1); + if (parent[0]) chain.push(parent[0]); + } + return chain; + } + + private async findSkill(slug: string, ctx: ResolveContext): Promise { + const conditions = [eq(skills.slug, slug), eq(skills.status, 'active')]; + if (ctx.tenantId) conditions.push(eq(skills.tenantId, ctx.tenantId)); + const [skill] = await db.select().from(skills).where(and(...conditions)).limit(1); + return skill ?? null; + } + + private async resolveDependencies(skill: Skill, ctx: ResolveContext): Promise> { + if (!skill.body) return {}; + const refs = referenceParser.parse(skill.body); + const resolved: Record = {}; + for (const ref of refs) { + const key = ref.fullMatch.replace(/\//g, '_').replace(/[^a-z0-9_]/gi, ''); + resolved[key] = await referenceResolver.resolve(ref, ctx); + } + return resolved; + } + + private async runBody(skill: Skill, ctx: ResolveContext): Promise { + // Fase 2: aqui o body Markdown será interpretado passo a passo pelo Manus/LLM + // Por ora retorna o body com variáveis simples substituídas + if (!skill.body) return null; + let output = skill.body; + + // Substituir {{parameters.x}} e {{/var/x}} + const vars = (ctx.variables ?? {}) as Record; + const params = (vars.parameters ?? {}) as Record; + output = output.replace(/\{\{parameters\.([^}]+)\}\}/g, (_, k) => String(params[k] ?? '')); + output = output.replace(/\{\{\/var\/([^}]+)\}\}/g, (_, path) => { + const parts = path.split('.'); + let v: unknown = vars; + for (const p of parts) v = (v as Record)?.[p]; + return String(v ?? ''); + }); + + return { rendered: output, resolvedAt: new Date().toISOString() }; + } +} + +export const skillEngine = new SkillEngine(); diff --git a/server/skills/reference/parser.ts b/server/skills/reference/parser.ts new file mode 100644 index 0000000..cc17cbf --- /dev/null +++ b/server/skills/reference/parser.ts @@ -0,0 +1,51 @@ +// server/skills/reference/parser.ts +// Parser de referências /tipo/caminho usadas no corpo das skills + +export type ReferenceType = 'skill' | 'kg' | 'file' | 'data' | 'var' | 'tool' | 'agent'; +export type ReferenceNamespace = 'system' | 'tenant' | 'company' | 'user'; + +export interface Reference { + type: ReferenceType; + namespace?: ReferenceNamespace; + path: string; + queryParams?: Record; + fullMatch: string; +} + +// Padrão: /skill/slug, /skill:system/slug, /kg/entidade:id/campo?param=val +const PATTERN = + /\/(skill|kg|file|data|var|tool|agent)(?::(system|tenant|company|user))?\/([a-zA-Z0-9_\-./:%]+)(?:\?([^\s`"']+))?/g; + +export class ReferenceParser { + parse(text: string): Reference[] { + const refs: Reference[] = []; + let match: RegExpExecArray | null; + PATTERN.lastIndex = 0; + + while ((match = PATTERN.exec(text)) !== null) { + const [fullMatch, type, namespace, path, queryString] = match; + refs.push({ + type: type as ReferenceType, + namespace: namespace as ReferenceNamespace | undefined, + path, + queryParams: queryString ? parseQS(queryString) : undefined, + fullMatch, + }); + } + + return refs; + } + + /** Extrai apenas os slugs de dependências /skill/... do corpo */ + extractSkillDeps(text: string): string[] { + return this.parse(text) + .filter((r) => r.type === 'skill') + .map((r) => (r.namespace ? `${r.namespace}/${r.path}` : r.path)); + } +} + +function parseQS(qs: string): Record { + return Object.fromEntries(new URLSearchParams(qs)); +} + +export const referenceParser = new ReferenceParser(); diff --git a/server/skills/reference/resolver.ts b/server/skills/reference/resolver.ts new file mode 100644 index 0000000..88798cd --- /dev/null +++ b/server/skills/reference/resolver.ts @@ -0,0 +1,63 @@ +// server/skills/reference/resolver.ts +// Resolve referências /tipo/caminho para seus valores reais em tempo de execução + +import { db } from "../../../db"; +import { skills } from "../../../shared/schema"; +import { eq, and } from "drizzle-orm"; +import type { Reference } from "./parser"; + +export interface ResolveContext { + tenantId?: number; + companyId?: number; + userId?: string; + variables?: Record; + lastResult?: unknown; +} + +export class ReferenceResolver { + async resolve(ref: Reference, ctx: ResolveContext): Promise { + switch (ref.type) { + case 'skill': + return this.resolveSkill(ref, ctx); + case 'var': + return this.resolveVar(ref, ctx); + case 'data': + return this.resolveData(ref, ctx); + default: + // kg, file, tool, agent — implementados nas fases seguintes + return `[${ref.type}:${ref.path}]`; + } + } + + private async resolveSkill(ref: Reference, ctx: ResolveContext) { + const ns = ref.namespace ?? 'tenant'; + const conditions = [ + eq(skills.slug, ref.path), + eq(skills.namespace, ns), + ]; + if (ns === 'tenant' && ctx.tenantId) { + conditions.push(eq(skills.tenantId, ctx.tenantId)); + } + const [skill] = await db.select().from(skills).where(and(...conditions)).limit(1); + return skill ?? null; + } + + private resolveVar(ref: Reference, ctx: ResolveContext): unknown { + if (!ctx.variables) return null; + // Suporta /var/usuario.nome via notação pontilhada + const parts = ref.path.split('.'); + let val: unknown = ctx.variables; + for (const part of parts) { + if (val == null || typeof val !== 'object') return null; + val = (val as Record)[part]; + } + return val; + } + + private resolveData(ref: Reference, ctx: ResolveContext): unknown { + // Placeholder — integração com queries dinâmicas na Fase 2 + return `[data:${ref.path}]`; + } +} + +export const referenceResolver = new ReferenceResolver(); diff --git a/server/skills/routes.ts b/server/skills/routes.ts new file mode 100644 index 0000000..2478abf --- /dev/null +++ b/server/skills/routes.ts @@ -0,0 +1,113 @@ +// server/skills/routes.ts +// CRUD + execução + histórico de skills + +import { Router } from "express"; +import { db } from "../../db"; +import { skills, skillExecutions } from "../../shared/schema"; +import { eq, and, desc } from "drizzle-orm"; +import { insertSkillSchema } from "../../shared/schema"; +import { skillEngine } from "./engine"; + +const router = Router(); + +// ── Listar skills do tenant ──────────────────────────────────────────────── +router.get("/", async (req, res) => { + try { + const tenantId = (req as any).tenantId as number | undefined; + const where = tenantId ? eq(skills.tenantId, tenantId) : undefined; + const rows = await db.select().from(skills).where(where).orderBy(desc(skills.createdAt)); + res.json(rows); + } catch (err) { + res.status(500).json({ error: "Erro ao listar skills" }); + } +}); + +// ── Buscar skill por id ──────────────────────────────────────────────────── +router.get("/:id", async (req, res) => { + try { + const [skill] = await db.select().from(skills).where(eq(skills.id, req.params.id)).limit(1); + if (!skill) return res.status(404).json({ error: "Skill não encontrada" }); + res.json(skill); + } catch (err) { + res.status(500).json({ error: "Erro ao buscar skill" }); + } +}); + +// ── Criar skill ──────────────────────────────────────────────────────────── +router.post("/", async (req, res) => { + try { + const tenantId = (req as any).tenantId as number | undefined; + const parsed = insertSkillSchema.safeParse({ ...req.body, tenantId }); + if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); + + const [skill] = await db.insert(skills).values(parsed.data).returning(); + res.status(201).json(skill); + } catch (err: any) { + if (err?.code === "23505") return res.status(409).json({ error: "Slug já existe neste tenant" }); + res.status(500).json({ error: "Erro ao criar skill" }); + } +}); + +// ── Atualizar skill ──────────────────────────────────────────────────────── +router.put("/:id", async (req, res) => { + try { + const [skill] = await db.update(skills) + .set({ ...req.body, updatedAt: new Date() }) + .where(eq(skills.id, req.params.id)) + .returning(); + if (!skill) return res.status(404).json({ error: "Skill não encontrada" }); + res.json(skill); + } catch (err) { + res.status(500).json({ error: "Erro ao atualizar skill" }); + } +}); + +// ── Deletar (arquivar) skill ─────────────────────────────────────────────── +router.delete("/:id", async (req, res) => { + try { + await db.update(skills) + .set({ status: "archived", updatedAt: new Date() }) + .where(eq(skills.id, req.params.id)); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: "Erro ao arquivar skill" }); + } +}); + +// ── Executar skill ───────────────────────────────────────────────────────── +router.post("/:id/execute", async (req, res) => { + try { + const [skill] = await db.select().from(skills).where(eq(skills.id, req.params.id)).limit(1); + if (!skill) return res.status(404).json({ error: "Skill não encontrada" }); + + const tenantId = (req as any).tenantId as number | undefined; + const userId = (req as any).user?.id as string | undefined; + + const result = await skillEngine.execute(skill.slug, { + parameters: req.body.parameters ?? {}, + triggeredBy: "manual", + triggeredByUserId: userId, + ctx: { tenantId, userId }, + }); + + res.json(result); + } catch (err) { + res.status(500).json({ error: "Erro ao executar skill" }); + } +}); + +// ── Histórico de execuções ───────────────────────────────────────────────── +router.get("/:id/executions", async (req, res) => { + try { + const limit = Math.min(Number(req.query.limit ?? 50), 200); + const rows = await db.select().from(skillExecutions) + .where(eq(skillExecutions.skillId, req.params.id)) + .orderBy(desc(skillExecutions.startedAt)) + .limit(limit); + res.json(rows); + } catch (err) { + res.status(500).json({ error: "Erro ao buscar histórico" }); + } +}); + +export default router; diff --git a/server/soe/erpnext-rule-importer.ts b/server/soe/erpnext-rule-importer.ts index fa10800..4f28da1 100644 --- a/server/soe/erpnext-rule-importer.ts +++ b/server/soe/erpnext-rule-importer.ts @@ -9,7 +9,7 @@ // As regras geradas ficam na tabela soe_regras com origemPadrao=false // (origem: "erpnext") e podem ser sobrescritas por regras custom do tenant. -import { db } from "../db/index"; +import { db } from "../../db/index"; import { soeRegras } from "@shared/schema"; import { and, eq } from "drizzle-orm"; diff --git a/server/soe/event-bus.ts b/server/soe/event-bus.ts index bce482f..d81680d 100644 --- a/server/soe/event-bus.ts +++ b/server/soe/event-bus.ts @@ -2,7 +2,7 @@ // Permite que lançamentos contábeis, alertas e automações // sejam disparados de forma desacoplada após cada operação SOE. -import { db } from "../db/index"; +import { db } from "../../db/index"; import { soeEventos } from "@shared/schema"; import { contabilHandler } from "./handlers/contabil-handler"; import { financialHandler } from "./handlers/financial-handler"; diff --git a/server/soe/handlers/contabil-handler.ts b/server/soe/handlers/contabil-handler.ts index 10cc886..1e29c8a 100644 --- a/server/soe/handlers/contabil-handler.ts +++ b/server/soe/handlers/contabil-handler.ts @@ -3,7 +3,7 @@ // Os lançamentos são salvos em soe_lancamentos e depois sincronizados // com o Motor Contábil Python (:8003). -import { db } from "../../db/index"; +import { db } from "../../../db/index"; import { soeLancamentos } from "@shared/schema"; import type { SoeEventPayload } from "../event-bus"; diff --git a/server/soe/handlers/financial-handler.ts b/server/soe/handlers/financial-handler.ts index f2eeb35..002abff 100644 --- a/server/soe/handlers/financial-handler.ts +++ b/server/soe/handlers/financial-handler.ts @@ -1,7 +1,7 @@ // SOE — Handler Financeiro // Automações financeiras disparadas pelo Event Bus SOE. -import { db } from "../../db/index"; +import { db } from "../../../db/index"; import { finAccountsReceivable, finAccountsPayable } from "@shared/schema"; import type { SoeEventPayload } from "../event-bus"; diff --git a/server/soe/rule-engine/index.ts b/server/soe/rule-engine/index.ts index 4f92e30..7d44c70 100644 --- a/server/soe/rule-engine/index.ts +++ b/server/soe/rule-engine/index.ts @@ -2,7 +2,7 @@ // Intercepta chamadas às rotas SOE antes de despachar ao motor (Plus/ERPNext/local) // e aplica regras configuráveis de negócio, fiscal, contábil e financeiro. -import { db } from "../../db/index"; +import { db } from "../../../db/index"; import { soeRegras, soeEventos } from "@shared/schema"; import { eq, and, isNull, or } from "drizzle-orm"; import type { SoeRule, RuleContext, EnrichedPayload, SoeRuleCondition } from "./types"; diff --git a/server/xos/scheduler.ts b/server/xos/scheduler.ts index 80e35cf..53518d7 100644 --- a/server/xos/scheduler.ts +++ b/server/xos/scheduler.ts @@ -3,7 +3,7 @@ * - Every 5 min: check SLA breaches and emit events * - Every 30 sec: broadcast live supervisor stats via Socket.IO */ -import { db } from "../db"; +import { db } from "../../db"; import { sql } from "drizzle-orm"; import { broadcastXos } from "../socket-io"; diff --git a/shared/schema.ts b/shared/schema.ts index 66183a0..948c699 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -1,5 +1,5 @@ import { sql } from "drizzle-orm"; -import { pgTable, text, varchar, primaryKey, serial, integer, timestamp, numeric, jsonb, boolean, date } from "drizzle-orm/pg-core"; +import { pgTable, text, varchar, primaryKey, serial, integer, timestamp, numeric, jsonb, boolean, date, uuid } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; @@ -7429,3 +7429,87 @@ export type SoeEvento = typeof soeEventos.$inferSelect; export type InsertSoeEvento = z.infer; export type SoeLancamento = typeof soeLancamentos.$inferSelect; export type InsertSoeLancamento = z.infer; + +// ============================================================================= +// AGENTIC SUITE — Skills (POO) +// ============================================================================= + +export const skills = pgTable("skills", { + id: uuid("id").primaryKey().defaultRandom(), + tenantId: integer("tenant_id").references(() => tenants.id), + companyId: integer("company_id"), + userId: varchar("user_id").references(() => users.id), + + // Identidade + name: varchar("name", { length: 255 }).notNull(), + slug: varchar("slug", { length: 255 }).notNull(), + namespace: varchar("namespace", { length: 20 }).notNull().default("tenant"), // system | tenant | company | user + description: text("description"), + version: varchar("version", { length: 20 }).notNull().default("1.0.0"), + tags: text("tags").array().default([]), + + // Herança e composição (referências /skill/...) + extends: text("extends").array().default([]), // skills herdadas + implements: text("implements").array().default([]), // interfaces/contratos + dependencies: jsonb("dependencies").default([]), // referências / resolvidas + + // Visibilidade + executeVisibility: varchar("execute_visibility", { length: 20 }).default("public"), + parametersVisibility: varchar("parameters_visibility", { length: 20 }).default("public"), + + // Trigger + triggerType: varchar("trigger_type", { length: 30 }), // schedule | event | webhook | manual + triggerConfig: jsonb("trigger_config"), + + // Schemas de entrada/saída + parametersSchema: jsonb("parameters_schema").default({}), + returnSchema: jsonb("return_schema").default({}), + + // Corpo da skill (Markdown/YAML com frontmatter) + body: text("body"), + + // Estado + status: varchar("status", { length: 20 }).notNull().default("active"), // draft | active | archived + isSystem: boolean("is_system").default(false), + + // Auditoria + createdBy: varchar("created_by").references(() => users.id), + createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(), +}); + +export const skillExecutions = pgTable("skill_executions", { + id: uuid("id").primaryKey().defaultRandom(), + skillId: uuid("skill_id").notNull().references(() => skills.id, { onDelete: "cascade" }), + tenantId: integer("tenant_id").references(() => tenants.id), + + // Contexto de execução + triggeredBy: varchar("triggered_by", { length: 30 }).notNull().default("manual"), // manual | schedule | event | agent + triggeredByUserId: varchar("triggered_by_user_id").references(() => users.id), + triggeredByAgentId: varchar("triggered_by_agent_id", { length: 100 }), + + // Parâmetros e resultado + parameters: jsonb("parameters").default({}), + result: jsonb("result"), + error: text("error"), + + // Estado e métricas + status: varchar("status", { length: 20 }).notNull().default("pending"), // pending | running | success | failed + durationMs: integer("duration_ms"), + stepsCount: integer("steps_count").default(0), + + // Rastreabilidade + correlationId: uuid("correlation_id").defaultRandom(), + auditHash: varchar("audit_hash", { length: 64 }), // SHA-256 do resultado para imutabilidade + + startedAt: timestamp("started_at").default(sql`CURRENT_TIMESTAMP`).notNull(), + completedAt: timestamp("completed_at"), +}); + +export const insertSkillSchema = createInsertSchema(skills).omit({ id: true, createdAt: true, updatedAt: true }); +export const insertSkillExecutionSchema = createInsertSchema(skillExecutions).omit({ id: true, startedAt: true }); + +export type Skill = typeof skills.$inferSelect; +export type InsertSkill = z.infer; +export type SkillExecution = typeof skillExecutions.$inferSelect; +export type InsertSkillExecution = z.infer;