mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
feat(web): wire agent/chat through HTTP in backend mode
Add HTTP wrapper functions (executeQuery, textSearch) to the ingestion worker and a new initializeBackendAgent method that creates the agent with HTTP-backed tool closures instead of local KuzuDB. useAppState detects backend mode and routes agent initialization accordingly.
This commit is contained in:
parent
56f92ca1ad
commit
8dd1c19bec
4 changed files with 148 additions and 14 deletions
|
|
@ -32,7 +32,7 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
|
|||
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser |
|
||||
| **For** | Daily development with Cursor, Claude Code, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
|
||||
| **Scale** | Full repos, any size | Limited by browser memory (~5k files) |
|
||||
| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode |
|
||||
| **Install** | `npm install -g gitnexus` | No install —[gitnexus.vercel.app](https://gitnexus.vercel.app) |
|
||||
| **Storage** | KuzuDB native (fast, persistent) | KuzuDB WASM (in-memory, per session) |
|
||||
| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM |
|
||||
|
|
@ -235,7 +235,7 @@ npm run dev
|
|||
|
||||
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, KuzuDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
|
||||
|
||||
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos. No need to re-upload or re-index.
|
||||
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { LLMSettings, ProviderConfig, AgentStreamChunk, ChatMessage, ToolCa
|
|||
import { loadSettings, getActiveProviderConfig, saveSettings } from '../core/llm/settings-service';
|
||||
import type { AgentMessage } from '../core/llm/agent';
|
||||
import { DEFAULT_VISIBLE_EDGES, type EdgeType } from '../lib/constants';
|
||||
import { runCypherQuery } from '../services/backend';
|
||||
import { runCypherQuery, getBackendUrl } from '../services/backend';
|
||||
|
||||
export type ViewMode = 'onboarding' | 'loading' | 'exploring';
|
||||
export type RightPanelTab = 'code' | 'chat';
|
||||
|
|
@ -571,25 +571,39 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
}, []);
|
||||
|
||||
const initializeAgent = useCallback(async (overrideProjectName?: string): Promise<void> => {
|
||||
const api = apiRef.current;
|
||||
if (!api) {
|
||||
setAgentError('Worker not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getActiveProviderConfig();
|
||||
if (!config) {
|
||||
setAgentError('Please configure an LLM provider in settings');
|
||||
return;
|
||||
}
|
||||
|
||||
const api = apiRef.current;
|
||||
if (!api) {
|
||||
setAgentError('Worker not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAgentInitializing(true);
|
||||
setAgentError(null);
|
||||
|
||||
try {
|
||||
// Use override if provided (for fresh loads), fallback to state (for re-init)
|
||||
const effectiveProjectName = overrideProjectName || projectName || 'project';
|
||||
const result = await api.initializeAgent(config, effectiveProjectName);
|
||||
let result: { success: boolean; error?: string };
|
||||
|
||||
if (isBackendMode && backendRepo) {
|
||||
// Backend mode: pass HTTP config + file contents to worker
|
||||
const entries = Array.from(fileContents.entries());
|
||||
result = await api.initializeBackendAgent(
|
||||
config,
|
||||
getBackendUrl(),
|
||||
backendRepo,
|
||||
entries,
|
||||
effectiveProjectName,
|
||||
);
|
||||
} else {
|
||||
result = await api.initializeAgent(config, effectiveProjectName);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
setIsAgentReady(true);
|
||||
setAgentError(null);
|
||||
|
|
@ -607,7 +621,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
} finally {
|
||||
setIsAgentInitializing(false);
|
||||
}
|
||||
}, [projectName]);
|
||||
}, [projectName, isBackendMode, backendRepo, fileContents]);
|
||||
|
||||
const sendChatMessage = useCallback(async (message: string): Promise<void> => {
|
||||
const api = apiRef.current;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { SystemMessage } from '@langchain/core/messages';
|
|||
import { enrichClustersBatch, ClusterMemberInfo, ClusterEnrichment } from '../core/ingestion/cluster-enricher';
|
||||
import { CommunityNode } from '../core/ingestion/community-processor';
|
||||
import { PipelineResult } from '../types/pipeline';
|
||||
import { buildCodebaseContext } from '../core/llm/context-builder';
|
||||
import { buildCodebaseContext, type CodebaseContext } from '../core/llm/context-builder';
|
||||
import {
|
||||
buildBM25Index,
|
||||
searchBM25,
|
||||
|
|
@ -54,6 +54,65 @@ let enrichmentCancelled = false;
|
|||
// Chat cancellation flag
|
||||
let chatCancelled = false;
|
||||
|
||||
// ============================================================
|
||||
// HTTP helpers for backend mode
|
||||
// ============================================================
|
||||
|
||||
const httpFetchWithTimeout = async (
|
||||
url: string,
|
||||
init: RequestInit = {},
|
||||
timeoutMs: number = 30_000,
|
||||
): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { ...init, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
const createHttpExecuteQuery = (backendUrl: string, repo: string) => {
|
||||
return async (cypher: string): Promise<any[]> => {
|
||||
const response = await httpFetchWithTimeout(`${backendUrl}/api/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cypher, repo }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Backend query failed: ${response.status}`);
|
||||
}
|
||||
const body = await response.json();
|
||||
return (body.result ?? body) as any[];
|
||||
};
|
||||
};
|
||||
|
||||
const createHttpTextSearch = (executeQuery: (cypher: string) => Promise<any[]>) => {
|
||||
return async (query: string, k: number = 15): Promise<any[]> => {
|
||||
const sanitized = query.replace(/'/g, "\\'");
|
||||
const cypher = `
|
||||
MATCH (n)
|
||||
WHERE n.name IS NOT NULL AND toLower(n.name) CONTAINS toLower('${sanitized}')
|
||||
RETURN n.id AS id, label(n) AS type, n.name AS name, n.filePath AS filePath, n.content AS content
|
||||
LIMIT ${k}
|
||||
`;
|
||||
try {
|
||||
const results = await executeQuery(cypher);
|
||||
return results.map((r: any, i: number) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
type: r.type,
|
||||
filePath: r.filePath,
|
||||
content: r.content ?? '',
|
||||
score: 1 - (i * 0.05),
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Worker API exposed via Comlink
|
||||
*
|
||||
|
|
@ -540,6 +599,67 @@ const workerApi = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the Graph RAG agent in backend mode (HTTP-backed tools).
|
||||
* Uses HTTP wrappers instead of local KuzuDB for all tool queries.
|
||||
* @param config - Provider configuration for the LLM
|
||||
* @param backendUrl - Base URL of the gitnexus serve backend
|
||||
* @param repoName - Repository name on the backend
|
||||
* @param fileContentsEntries - File contents as [path, content][] (Comlink can't transfer Maps)
|
||||
* @param projectName - Display name for the project
|
||||
*/
|
||||
async initializeBackendAgent(
|
||||
config: ProviderConfig,
|
||||
backendUrl: string,
|
||||
repoName: string,
|
||||
fileContentsEntries: [string, string][],
|
||||
projectName?: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// Rebuild Map from serializable entries (Comlink can't transfer Maps)
|
||||
const contents = new Map<string, string>(fileContentsEntries);
|
||||
storedFileContents = contents;
|
||||
|
||||
// Create HTTP-based tool wrappers
|
||||
const executeQuery = createHttpExecuteQuery(backendUrl, repoName);
|
||||
const textSearch = createHttpTextSearch(executeQuery);
|
||||
|
||||
// Build codebase context (uses Cypher queries — works via HTTP)
|
||||
let codebaseContext: CodebaseContext | undefined;
|
||||
try {
|
||||
codebaseContext = await buildCodebaseContext(executeQuery, projectName || repoName);
|
||||
} catch {
|
||||
// Non-fatal — agent works without context
|
||||
}
|
||||
|
||||
// Create agent with HTTP-backed tools
|
||||
currentAgent = createGraphRAGAgent(
|
||||
config,
|
||||
executeQuery, // Cypher via HTTP
|
||||
textSearch, // semanticSearch → Cypher text search
|
||||
textSearch, // semanticSearchWithContext → same
|
||||
textSearch, // hybridSearch → same
|
||||
() => true, // isEmbeddingReady → always true (routes to textSearch)
|
||||
() => true, // isBM25Ready → always true (routes to textSearch)
|
||||
contents, // fileContents Map
|
||||
codebaseContext,
|
||||
);
|
||||
|
||||
currentProviderConfig = config;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🤖 Backend agent initialized with provider:', config.provider);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('❌ Backend agent initialization failed:', err);
|
||||
}
|
||||
return { success: false, error: err.message || 'Failed to initialize backend agent' };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the agent is initialized
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setu
|
|||
|
||||
GitNexus also has a browser-based UI at [gitnexus.vercel.app](https://gitnexus.vercel.app) — 100% client-side, your code never leaves the browser.
|
||||
|
||||
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos. No need to re-upload or re-index.
|
||||
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue