224 lines
7.4 KiB
TypeScript
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;
|
|
}
|
|
}
|