arcadiasuite/server/openclaw/pattern-detector.ts

224 lines
7.4 KiB
TypeScript

/**
* 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 {
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<void> {
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<Date>`MIN(${skillExecutions.startedAt})`,
lastSeen: sql<Date>`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<void> {
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<typeof setInterval> | 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;
}
}