mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
Merge pull request #18 from abhigyanpatwari/Schema_Experiments
Agent UI improved. Prompt injected with repo directory structure, hot…
This commit is contained in:
commit
16c50deea1
5 changed files with 639 additions and 241 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
import {
|
||||
Send, Sparkles, User,
|
||||
PanelRightClose, Loader2, Settings, AlertTriangle
|
||||
PanelRightClose, Loader2, Settings, AlertTriangle
|
||||
} from 'lucide-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
|
@ -29,7 +29,7 @@ const customTheme = {
|
|||
};
|
||||
|
||||
export const RightPanel = () => {
|
||||
const {
|
||||
const {
|
||||
isRightPanelOpen,
|
||||
setRightPanelOpen,
|
||||
fileContents,
|
||||
|
|
@ -46,7 +46,7 @@ export const RightPanel = () => {
|
|||
sendChatMessage,
|
||||
clearChat,
|
||||
} = useAppState();
|
||||
|
||||
|
||||
const [chatInput, setChatInput] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
|
|
@ -177,12 +177,12 @@ export const RightPanel = () => {
|
|||
|
||||
return null;
|
||||
}, [extractTextFromChildren, isLikelyFileRefHref]);
|
||||
|
||||
|
||||
// Auto-resize textarea as user types
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
|
||||
// Reset height to get accurate scrollHeight
|
||||
textarea.style.height = 'auto';
|
||||
// Set to scrollHeight, capped at max
|
||||
|
|
@ -192,7 +192,7 @@ export const RightPanel = () => {
|
|||
// Show scrollbar if content exceeds max
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
||||
}, []);
|
||||
|
||||
|
||||
// Adjust height when input changes
|
||||
useEffect(() => {
|
||||
adjustTextareaHeight();
|
||||
|
|
@ -235,7 +235,7 @@ export const RightPanel = () => {
|
|||
<span className="font-medium text-sm">Nexus AI</span>
|
||||
<span className="text-xs text-text-muted">• Ask about the codebase</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setRightPanelOpen(false)}
|
||||
|
|
@ -271,87 +271,79 @@ export const RightPanel = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status / errors */}
|
||||
{agentError && (
|
||||
<div className="px-4 py-3 bg-rose-500/10 border-b border-rose-500/30 text-rose-100 text-sm flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>{agentError}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Status / errors */}
|
||||
{agentError && (
|
||||
<div className="px-4 py-3 bg-rose-500/10 border-b border-rose-500/30 text-rose-100 text-sm flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span>{agentError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active tool calls - shown at top during execution */}
|
||||
{currentToolCalls.length > 0 && (
|
||||
<div className="px-4 py-3 bg-elevated/60 border-b border-border-subtle space-y-2">
|
||||
<div className="text-[10px] uppercase tracking-wider text-text-muted flex items-center gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span>Working...</span>
|
||||
</div>
|
||||
{currentToolCalls.map(tc => (
|
||||
<ToolCallCard key={tc.id} toolCall={tc} defaultExpanded={false} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
||||
<div className="w-14 h-14 mb-4 flex items-center justify-center bg-gradient-to-br from-accent to-node-interface rounded-xl shadow-glow text-2xl">
|
||||
🧠
|
||||
</div>
|
||||
<h3 className="text-base font-medium mb-2">
|
||||
Ask me anything
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary leading-relaxed mb-5">
|
||||
I can help you understand the architecture, find functions, or explain connections.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{chatSuggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
onClick={() => setChatInput(suggestion)}
|
||||
className="px-3 py-1.5 bg-elevated border border-border-subtle rounded-full text-xs text-text-secondary hover:border-accent hover:text-text-primary transition-colors"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
||||
<div className="w-14 h-14 mb-4 flex items-center justify-center bg-gradient-to-br from-accent to-node-interface rounded-xl shadow-glow text-2xl">
|
||||
🧠
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
{chatMessages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${message.role === 'user' ? 'flex-row-reverse' : ''} animate-fade-in`}
|
||||
<h3 className="text-base font-medium mb-2">
|
||||
Ask me anything
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary leading-relaxed mb-5">
|
||||
I can help you understand the architecture, find functions, or explain connections.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{chatSuggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
onClick={() => setChatInput(suggestion)}
|
||||
className="px-3 py-1.5 bg-elevated border border-border-subtle rounded-full text-xs text-text-secondary hover:border-accent hover:text-text-primary transition-colors"
|
||||
>
|
||||
<div className={`
|
||||
w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-md text-sm
|
||||
${message.role === 'assistant'
|
||||
? 'bg-gradient-to-br from-accent to-node-interface text-white'
|
||||
: 'bg-elevated border border-border-subtle text-text-secondary'
|
||||
}
|
||||
`}>
|
||||
{message.role === 'assistant' ? (
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<User className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
{chatMessages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className="animate-fade-in"
|
||||
>
|
||||
{/* User message - compact label style */}
|
||||
{message.role === 'user' && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User className="w-4 h-4 text-text-muted" />
|
||||
<span className="text-xs font-medium text-text-muted uppercase tracking-wide">You</span>
|
||||
</div>
|
||||
<div className="pl-6 text-sm text-text-primary">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`
|
||||
max-w-[90%] rounded-xl
|
||||
${message.role === 'assistant'
|
||||
? 'px-4 py-3 bg-elevated border border-border-subtle text-text-primary chat-prose'
|
||||
: 'px-3.5 py-2.5 bg-accent text-white text-sm'
|
||||
}
|
||||
`}>
|
||||
{message.role === 'assistant' ? (
|
||||
// Render steps in order (reasoning, tool calls, content interleaved)
|
||||
message.steps && message.steps.length > 0 ? (
|
||||
)}
|
||||
|
||||
{/* Assistant message - copilot style */}
|
||||
{message.role === 'assistant' && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles className="w-4 h-4 text-accent" />
|
||||
<span className="text-xs font-medium text-text-muted uppercase tracking-wide">Nexus AI</span>
|
||||
{isChatLoading && message === chatMessages[chatMessages.length - 1] && (
|
||||
<Loader2 className="w-3 h-3 animate-spin text-accent" />
|
||||
)}
|
||||
</div>
|
||||
<div className="pl-6 chat-prose">
|
||||
{/* Render steps in order (reasoning, tool calls, content interleaved) */}
|
||||
{message.steps && message.steps.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{message.steps.map((step) => (
|
||||
<div key={step.id}>
|
||||
{step.type === 'reasoning' && step.content && (
|
||||
<div className="text-text-primary text-sm chat-prose">
|
||||
<div className="text-text-secondary text-sm italic border-l-2 border-text-muted/30 pl-3 mb-3">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => {
|
||||
|
|
@ -439,100 +431,104 @@ export const RightPanel = () => {
|
|||
</div>
|
||||
)}
|
||||
{step.type === 'tool_call' && step.toolCall && (
|
||||
<ToolCallCard toolCall={step.toolCall} defaultExpanded={false} />
|
||||
<div className="mb-3">
|
||||
<ToolCallCard toolCall={step.toolCall} defaultExpanded={false} />
|
||||
</div>
|
||||
)}
|
||||
{step.type === 'content' && step.content && (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => {
|
||||
if (href && href.startsWith('code-ref:')) {
|
||||
const inner = decodeURIComponent(href.slice('code-ref:'.length));
|
||||
const label = formatRefChipLabel(inner);
|
||||
<div className="text-text-primary text-sm">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => {
|
||||
if (href && href.startsWith('code-ref:')) {
|
||||
const inner = decodeURIComponent(href.slice('code-ref:'.length));
|
||||
const label = formatRefChipLabel(inner);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(inner);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${inner}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const internalRef = getInternalRefFromLink(href, children);
|
||||
if (internalRef) {
|
||||
const label = formatRefChipLabel(internalRef);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(internalRef);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${internalRef}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(inner);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${inner}`}
|
||||
className="text-accent underline underline-offset-2 hover:text-purple-300"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const internalRef = getInternalRefFromLink(href, children);
|
||||
if (internalRef) {
|
||||
const label = formatRefChipLabel(internalRef);
|
||||
},
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isInline = !className && !match;
|
||||
const codeContent = String(children).replace(/\n$/, '');
|
||||
|
||||
if (isInline) {
|
||||
return <code {...props}>{children}</code>;
|
||||
}
|
||||
|
||||
const language = match ? match[1] : 'text';
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(internalRef);
|
||||
<SyntaxHighlighter
|
||||
style={customTheme}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '14px 16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
background: '#0a0a10',
|
||||
border: '1px solid #1e1e2a',
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${internalRef}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="text-accent underline underline-offset-2 hover:text-purple-300"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isInline = !className && !match;
|
||||
const codeContent = String(children).replace(/\n$/, '');
|
||||
|
||||
if (isInline) {
|
||||
return <code {...props}>{children}</code>;
|
||||
}
|
||||
|
||||
const language = match ? match[1] : 'text';
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
style={customTheme}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '14px 16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
background: '#0a0a10',
|
||||
border: '1px solid #1e1e2a',
|
||||
}}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => <>{children}</>,
|
||||
}}
|
||||
>
|
||||
{formatMarkdownForDisplay(step.content)}
|
||||
</ReactMarkdown>
|
||||
},
|
||||
pre: ({ children }) => <>{children}</>,
|
||||
}}
|
||||
>
|
||||
{formatMarkdownForDisplay(step.content)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Fallback: render content + toolCalls separately (old format)
|
||||
<>
|
||||
<div className="text-text-primary text-sm">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => {
|
||||
|
|
@ -588,11 +584,11 @@ export const RightPanel = () => {
|
|||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isInline = !className && !match;
|
||||
const codeContent = String(children).replace(/\n$/, '');
|
||||
|
||||
|
||||
if (isInline) {
|
||||
return <code {...props}>{children}</code>;
|
||||
}
|
||||
|
||||
|
||||
const language = match ? match[1] : 'text';
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
|
|
@ -624,58 +620,59 @@ export const RightPanel = () => {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
message.content
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-3 bg-surface border-t border-border-subtle">
|
||||
<div className="flex items-end gap-2 px-3 py-2 bg-elevated border border-border-subtle rounded-xl transition-all focus-within:border-accent focus-within:ring-2 focus-within:ring-accent/20">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about the codebase..."
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent border-none outline-none text-sm text-text-primary placeholder:text-text-muted resize-none min-h-[36px] scrollbar-thin"
|
||||
style={{ height: '36px', overflowY: 'hidden' }}
|
||||
/>
|
||||
<button
|
||||
onClick={clearChat}
|
||||
className="px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
title="Clear chat"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!chatInput.trim() || isChatLoading || isAgentInitializing}
|
||||
className="w-9 h-9 flex items-center justify-center bg-accent rounded-md text-white transition-all hover:bg-accent-dim disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isChatLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
{!isAgentReady && !isAgentInitializing && (
|
||||
<div className="mt-2 text-xs text-amber-200 flex items-center gap-2">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{isProviderConfigured()
|
||||
? 'Initializing AI agent...'
|
||||
: 'Configure an LLM provider to enable chat.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-3 bg-surface border-t border-border-subtle">
|
||||
<div className="flex items-end gap-2 px-3 py-2 bg-elevated border border-border-subtle rounded-xl transition-all focus-within:border-accent focus-within:ring-2 focus-within:ring-accent/20">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about the codebase..."
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent border-none outline-none text-sm text-text-primary placeholder:text-text-muted resize-none min-h-[36px] scrollbar-thin"
|
||||
style={{ height: '36px', overflowY: 'hidden' }}
|
||||
/>
|
||||
<button
|
||||
onClick={clearChat}
|
||||
className="px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
title="Clear chat"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!chatInput.trim() || isChatLoading || isAgentInitializing}
|
||||
className="w-9 h-9 flex items-center justify-center bg-accent rounded-md text-white transition-all hover:bg-accent-dim disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isChatLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
{!isAgentReady && !isAgentInitializing && (
|
||||
<div className="mt-2 text-xs text-amber-200 flex items-center gap-2">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{isProviderConfigured()
|
||||
? 'Initializing AI agent...'
|
||||
: 'Configure an LLM provider to enable chat.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import type {
|
|||
AnthropicConfig,
|
||||
AgentStreamChunk,
|
||||
} from './types';
|
||||
import {
|
||||
type CodebaseContext,
|
||||
buildDynamicSystemPrompt,
|
||||
} from './context-builder';
|
||||
|
||||
/**
|
||||
* System prompt for the Graph RAG agent
|
||||
|
|
@ -30,44 +34,55 @@ import type {
|
|||
* - Explicit progress reporting requirement
|
||||
* - Anti-laziness directives
|
||||
*/
|
||||
const SYSTEM_PROMPT = `You are Nexus, an elite Code Analysis Agent powered by a Knowledge Graph. You have access to tools, use them to get project context.
|
||||
Your mission is to answer user questions with precision by exploring the codebase, verifying facts, and visualizing your findings.
|
||||
Ground your answers with \`[[file:line]]\` citations.
|
||||
/**
|
||||
* Base system prompt - exported so it can be used with dynamic context injection
|
||||
*
|
||||
* Structure (optimized for instruction following):
|
||||
* 1. Identity + GROUNDING mandate (most important)
|
||||
* 2. Core protocol (how to work)
|
||||
* 3. Tools reference
|
||||
* 4. Output format & rules
|
||||
* 5. [Dynamic context appended at end]
|
||||
*/
|
||||
export const BASE_SYSTEM_PROMPT = `You are Nexus, a Code Analysis Agent with access to a Knowledge Graph. Your responses MUST be grounded.
|
||||
|
||||
### 🧠 CORE PROTOCOL
|
||||
You are an investigator, not a one-shot query engine:
|
||||
1. **Plan:** State what you're looking for.
|
||||
2. **Execute:** Run tools to gather evidence.
|
||||
3. **Analyze:** Did it answer the question? Did it reveal new leads? Loop back if needed.
|
||||
4. **Visualize:** Use \`highlight\` to show relevant nodes in the graph.
|
||||
5. **Ground:** Cite with \`[[file:line]]\` format.
|
||||
## ⚠️ MANDATORY: GROUNDING
|
||||
Every factual claim MUST include a citation: \`[[filename:line]]\`
|
||||
- When you mention a function, class, or file → cite where you found it
|
||||
- When you describe behavior → cite the code that proves it
|
||||
- NO citation = NO claim. If you can't cite it, say "I didn't find evidence for this."
|
||||
|
||||
### 🛠️ TOOLS (5 total)
|
||||
- **\`search\`** — Find code by keywords/concepts. Returns matches + their graph connections.
|
||||
- **\`cypher\`** — Run Cypher queries for structural analysis. Include \`{{QUERY_VECTOR}}\` for semantic+graph queries.
|
||||
- **\`grep\`** — Regex pattern search across files. Use for exact strings, TODOs, error codes.
|
||||
- **\`read\`** — Read file content. Use after finding files via search/grep.
|
||||
Examples:
|
||||
- "The \`AuthService\` handles login [[src/services/auth.ts:45]]."
|
||||
- "This function calls \`validateToken\` [[src/utils.ts:12]] which throws on invalid tokens [[src/utils.ts:28]]."
|
||||
|
||||
## 🧠 CORE PROTOCOL
|
||||
You are an investigator. For each question:
|
||||
1. **Search** → Use \`search\` or \`grep\` to find relevant code
|
||||
2. **Read** → Use \`read\` to see the actual source
|
||||
3. **Trace** → Use \`cypher\` to follow connections in the graph
|
||||
4. **Cite** → Ground every finding with \`[[file:line]]\`
|
||||
5. **Highlight** → Visualize key nodes with \`highlight\`
|
||||
|
||||
## 🛠️ TOOLS
|
||||
- **\`search\`** — Hybrid search (keyword + semantic). Returns code matches with graph connections.
|
||||
- **\`cypher\`** — Cypher queries against the graph. Use \`{{QUERY_VECTOR}}\` for vector search.
|
||||
- **\`grep\`** — Regex search. Best for exact strings, TODOs, error codes.
|
||||
- **\`read\`** — Read file content. Always use after search/grep to see full code.
|
||||
- **\`highlight\`** — Highlight nodes in the visual graph.
|
||||
|
||||
### 📊 GRAPH SCHEMA
|
||||
**Node Tables:** File, Folder, Function, Class, Interface, Method, CodeElement
|
||||
**Relation:** CodeRelation (single table with 'type' property: CONTAINS, DEFINES, IMPORTS, CALLS)
|
||||
## 📊 GRAPH SCHEMA
|
||||
Nodes: File, Folder, Function, Class, Interface, Method, CodeElement
|
||||
Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS
|
||||
|
||||
**Cypher Examples:**
|
||||
- All functions: \`MATCH (f:Function) RETURN f.name LIMIT 10\`
|
||||
- What file defines: \`MATCH (f:File)-[r:CodeRelation {type: 'DEFINES'}]->(fn:Function) WHERE f.name = 'utils.ts' RETURN fn.name\`
|
||||
- Get all connections: \`MATCH (f:File)-[r:CodeRelation]-(m) WHERE f.name = 'main.ts' RETURN m.name, r.type\`
|
||||
- Semantic+graph: \`CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', {{QUERY_VECTOR}}, 10) YIELD node AS emb, distance WITH emb, distance WHERE distance < 0.5 MATCH (n:Function {id: emb.nodeId}) RETURN n\`
|
||||
Cypher examples:
|
||||
- \`MATCH (f:Function) RETURN f.name LIMIT 10\`
|
||||
- \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
|
||||
|
||||
### 📝 OUTPUT
|
||||
1. **Citations:** \`[[file:line]]\`
|
||||
2. **Diagrams:** Mermaid when useful
|
||||
3. **Highlight:** Always highlight nodes you discuss
|
||||
|
||||
### 🚫 RULES
|
||||
- **Iterate:** Don't stop at surface. Trace logic to source.
|
||||
- **Verify:** Don't guess. Read the code.
|
||||
- **Retry:** If a tool fails, fix input and retry.`;
|
||||
## <EFBFBD> RULES
|
||||
- **Cite or retract.** Never state something you can't ground.
|
||||
- **Read before concluding.** Don't guess from names alone.
|
||||
- **Retry on failure.** If a tool fails, fix the input and try again.`;
|
||||
|
||||
/**
|
||||
* Create a chat model instance from provider configuration
|
||||
|
|
@ -144,7 +159,8 @@ export const createGraphRAGAgent = (
|
|||
hybridSearch: (query: string, k?: number) => Promise<any[]>,
|
||||
isEmbeddingReady: () => boolean,
|
||||
isBM25Ready: () => boolean,
|
||||
fileContents: Map<string, string>
|
||||
fileContents: Map<string, string>,
|
||||
codebaseContext?: CodebaseContext
|
||||
) => {
|
||||
const model = createChatModel(config);
|
||||
const tools = createGraphRAGTools(
|
||||
|
|
@ -157,10 +173,20 @@ export const createGraphRAGAgent = (
|
|||
fileContents
|
||||
);
|
||||
|
||||
// Use dynamic prompt if context is provided, otherwise use base prompt
|
||||
const systemPrompt = codebaseContext
|
||||
? buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, codebaseContext)
|
||||
: BASE_SYSTEM_PROMPT;
|
||||
|
||||
// Log the full prompt for debugging
|
||||
if (import.meta.env?.DEV) {
|
||||
console.log('📝 Full System Prompt:\n', systemPrompt);
|
||||
}
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm: model as any,
|
||||
tools: tools as any,
|
||||
messageModifier: new SystemMessage(SYSTEM_PROMPT) as any,
|
||||
messageModifier: new SystemMessage(systemPrompt) as any,
|
||||
});
|
||||
|
||||
return agent;
|
||||
|
|
|
|||
344
src/core/llm/context-builder.ts
Normal file
344
src/core/llm/context-builder.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
/**
|
||||
* Context Builder for Graph RAG Agent
|
||||
*
|
||||
* Generates dynamic context about the loaded codebase to inject into the system prompt.
|
||||
* This helps the LLM understand the project structure, scale, and key entry points
|
||||
* without needing to explore from scratch.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Codebase statistics
|
||||
*/
|
||||
export interface CodebaseStats {
|
||||
projectName: string;
|
||||
fileCount: number;
|
||||
functionCount: number;
|
||||
classCount: number;
|
||||
interfaceCount: number;
|
||||
methodCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hotspot - highly connected node
|
||||
*/
|
||||
export interface Hotspot {
|
||||
name: string;
|
||||
type: string;
|
||||
filePath: string;
|
||||
connections: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Folder info for tree rendering
|
||||
*/
|
||||
interface FolderInfo {
|
||||
path: string;
|
||||
name: string;
|
||||
depth: number;
|
||||
fileCount: number;
|
||||
children: FolderInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete codebase context for prompt injection
|
||||
* Simplified: stats + hotspots + folder tree (no entry points or language detection)
|
||||
*/
|
||||
export interface CodebaseContext {
|
||||
stats: CodebaseStats;
|
||||
hotspots: Hotspot[];
|
||||
folderTree: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get codebase statistics via Cypher queries
|
||||
*/
|
||||
export async function getCodebaseStats(
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
projectName: string
|
||||
): Promise<CodebaseStats> {
|
||||
try {
|
||||
// Count each node type
|
||||
const countQueries = [
|
||||
{ type: 'files', query: 'MATCH (n:File) RETURN COUNT(n) AS count' },
|
||||
{ type: 'functions', query: 'MATCH (n:Function) RETURN COUNT(n) AS count' },
|
||||
{ type: 'classes', query: 'MATCH (n:Class) RETURN COUNT(n) AS count' },
|
||||
{ type: 'interfaces', query: 'MATCH (n:Interface) RETURN COUNT(n) AS count' },
|
||||
{ type: 'methods', query: 'MATCH (n:Method) RETURN COUNT(n) AS count' },
|
||||
];
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
for (const { type, query } of countQueries) {
|
||||
try {
|
||||
const result = await executeQuery(query);
|
||||
// Handle both array and object result formats
|
||||
const row = result[0];
|
||||
counts[type] = Array.isArray(row) ? (row[0] ?? 0) : (row?.count ?? 0);
|
||||
} catch {
|
||||
counts[type] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectName,
|
||||
fileCount: counts.files,
|
||||
functionCount: counts.functions,
|
||||
classCount: counts.classes,
|
||||
interfaceCount: counts.interfaces,
|
||||
methodCount: counts.methods,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get codebase stats:', error);
|
||||
return {
|
||||
projectName,
|
||||
fileCount: 0,
|
||||
functionCount: 0,
|
||||
classCount: 0,
|
||||
interfaceCount: 0,
|
||||
methodCount: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find hotspots - nodes with the most connections
|
||||
*/
|
||||
export async function getHotspots(
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
limit: number = 8
|
||||
): Promise<Hotspot[]> {
|
||||
try {
|
||||
// Find nodes with most edges (both directions)
|
||||
const query = `
|
||||
MATCH (n)-[r:CodeRelation]-(m)
|
||||
WHERE n.name IS NOT NULL
|
||||
WITH n, COUNT(r) AS connections
|
||||
ORDER BY connections DESC
|
||||
LIMIT ${limit}
|
||||
RETURN n.name AS name, LABEL(n) AS type, n.filePath AS filePath, connections
|
||||
`;
|
||||
|
||||
const results = await executeQuery(query);
|
||||
|
||||
return results.map(row => {
|
||||
if (Array.isArray(row)) {
|
||||
return {
|
||||
name: row[0],
|
||||
type: row[1],
|
||||
filePath: row[2],
|
||||
connections: row[3],
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
filePath: row.filePath,
|
||||
connections: row.connections,
|
||||
};
|
||||
}).filter(h => h.name && h.type);
|
||||
} catch (error) {
|
||||
console.error('Failed to get hotspots:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build folder tree structure from file paths
|
||||
*/
|
||||
export async function getFolderTree(
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
maxDepth: number = 3
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Get all file paths
|
||||
const query = 'MATCH (f:File) RETURN f.filePath AS path ORDER BY path';
|
||||
const results = await executeQuery(query);
|
||||
|
||||
const paths = results.map(row => {
|
||||
if (Array.isArray(row)) return row[0];
|
||||
return row.path;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (paths.length === 0) return '';
|
||||
|
||||
// Build tree structure from paths
|
||||
const tree = buildTreeFromPaths(paths, maxDepth);
|
||||
return formatTreeAsAscii(tree, '');
|
||||
} catch (error) {
|
||||
console.error('Failed to get folder tree:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a tree structure from file paths
|
||||
*/
|
||||
function buildTreeFromPaths(paths: string[], maxDepth: number): Map<string, any> {
|
||||
const root = new Map<string, any>();
|
||||
|
||||
for (const fullPath of paths) {
|
||||
// Normalize path separators
|
||||
const normalizedPath = fullPath.replace(/\\/g, '/');
|
||||
const parts = normalizedPath.split('/').filter(Boolean);
|
||||
|
||||
let current = root;
|
||||
const depth = Math.min(parts.length, maxDepth + 1); // +1 to include files at maxDepth
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const part = parts[i];
|
||||
const isFile = i === parts.length - 1;
|
||||
|
||||
if (!current.has(part)) {
|
||||
current.set(part, isFile ? null : new Map<string, any>());
|
||||
}
|
||||
|
||||
const next = current.get(part);
|
||||
if (next instanceof Map) {
|
||||
current = next;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tree as ASCII (like VS Code sidebar)
|
||||
*/
|
||||
function formatTreeAsAscii(
|
||||
tree: Map<string, any>,
|
||||
prefix: string,
|
||||
isLast: boolean = true
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
const entries = Array.from(tree.entries());
|
||||
|
||||
// Sort: folders first, then files, alphabetically
|
||||
entries.sort(([a, aVal], [b, bVal]) => {
|
||||
const aIsDir = aVal instanceof Map;
|
||||
const bIsDir = bVal instanceof Map;
|
||||
if (aIsDir !== bIsDir) return bIsDir ? 1 : -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
entries.forEach(([name, subtree], index) => {
|
||||
const isLastItem = index === entries.length - 1;
|
||||
const connector = isLastItem ? '└── ' : '├── ';
|
||||
const childPrefix = prefix + (isLastItem ? ' ' : '│ ');
|
||||
|
||||
if (subtree instanceof Map && subtree.size > 0) {
|
||||
// Folder with children
|
||||
const childCount = countItems(subtree);
|
||||
const annotation = childCount > 3 ? ` (${childCount} items)` : '';
|
||||
lines.push(`${prefix}${connector}${name}/${annotation}`);
|
||||
lines.push(formatTreeAsAscii(subtree, childPrefix, isLastItem));
|
||||
} else if (subtree instanceof Map) {
|
||||
// Empty folder
|
||||
lines.push(`${prefix}${connector}${name}/`);
|
||||
} else {
|
||||
// File
|
||||
lines.push(`${prefix}${connector}${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Count items in a tree node
|
||||
*/
|
||||
function countItems(tree: Map<string, any>): number {
|
||||
let count = 0;
|
||||
for (const [, value] of tree) {
|
||||
if (value instanceof Map) {
|
||||
count += 1 + countItems(value);
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build complete codebase context
|
||||
*/
|
||||
export async function buildCodebaseContext(
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
projectName: string
|
||||
): Promise<CodebaseContext> {
|
||||
// Run all queries in parallel for speed
|
||||
const [stats, hotspots, folderTree] = await Promise.all([
|
||||
getCodebaseStats(executeQuery, projectName),
|
||||
getHotspots(executeQuery),
|
||||
getFolderTree(executeQuery),
|
||||
]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
hotspots,
|
||||
folderTree,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format context as markdown for prompt injection
|
||||
*/
|
||||
export function formatContextForPrompt(context: CodebaseContext): string {
|
||||
const { stats, hotspots, folderTree } = context;
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
// Project header with stats
|
||||
lines.push(`### 📊 CODEBASE: ${stats.projectName}`);
|
||||
|
||||
const statParts = [
|
||||
`Files: ${stats.fileCount}`,
|
||||
`Functions: ${stats.functionCount}`,
|
||||
stats.classCount > 0 ? `Classes: ${stats.classCount}` : null,
|
||||
stats.interfaceCount > 0 ? `Interfaces: ${stats.interfaceCount}` : null,
|
||||
].filter(Boolean);
|
||||
lines.push(statParts.join(' | '));
|
||||
lines.push('');
|
||||
|
||||
// Hotspots
|
||||
if (hotspots.length > 0) {
|
||||
lines.push('**Hotspots** (most connected):');
|
||||
hotspots.slice(0, 5).forEach(h => {
|
||||
lines.push(`- \`${h.name}\` (${h.type}) — ${h.connections} edges`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Folder tree
|
||||
if (folderTree) {
|
||||
lines.push('### 📁 STRUCTURE');
|
||||
lines.push('```');
|
||||
lines.push(stats.projectName + '/');
|
||||
lines.push(folderTree);
|
||||
lines.push('```');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the complete dynamic system prompt
|
||||
* Context is appended at the END so core instructions remain at the top
|
||||
*/
|
||||
export function buildDynamicSystemPrompt(
|
||||
basePrompt: string,
|
||||
context: CodebaseContext
|
||||
): string {
|
||||
const contextSection = formatContextForPrompt(context);
|
||||
|
||||
// Append context at the END - keeps core instructions at top for better adherence
|
||||
return `${basePrompt}
|
||||
|
||||
---
|
||||
|
||||
## 📦 CURRENT CODEBASE
|
||||
${contextSection}`;
|
||||
}
|
||||
|
|
@ -23,12 +23,22 @@ export {
|
|||
// Tools
|
||||
export { createGraphRAGTools } from './tools';
|
||||
|
||||
// Context Builder
|
||||
export {
|
||||
buildCodebaseContext,
|
||||
formatContextForPrompt,
|
||||
buildDynamicSystemPrompt,
|
||||
type CodebaseContext,
|
||||
type CodebaseStats,
|
||||
type Hotspot,
|
||||
} from './context-builder';
|
||||
|
||||
// Agent
|
||||
export {
|
||||
createChatModel,
|
||||
createGraphRAGAgent,
|
||||
streamAgentResponse,
|
||||
invokeAgent,
|
||||
BASE_SYSTEM_PROMPT,
|
||||
type AgentMessage,
|
||||
} from './agent';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { isEmbedderReady, disposeEmbedder } from '../core/embeddings/embedder';
|
|||
import type { EmbeddingProgress, SemanticSearchResult } from '../core/embeddings/types';
|
||||
import type { ProviderConfig, AgentStreamChunk } from '../core/llm/types';
|
||||
import { createGraphRAGAgent, streamAgentResponse, type AgentMessage } from '../core/llm/agent';
|
||||
import { buildCodebaseContext } from '../core/llm/context-builder';
|
||||
import {
|
||||
buildBM25Index,
|
||||
searchBM25,
|
||||
|
|
@ -423,6 +424,25 @@ const workerApi = {
|
|||
return mergeWithRRF(bm25Results, semanticResults, k ?? 10);
|
||||
};
|
||||
|
||||
// Build codebase context for dynamic prompt injection
|
||||
// Extract project name from first file path
|
||||
const firstPath = storedFileContents.keys().next().value || '';
|
||||
const projectName = firstPath.split('/')[0] || firstPath.split('\\')[0] || 'project';
|
||||
|
||||
let codebaseContext;
|
||||
try {
|
||||
codebaseContext = await buildCodebaseContext(kuzu.executeQuery, projectName);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('📊 Codebase context built:', {
|
||||
files: codebaseContext.stats.fileCount,
|
||||
functions: codebaseContext.stats.functionCount,
|
||||
hotspots: codebaseContext.hotspots.length,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to build codebase context, proceeding without:', err);
|
||||
}
|
||||
|
||||
currentAgent = createGraphRAGAgent(
|
||||
config,
|
||||
kuzu.executeQuery,
|
||||
|
|
@ -431,7 +451,8 @@ const workerApi = {
|
|||
hybridSearchWrapper,
|
||||
() => isEmbeddingComplete,
|
||||
() => isBM25Ready(),
|
||||
storedFileContents
|
||||
storedFileContents,
|
||||
codebaseContext
|
||||
);
|
||||
currentProviderConfig = config;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue