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
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Graphify Client
|
||||
* ----------------
|
||||
* Talks to the Graphify MCP HTTP service (Docker container) to:
|
||||
* 1. Build/refresh a knowledge graph from a Git repo (per tenant instance)
|
||||
* 2. Query the graph (codebase search, god nodes, shortest path, etc.)
|
||||
*
|
||||
* Graphify is a local-first knowledge-graph builder for code. It parses
|
||||
* 40+ languages with tree-sitter (deterministic, no LLM) and produces a
|
||||
* traversable graph.json. The MCP HTTP service exposes 7 query tools.
|
||||
*
|
||||
* Config (env):
|
||||
* GRAPHIFY_URL - e.g. http://localhost:8080 (Docker container)
|
||||
* GRAPHIFY_API_KEY - shared secret (--api-key on the Graphify server)
|
||||
*
|
||||
* Per-tenant graphs are stored at: /data/graphify/{instanceId}/graph.json
|
||||
*/
|
||||
|
||||
export interface GraphifyConfig {
|
||||
url: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export interface GraphStats {
|
||||
node_count: number
|
||||
edge_count: number
|
||||
community_count: number
|
||||
god_nodes: number
|
||||
extracted_edges: number
|
||||
inferred_edges: number
|
||||
ambiguous_edges: number
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
id: string
|
||||
label: string
|
||||
type: string
|
||||
file?: string
|
||||
line?: number
|
||||
community?: number
|
||||
is_god_node?: boolean
|
||||
}
|
||||
|
||||
export interface GraphQueryResult {
|
||||
answer: string
|
||||
nodes: GraphNode[]
|
||||
confidence: 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS' | 'NONE'
|
||||
path?: string[]
|
||||
}
|
||||
|
||||
export interface GodNode {
|
||||
id: string
|
||||
label: string
|
||||
type: string
|
||||
in_degree: number
|
||||
out_degree: number
|
||||
file?: string
|
||||
community?: number
|
||||
}
|
||||
|
||||
class GraphifyError extends Error {
|
||||
constructor(message: string, public status: number, public body?: unknown) {
|
||||
super(message)
|
||||
this.name = 'GraphifyError'
|
||||
}
|
||||
}
|
||||
|
||||
export function createGraphifyClient(config: GraphifyConfig) {
|
||||
const baseUrl = config.url.replace(/\/$/, '')
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (config.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${config.apiKey}`
|
||||
}
|
||||
|
||||
// --- MCP JSON-RPC call ---
|
||||
async function mcpCall<T = unknown>(tool: string, args: Record<string, unknown> = {}): Promise<T> {
|
||||
const res = await fetch(`${baseUrl}/mcp`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: { name: tool, arguments: args },
|
||||
id: Date.now(),
|
||||
}),
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: unknown
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null
|
||||
} catch {
|
||||
json = text
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new GraphifyError(
|
||||
`Graphify MCP ${tool} failed: HTTP ${res.status}`,
|
||||
res.status,
|
||||
json
|
||||
)
|
||||
}
|
||||
// MCP response: { result: { content: [{ type: 'text', text: '...' }] } }
|
||||
const result = (json as { result?: { content?: { type: string; text: string }[] } })?.result
|
||||
if (!result) return json as T
|
||||
const textContent = result.content?.find((c) => c.type === 'text')?.text
|
||||
if (!textContent) return json as T
|
||||
try {
|
||||
return JSON.parse(textContent) as T
|
||||
} catch {
|
||||
return textContent as unknown as T
|
||||
}
|
||||
}
|
||||
|
||||
// --- Graph build (CLI invocation via HTTP trigger if supported) ---
|
||||
// Note: Graphify's MCP server is read-only (query tools). To BUILD a graph,
|
||||
// we either:
|
||||
// a) Shell out to `graphify extract` + `graphify build` in a worker process
|
||||
// b) Call a custom build endpoint (if the Docker image exposes one)
|
||||
// Here we provide a high-level function that the provisioning hook calls.
|
||||
// The actual build is done via a sidecar job (see instance provisioning route).
|
||||
|
||||
// --- Query tools (read-only, exposed by Graphify MCP) ---
|
||||
|
||||
async function queryGraph(question: string, graphPath?: string): Promise<GraphQueryResult> {
|
||||
const args: Record<string, unknown> = { question }
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall<GraphQueryResult>('query_graph', args)
|
||||
}
|
||||
|
||||
async function getNode(nodeId: string, graphPath?: string): Promise<GraphNode> {
|
||||
const args: Record<string, unknown> = { node_id: nodeId }
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall<GraphNode>('get_node', args)
|
||||
}
|
||||
|
||||
async function getNeighbors(nodeId: string, graphPath?: string): Promise<GraphNode[]> {
|
||||
const args: Record<string, unknown> = { node_id: nodeId }
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall<GraphNode[]>('get_neighbors', args)
|
||||
}
|
||||
|
||||
async function getCommunity(communityId: number, graphPath?: string): Promise<{
|
||||
id: number
|
||||
label: string
|
||||
nodes: GraphNode[]
|
||||
}> {
|
||||
const args: Record<string, unknown> = { community_id: communityId }
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall('get_community', args)
|
||||
}
|
||||
|
||||
async function getGodNodes(graphPath?: string): Promise<GodNode[]> {
|
||||
const args: Record<string, unknown> = {}
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall<GodNode[]>('god_nodes', args)
|
||||
}
|
||||
|
||||
async function getGraphStats(graphPath?: string): Promise<GraphStats> {
|
||||
const args: Record<string, unknown> = {}
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall<GraphStats>('graph_stats', args)
|
||||
}
|
||||
|
||||
async function shortestPath(sourceId: string, targetId: string, graphPath?: string): Promise<{
|
||||
path: string[]
|
||||
length: number
|
||||
found: boolean
|
||||
}> {
|
||||
const args: Record<string, unknown> = { source: sourceId, target: targetId }
|
||||
if (graphPath) args.graph_path = graphPath
|
||||
return mcpCall('shortest_path', args)
|
||||
}
|
||||
|
||||
// --- Health check ---
|
||||
async function health(): Promise<{ ok: boolean; version?: string }> {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/health`, { headers, signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) return { ok: false }
|
||||
const data = await res.json().catch(() => ({ ok: true }))
|
||||
return { ok: true, version: (data as { version?: string }).version }
|
||||
} catch {
|
||||
return { ok: false }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mcpCall,
|
||||
queryGraph,
|
||||
getNode,
|
||||
getNeighbors,
|
||||
getCommunity,
|
||||
getGodNodes,
|
||||
getGraphStats,
|
||||
shortestPath,
|
||||
health,
|
||||
}
|
||||
}
|
||||
|
||||
export type GraphifyClient = ReturnType<typeof createGraphifyClient>
|
||||
|
||||
export function createGraphifyClientFromEnv(): GraphifyClient | null {
|
||||
const url = process.env.GRAPHIFY_URL
|
||||
if (!url) return null
|
||||
return createGraphifyClient({
|
||||
url,
|
||||
apiKey: process.env.GRAPHIFY_API_KEY,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Helper: graph path per instance ---
|
||||
// Each tenant's graph is stored at /data/graphify/{instanceId}/graph.json
|
||||
export function graphPathForInstance(instanceId: string): string {
|
||||
return `/data/graphify/${instanceId}/graph.json`
|
||||
}
|
||||
|
||||
// --- Helper: build graph from a Git repo ---
|
||||
// This is a high-level function that triggers a graph build via the Graphify
|
||||
// CLI. In production, this would be a background job (e.g. via a queue or
|
||||
// Dokploy cron). Here we provide the command that should be executed.
|
||||
export function buildGraphCommand(input: {
|
||||
gitRepo: string
|
||||
gitBranch: string
|
||||
outputPath: string // /data/graphify/{instanceId}/graph.json
|
||||
}): string {
|
||||
return [
|
||||
`git clone --depth 1 -b ${input.gitBranch} ${input.gitRepo} /tmp/repo`,
|
||||
`&& graphify extract /tmp/repo --output ${input.outputPath}`,
|
||||
`&& graphify build ${input.outputPath} --update`,
|
||||
`&& graphify cluster ${input.outputPath}`,
|
||||
`&& rm -rf /tmp/repo`,
|
||||
].join(' ')
|
||||
}
|
||||
Reference in new issue
Block a user