arcadiasuite/server/graph/service.ts

243 lines
8.3 KiB
TypeScript

import { db } from "../../db/index";
import { eq, desc, and, sql, ilike, or } from "drizzle-orm";
import {
graphNodes,
graphEdges,
knowledgeBase,
learnedInteractions,
insertGraphNodeSchema,
insertGraphEdgeSchema,
insertKnowledgeBaseSchema,
type GraphNode,
type GraphEdge,
type KnowledgeBaseEntry,
type InsertGraphNode,
type InsertGraphEdge,
type InsertKnowledgeBaseEntry,
} from "@shared/schema";
const EMBEDDINGS_URL = process.env.PYTHON_SERVICE_URL || "http://localhost:8001";
// ─── Nodes ────────────────────────────────────────────────────────────────────
export async function getNodes(tenantId?: number, type?: string, limit = 100): Promise<GraphNode[]> {
const conditions = [];
if (tenantId) conditions.push(eq(graphNodes.tenantId, tenantId));
if (type) conditions.push(eq(graphNodes.type, type));
return db
.select()
.from(graphNodes)
.where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(graphNodes.createdAt))
.limit(limit);
}
export async function getNodeById(id: number): Promise<GraphNode | undefined> {
const [node] = await db.select().from(graphNodes).where(eq(graphNodes.id, id));
return node;
}
export async function createNode(data: InsertGraphNode): Promise<GraphNode> {
const [node] = await db.insert(graphNodes).values(data).returning();
// Indexar embedding em background (não bloqueia a resposta)
const content = typeof data.data === "object" ? JSON.stringify(data.data) : String(data.data);
indexNodeEmbedding(node.id, content, data.type).catch(() => {});
return node;
}
export async function updateNode(id: number, data: Partial<InsertGraphNode>): Promise<GraphNode | undefined> {
const [node] = await db
.update(graphNodes)
.set({ ...data, updatedAt: new Date() })
.where(eq(graphNodes.id, id))
.returning();
return node;
}
export async function deleteNode(id: number): Promise<boolean> {
const result = await db.delete(graphNodes).where(eq(graphNodes.id, id));
return (result.rowCount ?? 0) > 0;
}
// ─── Edges ────────────────────────────────────────────────────────────────────
export async function getEdges(sourceId?: number, targetId?: number): Promise<GraphEdge[]> {
const conditions = [];
if (sourceId) conditions.push(eq(graphEdges.sourceId, sourceId));
if (targetId) conditions.push(eq(graphEdges.targetId, targetId));
return db
.select()
.from(graphEdges)
.where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(graphEdges.createdAt));
}
export async function createEdge(data: InsertGraphEdge): Promise<GraphEdge> {
const [edge] = await db.insert(graphEdges).values(data).returning();
return edge;
}
export async function deleteEdge(id: number): Promise<boolean> {
const result = await db.delete(graphEdges).where(eq(graphEdges.id, id));
return (result.rowCount ?? 0) > 0;
}
// ─── Knowledge Base ───────────────────────────────────────────────────────────
export async function getKnowledgeEntries(category?: string, search?: string): Promise<KnowledgeBaseEntry[]> {
const conditions = [];
if (category) conditions.push(eq(knowledgeBase.category, category));
if (search) {
conditions.push(
or(
ilike(knowledgeBase.title, `%${search}%`),
ilike(knowledgeBase.content, `%${search}%`)
)!
);
}
return db
.select()
.from(knowledgeBase)
.where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(knowledgeBase.createdAt))
.limit(50);
}
export async function getKnowledgeEntry(id: number): Promise<KnowledgeBaseEntry | undefined> {
const [entry] = await db.select().from(knowledgeBase).where(eq(knowledgeBase.id, id));
return entry;
}
export async function createKnowledgeEntry(data: InsertKnowledgeBaseEntry): Promise<KnowledgeBaseEntry> {
const [entry] = await db.insert(knowledgeBase).values(data).returning();
// Indexar no serviço de embeddings
indexKnowledgeEmbedding(entry.id, entry.title + "\n" + entry.content, entry.category).catch(() => {});
return entry;
}
export async function updateKnowledgeEntry(
id: number,
data: Partial<InsertKnowledgeBaseEntry>
): Promise<KnowledgeBaseEntry | undefined> {
const [entry] = await db.update(knowledgeBase).set(data).where(eq(knowledgeBase.id, id)).returning();
return entry;
}
export async function deleteKnowledgeEntry(id: number): Promise<boolean> {
const result = await db.delete(knowledgeBase).where(eq(knowledgeBase.id, id));
return (result.rowCount ?? 0) > 0;
}
// ─── Busca Semântica ──────────────────────────────────────────────────────────
export async function semanticSearch(
query: string,
nResults = 5
): Promise<{ results: any[]; source: "embeddings" | "text_fallback" }> {
// Tenta busca vetorial no serviço de embeddings Python
try {
const response = await fetch(`${EMBEDDINGS_URL}/embeddings/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, n_results: nResults }),
signal: AbortSignal.timeout(5000),
});
if (response.ok) {
const data = await response.json();
return { results: data.results || [], source: "embeddings" };
}
} catch {
// Serviço de embeddings indisponível — fallback para busca textual
}
// Fallback: busca textual simples no banco
const textResults = await db
.select()
.from(knowledgeBase)
.where(
or(
ilike(knowledgeBase.title, `%${query}%`),
ilike(knowledgeBase.content, `%${query}%`)
)!
)
.limit(nResults);
// Complementa com interações aprendidas
const interactionResults = await db
.select()
.from(learnedInteractions)
.where(
or(
ilike(learnedInteractions.question, `%${query}%`),
ilike(learnedInteractions.answer, `%${query}%`)
)!
)
.orderBy(desc(learnedInteractions.createdAt))
.limit(nResults);
return {
results: [
...textResults.map((r) => ({ type: "knowledge", score: 0.7, data: r })),
...interactionResults.map((r) => ({ type: "interaction", score: 0.6, data: r })),
],
source: "text_fallback",
};
}
// ─── Grafo Completo para Visualização ────────────────────────────────────────
export async function getGraphData(tenantId?: number): Promise<{ nodes: GraphNode[]; edges: GraphEdge[] }> {
const nodes = await getNodes(tenantId, undefined, 200);
const nodeIds = nodes.map((n) => n.id);
if (nodeIds.length === 0) return { nodes: [], edges: [] };
const edges = await db
.select()
.from(graphEdges)
.where(
or(
sql`${graphEdges.sourceId} = ANY(${sql.raw(`ARRAY[${nodeIds.join(",")}]`)})`,
sql`${graphEdges.targetId} = ANY(${sql.raw(`ARRAY[${nodeIds.join(",")}]`)})`
)!
);
return { nodes, edges };
}
// ─── Helpers Privados ────────────────────────────────────────────────────────
async function indexNodeEmbedding(nodeId: number, content: string, type: string) {
await fetch(`${EMBEDDINGS_URL}/embeddings/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
doc_id: `node_${nodeId}`,
document: content,
metadata: { type: "graph_node", node_type: type, node_id: nodeId },
}),
signal: AbortSignal.timeout(10000),
});
}
async function indexKnowledgeEmbedding(entryId: number, content: string, category: string) {
await fetch(`${EMBEDDINGS_URL}/embeddings/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
doc_id: `kb_${entryId}`,
document: content,
metadata: { type: "knowledge_base", category, entry_id: entryId },
}),
signal: AbortSignal.timeout(10000),
});
}