arcadiasuite/server/learning/routes.ts

481 lines
16 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { learningService, startPatternDetectionJob, runPatternDetectionNow, startIndexingJob } from "./service";
interface CollectorEvent {
type: string;
module: string;
data: Record<string, any>;
timestamp: number;
sessionId: string;
}
export function registerLearningRoutes(app: Express): void {
app.post("/api/collector/events", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const { events } = req.body as { events: CollectorEvent[] };
if (!events || !Array.isArray(events)) {
return res.status(400).json({ error: "events array is required" });
}
const savedCount = await learningService.saveEvents(userId, events);
console.log(`[Collector] Saved ${savedCount} events for user ${userId}`);
res.json({ success: true, count: savedCount });
} catch (error) {
console.error("Error saving collector events:", error);
res.status(500).json({ error: "Failed to save events" });
}
});
app.get("/api/collector/events", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const limit = parseInt(req.query.limit as string) || 100;
const eventType = req.query.type as string | undefined;
const events = await learningService.getEvents(limit, eventType);
res.json(events);
} catch (error) {
console.error("Error fetching events:", error);
res.status(500).json({ error: "Failed to fetch events" });
}
});
app.get("/api/collector/stats", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const stats = await learningService.getEventStats();
res.json(stats);
} catch (error) {
console.error("Error fetching event stats:", error);
res.status(500).json({ error: "Failed to fetch event stats" });
}
});
app.post("/api/learning/navigation", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const { module, action, metadata } = req.body;
if (!module || !action) {
return res.status(400).json({ error: "module and action are required" });
}
const question = `Navegação: ${action} em ${module}`;
const answer = metadata ? JSON.stringify(metadata) : `Usuário acessou ${module}`;
await learningService.saveInteraction({
userId,
source: 'navigation',
sessionId: `nav_${Date.now()}`,
question,
answer,
toolsUsed: [module],
context: metadata,
});
res.json({ success: true });
} catch (error) {
console.error("Error tracking navigation:", error);
res.status(500).json({ error: "Failed to track navigation" });
}
});
app.get("/api/learning/stats", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const stats = await learningService.getStats();
res.json(stats);
} catch (error) {
console.error("Error fetching learning stats:", error);
res.status(500).json({ error: "Failed to fetch learning stats" });
}
});
app.get("/api/learning/interactions", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const limit = parseInt(req.query.limit as string) || 50;
const interactions = await learningService.getRecentInteractions(limit);
res.json(interactions);
} catch (error) {
console.error("Error fetching interactions:", error);
res.status(500).json({ error: "Failed to fetch interactions" });
}
});
app.get("/api/learning/patterns", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const patterns = await learningService.getActivePatterns();
res.json(patterns);
} catch (error) {
console.error("Error fetching patterns:", error);
res.status(500).json({ error: "Failed to fetch patterns" });
}
});
app.get("/api/learning/codes", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const codeType = req.query.type as string | undefined;
const codes = await learningService.getGeneratedCodes(codeType);
res.json(codes);
} catch (error) {
console.error("Error fetching codes:", error);
res.status(500).json({ error: "Failed to fetch codes" });
}
});
app.post("/api/learning/patterns", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { name, description, patternType, sourceDataset, sourceTable, pattern, confidence } = req.body;
if (!name || !patternType || !pattern) {
return res.status(400).json({ error: "name, patternType, and pattern are required" });
}
const id = await learningService.savePattern({
name,
description,
patternType,
sourceDataset,
sourceTable,
pattern,
confidence,
});
res.status(201).json({ id });
} catch (error) {
console.error("Error saving pattern:", error);
res.status(500).json({ error: "Failed to save pattern" });
}
});
app.post("/api/learning/codes", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { name, description, language, codeType, code, parameters, generatedFrom } = req.body;
if (!name || !codeType || !code) {
return res.status(400).json({ error: "name, codeType, and code are required" });
}
const id = await learningService.saveGeneratedCode({
name,
description,
language,
codeType,
code,
parameters,
generatedFrom,
});
res.status(201).json({ id });
} catch (error) {
console.error("Error saving code:", error);
res.status(500).json({ error: "Failed to save code" });
}
});
app.post("/api/learning/interactions/:id/feedback", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const id = parseInt(req.params.id);
const { feedback } = req.body;
if (!['positive', 'negative', 'neutral'].includes(feedback)) {
return res.status(400).json({ error: "feedback must be 'positive', 'negative', or 'neutral'" });
}
await learningService.updateInteractionFeedback(id, feedback);
res.json({ success: true });
} catch (error) {
console.error("Error updating feedback:", error);
res.status(500).json({ error: "Failed to update feedback" });
}
});
app.post("/api/learning/index", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const result = await learningService.indexInteractionsToChromaDB();
res.json(result);
} catch (error) {
console.error("Error indexing:", error);
res.status(500).json({ error: "Failed to index interactions" });
}
});
app.post("/api/learning/search", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { query } = req.body;
if (!query) {
return res.status(400).json({ error: "query is required" });
}
const results = await learningService.searchSimilarInteractions(query);
res.json(results);
} catch (error) {
console.error("Error searching:", error);
res.status(500).json({ error: "Failed to search interactions" });
}
});
app.post("/api/learning/capture", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const { url, appName, extractRequirements, context } = req.body;
if (!url) {
return res.status(400).json({ error: "url is required" });
}
const { spawn } = await import('child_process');
const path = await import('path');
const scriptPath = path.join(process.cwd(), 'python-service', 'scripts', 'run_capture.py');
const args = [scriptPath, url, '--wait', '2000'];
if (extractRequirements) {
args.push('--extract');
if (context) {
args.push('--context', context);
}
}
const captureResult = await new Promise<any>((resolve, reject) => {
const proc = spawn('python3', args, {
timeout: 45000,
cwd: process.cwd()
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('close', (code) => {
if (code === 0 && stdout) {
try {
resolve(JSON.parse(stdout.trim()));
} catch (e) {
reject(new Error(`Failed to parse output: ${stdout}`));
}
} else {
reject(new Error(stderr || `Process exited with code ${code}`));
}
});
proc.on('error', (err) => reject(err));
setTimeout(() => {
proc.kill();
reject(new Error('Capture timeout (45s)'));
}, 45000);
});
if (!captureResult.success) {
return res.status(500).json(captureResult);
}
const interactionId = await learningService.saveInteraction({
userId,
source: 'web_capture',
sessionId: `capture_${Date.now()}`,
question: `Captura de conhecimento: ${captureResult.title || url}`,
answer: captureResult.text_content?.substring(0, 5000) || '',
toolsUsed: ['web_capture', 'httpx'],
context: {
url,
appName,
title: captureResult.title,
headings: captureResult.headings,
links: captureResult.links?.slice(0, 10),
images: captureResult.images?.slice(0, 10),
meta: captureResult.meta,
extraction: captureResult.extraction,
word_count: captureResult.word_count,
captured_at: captureResult.captured_at
}
});
res.json({
success: true,
interactionId,
title: captureResult.title,
word_count: captureResult.word_count,
headings_count: captureResult.headings?.length || 0,
links_count: captureResult.links?.length || 0,
images_count: captureResult.images?.length || 0,
extraction: captureResult.extraction,
screenshot_base64: captureResult.screenshot_base64
});
} catch (error) {
console.error("Error capturing URL:", error);
res.status(500).json({ error: "Failed to capture URL" });
}
});
app.get("/api/learning/captures", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const captures = await learningService.getRecentInteractions(30);
const webCaptures = captures.filter((c: any) => c.source === 'web_capture');
res.json(webCaptures);
} catch (error) {
console.error("Error fetching captures:", error);
res.status(500).json({ error: "Failed to fetch captures" });
}
});
app.post("/api/learning/learn-url", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const userId = req.user!.id;
const { url, priority } = req.body;
if (!url) {
return res.status(400).json({ error: "url is required" });
}
const { spawn } = await import('child_process');
const path = await import('path');
const captureScript = path.join(process.cwd(), 'python-service', 'scripts', 'run_capture.py');
const proc = spawn('python3', [captureScript, url]);
const captureResult: any = await new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('close', (code) => {
if (code === 0 && stdout) {
try {
resolve(JSON.parse(stdout.trim()));
} catch (e) {
reject(new Error(`Failed to parse output: ${stdout}`));
}
} else {
reject(new Error(stderr || `Process exited with code ${code}`));
}
});
proc.on('error', (err) => reject(err));
setTimeout(() => {
proc.kill();
reject(new Error('Capture timeout (45s)'));
}, 45000);
});
if (!captureResult.success) {
return res.status(500).json(captureResult);
}
const nodeId = `url_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const textContent = captureResult.text_content || '';
try {
const pythonServiceUrl = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
await fetch(`${pythonServiceUrl}/documents/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
doc_id: nodeId,
document: textContent.substring(0, 10000),
metadata: {
type: 'url_learned',
url,
title: captureResult.title,
priority: priority || 'normal',
userId,
capturedAt: new Date().toISOString()
}
})
});
} catch (embeddingError) {
console.warn("Embedding service unavailable, continuing without embeddings:", embeddingError);
}
await learningService.saveInteraction({
userId,
source: 'url_learned',
sessionId: `learn_${nodeId}`,
question: `Aprendizado de URL: ${captureResult.title || url}`,
answer: textContent.substring(0, 5000),
toolsUsed: ['learn_url', 'web_capture'],
context: {
url,
nodeId,
title: captureResult.title,
priority: priority || 'normal',
headings: captureResult.headings?.slice(0, 10),
word_count: captureResult.word_count
}
});
res.json({
success: true,
nodeId,
title: captureResult.title,
contentLength: textContent.length,
priority: priority || 'normal'
});
} catch (error) {
console.error("Error learning URL:", error);
res.status(500).json({ error: "Failed to learn URL" });
}
});
app.post("/api/learning/detect-patterns", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
const result = await runPatternDetectionNow();
res.json({ success: true, patternsDetected: result.detected });
} catch (error) {
console.error("Error running pattern detection:", error);
res.status(500).json({ error: "Failed to run pattern detection" });
}
});
startIndexingJob(60000);
startPatternDetectionJob(300000);
console.log("[Learning] Background jobs started (indexing: 60s, pattern detection: 300s)");
}