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:
1 parent
ada029d500
commit
7d94fe53ad
6 files changed
+461
-2
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user