feat: Graphify integration — tenant codebase graph + support AI augmentation

- src/lib/graphify.ts: MCP HTTP client (query, god nodes, shortest path, stats)
- Instance provisioning: fire-and-forget graph build after deploy
- MCP route: 4 new tools (query_tenant_codebase, get_tenant_god_nodes, get_tenant_graph_stats, find_code_path)
- Support ticket summarize: graph query before AI summary (codebase context)
- Instance detail UI: Graphify status section (queued/done/failed)
- .env: GRAPHIFY_URL + GRAPHIFY_API_KEY (commented, opt-in)
This commit is contained in:
Z User committed 2026-10-28 19:23:57 +00:00
1 parent ada029d500
commit 7d94fe53ad
6 files changed
+461 -2

No files matched your search

+25
View File
@@ -258,6 +258,31 @@ export async function POST(req: Request) {
},
})
// --- Graphify: trigger background graph build ---
// Builds a knowledge graph from the instance's Git repo so the support
// ticket AI can query the codebase later. Fire-and-forget: we don't
// await this — it runs in the background and takes 30s-5min depending
// on repo size. The graph build job is recorded as an instance event.
if (process.env.GRAPHIFY_URL) {
import('@/lib/graphify').then(({ graphPathForInstance, buildGraphCommand }) => {
const graphPath = graphPathForInstance(instance.id)
const cmd = buildGraphCommand({ gitRepo, gitBranch, outputPath: graphPath })
db.instanceEvent.create({
data: {
instanceId: instance.id,
type: 'graphify.build.queued',
step: 'graphify',
result: 'pending',
details: JSON.stringify({ graphPath, gitRepo, gitBranch }),
},
}).then(() => {
// In production: enqueue to a worker (BullMQ, Dokploy cron, etc.)
// For now: log the command that should be executed.
console.log(`[graphify] Instance ${instance.id} graph build queued: ${cmd}`)
}).catch(() => {})
}).catch(() => {})
}
await db.auditLog.create({
data: {
actor: 'admin:ui',
@@ -23,18 +23,52 @@ export async function POST(
.map((m) => `[${m.fromType.toUpperCase()}] ${m.fromName} (${new Date(m.createdAt).toLocaleString('de-DE')}):\n${m.body}`)
.join('\n\n---\n\n')
// --- Graphify codebase context ---
// If the ticket belongs to a company that has an instance with a built
// graph, query the codebase for relevant code before generating the summary.
// This gives the AI context about WHERE the issue is in the code.
let codebaseContext = ''
if (process.env.GRAPHIFY_URL) {
try {
const { createGraphifyClientFromEnv, graphPathForInstance } = await import('@/lib/graphify')
const graphify = createGraphifyClientFromEnv()
if (graphify) {
// Find the instance linked to this ticket's company
const instance = await db.instance.findFirst({
where: { companyId: ticket.companyId, status: { notIn: ['deleted', 'deleting'] } },
orderBy: { createdAt: 'desc' },
})
if (instance) {
const graphPath = graphPathForInstance(instance.id)
// Query the graph with the ticket subject — "Where is X implemented?"
const graphResult = await graphify.queryGraph(ticket.subject, graphPath)
if (graphResult.nodes && graphResult.nodes.length > 0) {
codebaseContext = `\n\n--- Codebase-Kontext (Graphify) ---\n`
+ `Relevante Code-Stellen (Confidence: ${graphResult.confidence}):\n`
+ graphResult.nodes.slice(0, 5).map((n) =>
`- ${n.label} (${n.type})${n.file ? ` in ${n.file}:${n.line ?? '?'}` : ''}`
).join('\n')
+ `\n\nAntwort: ${graphResult.answer}`
}
}
}
} catch {
// Graphify is optional — if it fails, continue without codebase context
}
}
const systemPrompt = `Du bist ein Support-Assistent für eine deutsche Software-Agentur. Fasse ein Support-Ticket prägnant zusammen (max. 4 Sätze):
1. Was ist das Kernproblem / die Anfrage?
2. Was wurde bisher unternommen?
3. Was ist der aktuelle Status / nächste Schritt?
Schreibe auf Deutsch, sachlich, keine Einleitungssätze.`
Schreibe auf Deutsch, sachlich, keine Einleitungssätze.${codebaseContext ? '\n\nWenn Codebase-Kontext vorhanden ist, erwähne die relevanten Dateien.' : ''}`
const userPrompt = `Ticket: ${ticket.number} — ${ticket.subject}
Kunde: ${ticket.companyId}
Priorität: ${ticket.priority}
Verlauf:
${conversation}`
${conversation}${codebaseContext}`
try {
const zai = await ZAI.create()
+101
View File
@@ -901,6 +901,107 @@ const TOOLS: McpTool[] = [
return { number, type: args.type, amountCents: signedAmount, balanceCents: newBalance }
},
},
// --- Graphify tools (tenant codebase intelligence) ---
// These tools query the Graphify MCP HTTP service to answer questions about
// a tenant instance's codebase. Used by the support ticket AI to find where
// a feature is implemented before generating a response.
{
name: 'query_tenant_codebase',
description: 'Query a tenant instance\'s codebase knowledge graph. Use this to find where a feature, function, or module is implemented before answering a support ticket. Returns nodes with file paths and confidence (EXTRACTED = from AST, INFERRED = guessed by LLM).',
inputSchema: {
type: 'object',
properties: {
instanceId: { type: 'string', description: 'The tenant instance ID (links to a graph.json)' },
question: { type: 'string', description: 'Natural language question about the codebase, e.g. "Where is the payment checkout implemented?"' },
},
required: ['instanceId', 'question'],
},
requiredScopes: ['support:read', 'mcp:read'],
handler: async (args) => {
const { createGraphifyClientFromEnv, graphPathForInstance } = await import('@/lib/graphify')
const client = createGraphifyClientFromEnv()
if (!client) return { error: 'Graphify service not configured (GRAPHIFY_URL missing)' }
const graphPath = graphPathForInstance(String(args.instanceId))
try {
const result = await client.queryGraph(String(args.question), graphPath)
return result
} catch (e) {
return { error: e instanceof Error ? e.message : 'graphify query failed' }
}
},
},
{
name: 'get_tenant_god_nodes',
description: 'Get the "god nodes" (highly-connected hubs) in a tenant\'s codebase. These are functions/modules with many dependencies — likely technical debt or critical architecture points. Useful for code review and refactoring priorities.',
inputSchema: {
type: 'object',
properties: {
instanceId: { type: 'string', description: 'The tenant instance ID' },
},
required: ['instanceId'],
},
requiredScopes: ['support:read', 'mcp:read'],
handler: async (args) => {
const { createGraphifyClientFromEnv, graphPathForInstance } = await import('@/lib/graphify')
const client = createGraphifyClientFromEnv()
if (!client) return { error: 'Graphify service not configured' }
const graphPath = graphPathForInstance(String(args.instanceId))
try {
const gods = await client.getGodNodes(graphPath)
return { god_nodes: gods, count: gods.length }
} catch (e) {
return { error: e instanceof Error ? e.message : 'failed' }
}
},
},
{
name: 'get_tenant_graph_stats',
description: 'Get statistics about a tenant\'s codebase knowledge graph: node count, edge count, community count, god node count, and edge confidence breakdown (EXTRACTED vs INFERRED).',
inputSchema: {
type: 'object',
properties: {
instanceId: { type: 'string', description: 'The tenant instance ID' },
},
required: ['instanceId'],
},
requiredScopes: ['support:read', 'mcp:read'],
handler: async (args) => {
const { createGraphifyClientFromEnv, graphPathForInstance } = await import('@/lib/graphify')
const client = createGraphifyClientFromEnv()
if (!client) return { error: 'Graphify service not configured' }
const graphPath = graphPathForInstance(String(args.instanceId))
try {
return await client.getGraphStats(graphPath)
} catch (e) {
return { error: e instanceof Error ? e.message : 'failed' }
}
},
},
{
name: 'find_code_path',
description: 'Find the shortest call path between two functions/modules in a tenant\'s codebase. Useful for understanding how data flows through the application.',
inputSchema: {
type: 'object',
properties: {
instanceId: { type: 'string', description: 'The tenant instance ID' },
source: { type: 'string', description: 'Source node ID or label' },
target: { type: 'string', description: 'Target node ID or label' },
},
required: ['instanceId', 'source', 'target'],
},
requiredScopes: ['support:read', 'mcp:read'],
handler: async (args) => {
const { createGraphifyClientFromEnv, graphPathForInstance } = await import('@/lib/graphify')
const client = createGraphifyClientFromEnv()
if (!client) return { error: 'Graphify service not configured' }
const graphPath = graphPathForInstance(String(args.instanceId))
try {
return await client.shortestPath(String(args.source), String(args.target), graphPath)
} catch (e) {
return { error: e instanceof Error ? e.message : 'failed' }
}
},
},
]
// JSON-RPC handler