/** * PatternDetector — OpenClaw Phase 4 * * Analisa skill_executions a cada hora, detecta padrões por (skillId + userId), * e gera DRAFT skills emergentes via llama3.1:8b quando threshold atingido. * * Thresholds: min 3 execuções, janela 30 dias, confiança ≥ 0.80 */ import crypto from "crypto"; import OpenAI from "openai"; import { db } from "../../db/index"; import { skills as arcadiaSkills, skillExecutions, detectedPatterns, skillSuggestions, } from "@shared/schema"; import { eq, and, gte, sql, count } from "drizzle-orm"; const MIN_FREQUENCY = 3; const WINDOW_DAYS = 30; const MIN_CONFIDENCE = 0.80; const CRON_INTERVAL_MS = 60 * 60 * 1000; // 1 hora const openai = new OpenAI({ apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, }); // ─── Análise de padrões ──────────────────────────────────────────────────────── async function detectPatterns(): Promise { const windowStart = new Date(Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000); // Agrupa execuções bem-sucedidas por skillId + userId na janela const grouped = await db .select({ skillId: skillExecutions.skillId, userId: skillExecutions.userId, tenantId: skillExecutions.tenantId, execCount: count(skillExecutions.id), firstSeen: sql`MIN(${skillExecutions.startedAt})`, lastSeen: sql`MAX(${skillExecutions.startedAt})`, }) .from(skillExecutions) .where( and( gte(skillExecutions.startedAt, windowStart), eq(skillExecutions.status, "success") ) ) .groupBy(skillExecutions.skillId, skillExecutions.userId, skillExecutions.tenantId); // Frequência máxima no período (para normalizar confiança) const maxFreq = grouped.reduce((m, r) => Math.max(m, Number(r.execCount)), 1); for (const row of grouped) { const frequency = Number(row.execCount); if (frequency < MIN_FREQUENCY) continue; const confidence = Math.min(frequency / maxFreq, 1); if (confidence < MIN_CONFIDENCE) continue; // Busca skill para obter nome const [skill] = await db .select({ name: arcadiaSkills.name, slug: arcadiaSkills.slug, description: arcadiaSkills.description }) .from(arcadiaSkills) .where(eq(arcadiaSkills.id, row.skillId)) .limit(1); if (!skill) continue; const actionType = `skill:${skill.slug}`; // Upsert em detected_patterns const existing = await db .select({ id: detectedPatterns.id }) .from(detectedPatterns) .where( and( eq(detectedPatterns.actionType, actionType), eq(detectedPatterns.userId, row.userId ?? "") ) ) .limit(1); let patternId: string; if (existing.length > 0) { patternId = existing[0].id; await db .update(detectedPatterns) .set({ frequency, confidence: String(confidence.toFixed(3)), lastSeenAt: row.lastSeen, updatedAt: new Date(), }) .where(eq(detectedPatterns.id, patternId)); } else { const [created] = await db .insert(detectedPatterns) .values({ tenantId: row.tenantId, userId: row.userId, actionType, description: `Skill "${skill.name}" executada repetidamente`, frequency, confidence: String(confidence.toFixed(3)), firstSeenAt: row.firstSeen, lastSeenAt: row.lastSeen, metadata: { skillId: row.skillId, skillName: skill.name }, status: "active", }) .returning({ id: detectedPatterns.id }); patternId = created.id; } // Cria sugestão se ainda não existe sugestão pendente para este padrão const existingSuggestion = await db .select({ id: skillSuggestions.id }) .from(skillSuggestions) .where( and( eq(skillSuggestions.patternId, patternId), eq(skillSuggestions.status, "pending") ) ) .limit(1); if (existingSuggestion.length === 0) { await createEmergentSkillDraft(patternId, skill, frequency, confidence, row.tenantId, row.userId); } } } // ─── Geração de DRAFT via AI ─────────────────────────────────────────────────── async function createEmergentSkillDraft( patternId: string, skill: { name: string; slug: string; description: string | null }, frequency: number, confidence: number, tenantId: number | null, userId: string | null ): Promise { const prompt = `Você é um assistente de automação. Um usuário executou a skill "${skill.name}" (${skill.description ?? "sem descrição"}) ${frequency} vezes nos últimos 30 dias. Gere um corpo (body) em Markdown para uma nova skill chamada "Auto: ${skill.name}" que automatize este fluxo de forma mais inteligente. O body deve usar blocos /skill/${skill.slug} para reutilizar a skill original e adicionar lógica de orquestração. Responda apenas com o body em Markdown, sem explicações.`; let body = `# Auto: ${skill.name}\n\nEsta skill emergiu de ${frequency} execuções detectadas.\n\n\`\`\`\n/skill/${skill.slug}\n\`\`\`\n`; try { const completion = await openai.chat.completions.create({ model: "llama3.1:8b", messages: [{ role: "user", content: prompt }], max_tokens: 512, temperature: 0.4, }); const generated = completion.choices[0]?.message?.content?.trim(); if (generated) body = generated; } catch { // AI indisponível — usa body padrão } // Cria DRAFT skill const draftSlug = `auto-${skill.slug}-${crypto.randomBytes(4).toString("hex")}`; const [draftSkill] = await db .insert(arcadiaSkills) .values({ name: `Auto: ${skill.name}`, slug: draftSlug, description: `Skill emergente detectada automaticamente. Executada ${frequency} vezes com confiança ${(confidence * 100).toFixed(0)}%.`, body, status: "draft", namespace: "tenant", tenantId, userId, tags: ["emergente", "openclaw"], author: "openclaw", }) .returning({ id: arcadiaSkills.id }); // Registra sugestão await db.insert(skillSuggestions).values({ patternId, tenantId, userId, suggestedSkillName: `Auto: ${skill.name}`, suggestedDescription: `Automatização detectada com base em ${frequency} execuções.`, estimatedAutomation: body, confidence: String(confidence.toFixed(3)), generatedSkillId: draftSkill.id, status: "pending", source: "openclaw", }); } // ─── Inicialização do cron ───────────────────────────────────────────────────── let cronHandle: ReturnType | null = null; export function startPatternDetector(): void { if (cronHandle) return; // Executa imediatamente na inicialização, depois a cada hora detectPatterns().catch(console.error); cronHandle = setInterval(() => { detectPatterns().catch(console.error); }, CRON_INTERVAL_MS); console.log("[OpenClaw] PatternDetector iniciado (intervalo: 1h)"); } export function stopPatternDetector(): void { if (cronHandle) { clearInterval(cronHandle); cronHandle = null; } }