structured output fix wip, ChatInterface better UX

This commit is contained in:
abhigyantrumio 2025-08-20 06:12:48 +05:30
parent adf8810101
commit 889beff56f
7 changed files with 913 additions and 28098 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
import { HumanMessage, SystemMessage, AIMessage, BaseMessage } from '@langchain/core/messages';
import { z } from 'zod';
import type { LLMService, LLMConfig } from './llm-service.ts';
import type { CypherGenerator } from './cypher-generator.ts';
import type { KnowledgeGraph } from '../core/graph/types.ts';
import type { LocalStorageChatHistory } from '../lib/chat-history.ts';
// Define Zod schema for ReAct step
const ReActStepSchema = z.object({
thought: z.string().describe("The reasoning process - what you're thinking about"),
action: z.enum(['query_graph', 'get_code', 'search_files', 'final_answer']).describe("The action to take - must be one of: query_graph, get_code, search_files, or final_answer"),
actionInput: z.string().describe("Input for the action - the query, file path, search pattern, or final answer")
});
export async function debugStructuredOutput(llmService: LLMService, llmConfig: LLMConfig) {
console.log('=== DEBUGGING STRUCTURED OUTPUT ===');
try {
const model = llmService.getModel(llmConfig);
console.log('Model type:', model.constructor.name);
console.log('Model supports withStructuredOutput:', typeof model.withStructuredOutput === 'function');
if (typeof model.withStructuredOutput === 'function') {
console.log('Attempting to create structured model...');
const structuredModel = model.withStructuredOutput(ReActStepSchema);
console.log('Structured model created successfully');
// Test with a simple prompt
const testMessages = [
new SystemMessage('You are a helpful assistant. Respond with a structured output.'),
new HumanMessage('Think about searching for files and respond with the appropriate action.')
];
console.log('Testing structured output...');
const response = await structuredModel.invoke(testMessages);
console.log('Structured output response:', response);
return { success: true, response };
} else {
console.log('Model does not support structured output');
return { success: false, reason: 'Model does not support withStructuredOutput' };
}
} catch (error) {
console.error('Error in structured output:', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}

View file

@ -1 +1,90 @@
import { ReActAgent } from './react-agent';
import { LLMService } from './llm-service';
import { CypherGenerator } from './cypher-generator';
// Mock the LLM service for testing
jest.mock('./llm-service');
jest.mock('./cypher-generator');
describe('ReActAgent Structured Output', () => {
let reactAgent: ReActAgent;
let mockLLMService: jest.Mocked<LLMService>;
let mockCypherGenerator: jest.Mocked<CypherGenerator>;
beforeEach(() => {
mockLLMService = new LLMService() as jest.Mocked<LLMService>;
mockCypherGenerator = new CypherGenerator(mockLLMService) as jest.Mocked<CypherGenerator>;
reactAgent = new ReActAgent(mockLLMService, mockCypherGenerator);
});
test('should use structured output when available', async () => {
// Mock the model to support structured output
const mockModel = {
withStructuredOutput: jest.fn().mockReturnValue({
invoke: jest.fn().mockResolvedValue({
thought: 'I need to search for files',
action: 'search_files',
actionInput: 'test'
})
})
};
mockLLMService.getModel = jest.fn().mockReturnValue(mockModel);
// Mock context
const mockContext = {
graph: { nodes: [], relationships: [] },
fileContents: new Map([['test.ts', 'console.log("test")']])
};
await reactAgent.setContext(mockContext, {
provider: 'openai',
apiKey: 'test-key',
model: 'gpt-4o-mini'
});
// Test the process
const result = await reactAgent.processQuestion('Find test files', {
provider: 'openai',
apiKey: 'test-key',
model: 'gpt-4o-mini'
});
expect(mockModel.withStructuredOutput).toHaveBeenCalled();
expect(result.reasoning.length).toBeGreaterThan(0);
});
test('should fallback to regex parsing when structured output fails', async () => {
// Mock the model to not support structured output
const mockModel = {
withStructuredOutput: undefined
};
mockLLMService.getModel = jest.fn().mockReturnValue(mockModel);
mockLLMService.chat = jest.fn().mockResolvedValue({
content: 'Thought: I need to search\nAction: search_files\nAction Input: test'
});
// Mock context
const mockContext = {
graph: { nodes: [], relationships: [] },
fileContents: new Map([['test.ts', 'console.log("test")']])
};
await reactAgent.setContext(mockContext, {
provider: 'openai',
apiKey: 'test-key',
model: 'gpt-4o-mini'
});
// Test the process
const result = await reactAgent.processQuestion('Find test files', {
provider: 'openai',
apiKey: 'test-key',
model: 'gpt-4o-mini'
});
expect(mockLLMService.chat).toHaveBeenCalled();
expect(result.reasoning.length).toBeGreaterThan(0);
});
});

View file

@ -1,12 +1,15 @@
import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages';
import { HumanMessage, SystemMessage, AIMessage, BaseMessage } from '@langchain/core/messages';
import { z } from 'zod';
import type { LLMService, LLMConfig } from './llm-service.ts';
import type { CypherGenerator } from './cypher-generator.ts';
import type { KnowledgeGraph } from '../core/graph/types.ts';
import type { LocalStorageChatHistory } from '../lib/chat-history.ts';
export interface ReActContext {
graph: KnowledgeGraph;
fileContents: Map<string, string>;
projectName?: string;
sessionId?: string;
}
export interface ReActToolResult {
@ -50,7 +53,7 @@ export interface ReActOptions {
// Define Zod schema for ReAct step
const ReActStepSchema = z.object({
thought: z.string().describe("The reasoning process - what you're thinking about"),
action: z.string().describe("The action to take (query_graph, get_code, search_files, or final_answer)"),
action: z.enum(['query_graph', 'get_code', 'search_files', 'final_answer']).describe("The action to take - must be one of: query_graph, get_code, search_files, or final_answer"),
actionInput: z.string().describe("Input for the action - the query, file path, search pattern, or final answer")
});
@ -58,7 +61,7 @@ export class ReActAgent {
private llmService: LLMService;
private cypherGenerator: CypherGenerator;
private context: ReActContext | null = null;
private conversationHistory: any[] = [];
private chatHistory: LocalStorageChatHistory | null = null;
constructor(llmService: LLMService, cypherGenerator: CypherGenerator, _kuzuQueryEngine?: any) {
this.llmService = llmService;
@ -69,37 +72,31 @@ export class ReActAgent {
* Initialize the ReAct agent
*/
public async initialize(): Promise<void> {
// Initialize any required components
console.log('ReActAgent initialized');
}
/**
* Get conversation history
*/
public async getConversationHistory(): Promise<any[]> {
return this.conversationHistory;
}
/**
* Clear conversation history
*/
public async clearConversationHistory(): Promise<void> {
this.conversationHistory = [];
}
/**
* Set the context for ReAct operations
*/
public async setContext(context: ReActContext & { projectName?: string; sessionId?: string }, _llmConfig: LLMConfig): Promise<void> {
this.context = {
graph: context.graph,
fileContents: context.fileContents
fileContents: context.fileContents,
projectName: context.projectName,
sessionId: context.sessionId
};
this.cypherGenerator.updateSchema(context.graph);
}
/**
* Process a question using ReAct pattern
* Set chat history for conversation context
*/
public setChatHistory(chatHistory: LocalStorageChatHistory): void {
this.chatHistory = chatHistory;
}
/**
* Process a question using ReAct pattern with chat history
*/
public async processQuestion(
question: string,
@ -119,6 +116,7 @@ export class ReActAgent {
const reasoning: ReActStep[] = [];
const sources: string[] = [];
const cypherQueries: Array<{ cypher: string; explanation: string; confidence: number }> = [];
// Enhanced LLM config for reasoning
const reasoningConfig: LLMConfig = {
@ -131,11 +129,26 @@ export class ReActAgent {
let confidence = 0.5;
try {
// Initial system prompt for ReAct
// Build conversation with chat history
const conversation: BaseMessage[] = [];
// Add system prompt
const systemPrompt = this.buildReActSystemPrompt(strictMode);
const conversation = [new SystemMessage(systemPrompt)];
conversation.push(new SystemMessage(systemPrompt));
// Add the user question
// Add chat history if available
if (this.chatHistory) {
try {
const historyMessages = await this.chatHistory.getMessages();
// Add recent history (last 10 messages to avoid context overflow)
const recentHistory = historyMessages.slice(-10);
conversation.push(...recentHistory);
} catch (error) {
console.warn('Failed to load chat history:', error);
}
}
// Add the current user question
conversation.push(new HumanMessage(`Question: ${question}`));
while (currentStep <= maxIterations) {
@ -144,17 +157,31 @@ export class ReActAgent {
try {
// Try using structured output first
const model = this.llmService.getModel(reasoningConfig);
console.log('Attempting structured output with model:', model.constructor.name);
if (model && typeof model.withStructuredOutput === 'function') {
console.log('Model supports structured output, attempting to use it...');
const structuredModel = model.withStructuredOutput(ReActStepSchema);
const structuredResponse = await structuredModel.invoke(conversation);
reasoning_step = {
step: currentStep,
thought: structuredResponse.thought,
action: structuredResponse.action,
actionInput: structuredResponse.actionInput
};
console.log('Structured output successful:', structuredResponse);
// Validate the structured response
const validActions = ['query_graph', 'get_code', 'search_files', 'final_answer'];
if (!validActions.includes(structuredResponse.action)) {
console.warn(`Invalid action from structured output: ${structuredResponse.action}, falling back to regex parsing`);
const response = await this.llmService.chat(reasoningConfig, conversation);
reasoning_step = this.parseReasoningStep(String(response.content || ''), currentStep);
} else {
reasoning_step = {
step: currentStep,
thought: structuredResponse.thought,
action: structuredResponse.action,
actionInput: structuredResponse.actionInput
};
}
} else {
console.warn('Model does not support structured output, falling back to regex parsing');
// Fallback to regular chat + regex parsing
const response = await this.llmService.chat(reasoningConfig, conversation);
reasoning_step = this.parseReasoningStep(String(response.content || ''), currentStep);
@ -180,6 +207,15 @@ export class ReActAgent {
reasoning_step.toolResult = toolResult;
reasoning_step.observation = toolResult.output;
// Track Cypher queries
if (reasoning_step.action === 'query_graph' && toolResult.success) {
cypherQueries.push({
cypher: reasoning_step.actionInput || '',
explanation: 'Generated via ReAct reasoning',
confidence: confidence
});
}
// Add sources if successful
if (toolResult.success && toolResult.output) {
sources.push(`${reasoning_step.action}: ${reasoning_step.actionInput}`);
@ -207,7 +243,7 @@ export class ReActAgent {
reasoning: includeReasoning ? reasoning : [],
confidence,
sources: Array.from(new Set(sources)), // Remove duplicates
cypherQueries: [] // TODO: Track actual Cypher queries
cypherQueries
};
} catch (error) {
@ -216,7 +252,7 @@ export class ReActAgent {
}
/**
* Build the ReAct system prompt
* Build the ReAct system prompt with chat history context
*/
private buildReActSystemPrompt(strictMode: boolean): string {
const prompt = `You are an expert code analyst using a ReAct (Reasoning + Acting) approach to answer questions about a codebase.
@ -228,21 +264,25 @@ You have access to the following tools:
4. final_answer: Provide the final answer to the user's question
IMPORTANT INSTRUCTIONS:
- You have access to the conversation history above, so you can reference previous questions and answers
- Think step by step and provide your reasoning in the "thought" field
- Choose the appropriate action from the available tools
- Choose the appropriate action from the available tools (ONLY: query_graph, get_code, search_files, or final_answer)
- Provide the necessary input for the chosen action
- Use the tools to gather information before providing final answers
- Be precise and thorough in your analysis
- Cite specific files and code snippets when possible
- If the user refers to something from the conversation history, use that context
${strictMode ? 'STRICT MODE: Only use exact matches and precise queries.' : 'FLEXIBLE MODE: Use heuristic matching when exact matches fail.'}
You must respond with a structured output containing:
- thought: Your reasoning process for this step
- action: The tool you want to use (query_graph, get_code, search_files, or final_answer)
- action: The tool you want to use (MUST be one of: query_graph, get_code, search_files, or final_answer)
- actionInput: The input for the chosen tool
When providing a final_answer, make sure to give a complete, comprehensive response in the actionInput field.`;
When providing a final_answer, make sure to give a complete, comprehensive response in the actionInput field.
CRITICAL: The action field must be exactly one of these four values: query_graph, get_code, search_files, or final_answer.`;
return prompt;
}
@ -251,14 +291,41 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
* Parse a reasoning step from LLM response
*/
private parseReasoningStep(response: string, stepNumber: number): ReActStep {
const thoughtMatch = response.match(/Thought:\s*(.*?)(?=\nAction:|$)/);
const actionMatch = response.match(/Action:\s*(.*?)(?=\nAction Input:|$)/);
const actionInputMatch = response.match(/Action Input:\s*([\s\S]*?)(?=\nThought:|$)/);
// Normalize the response to handle different line endings and whitespace
const normalizedResponse = response.replace(/\r\n/g, '\n').trim();
// More robust regex patterns that handle various formats
const thoughtMatch = normalizedResponse.match(/Thought:\s*(.*?)(?=\nAction:|$)/);
const actionMatch = normalizedResponse.match(/Action:\s*(.*?)(?=\nAction Input:|$)/);
const actionInputMatch = normalizedResponse.match(/Action Input:\s*([\s\S]*?)(?=\nThought:|$)/);
let action = actionMatch ? actionMatch[1].trim() : '';
// Normalize action names to handle variations
if (action) {
action = action.toLowerCase().replace(/[^a-z_]/g, '');
// Map common variations to expected actions
const actionMap: Record<string, string> = {
'querygraph': 'query_graph',
'query_graph': 'query_graph',
'getcode': 'get_code',
'get_code': 'get_code',
'searchfiles': 'search_files',
'search_files': 'search_files',
'finalanswer': 'final_answer',
'final_answer': 'final_answer',
'answer': 'final_answer',
'respond': 'final_answer'
};
action = actionMap[action] || action;
}
return {
step: stepNumber,
thought: thoughtMatch ? thoughtMatch[1].trim() : '',
action: actionMatch ? actionMatch[1].trim() : '',
thought: thoughtMatch ? thoughtMatch[1].trim() : 'No thought provided',
action: action || 'unknown',
actionInput: actionInputMatch ? actionInputMatch[1].trim() : ''
};
}
@ -271,7 +338,10 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
let output = '';
let success = false;
switch (action) {
// Normalize action name for case-insensitive matching
const normalizedAction = action.toLowerCase().trim();
switch (normalizedAction) {
case 'query_graph':
if (!this.context) {
throw new Error('Context not set');
@ -312,12 +382,9 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
throw new Error('Context not set');
}
const matchingFiles: string[] = [];
for (const [filePath] of this.context.fileContents) {
if (filePath.toLowerCase().includes(input.toLowerCase())) {
matchingFiles.push(filePath);
}
}
const matchingFiles = Array.from(this.context.fileContents.keys())
.filter(file => file.toLowerCase().includes(input.toLowerCase()))
.slice(0, 10); // Limit results
output = JSON.stringify(matchingFiles, null, 2);
success = true;
@ -328,8 +395,14 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
success = true;
break;
case 'unknown':
case '':
output = `No action specified. Available actions: query_graph, get_code, search_files, final_answer`;
success = false;
break;
default:
output = `Unknown action: ${action}`;
output = `Unknown action: "${action}". Available actions: query_graph, get_code, search_files, final_answer`;
success = false;
}
@ -339,7 +412,6 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
output,
success
};
} catch (error) {
return {
toolName: action,
@ -352,35 +424,27 @@ When providing a final_answer, make sure to give a complete, comprehensive respo
}
/**
* Execute a graph query (placeholder - implement based on your graph engine)
* Execute a graph query (placeholder implementation)
*/
private async executeGraphQuery(cypherQuery: string): Promise<Record<string, unknown>> {
private async executeGraphQuery(cypher: string): Promise<any> {
// This is a placeholder - implement based on your graph engine
console.log('Executing Cypher query:', cypherQuery);
// For now, return a mock result
return {
nodes: [],
relationships: [],
message: 'Graph query executed (placeholder implementation)',
query: cypherQuery
};
console.log('Executing Cypher query:', cypher);
return { nodes: [], relationships: [], message: 'Graph query executed (placeholder)' };
}
/**
* Build summary prompt for incomplete reasoning
*/
private buildSummaryPrompt(question: string, reasoning: ReActStep[]): string {
const observations = reasoning
.filter(step => step.observation)
.map(step => `Step ${step.step}: ${step.observation}`)
.join('\n');
const reasoningText = reasoning.map(step =>
`Step ${step.step}: ${step.thought}\nAction: ${step.action}\nResult: ${step.observation || 'No result'}`
).join('\n\n');
return `Based on the following observations from my analysis, please provide a comprehensive answer to the original question: "${question}"
return `Based on the following reasoning steps, provide a comprehensive answer to the question: "${question}"
Observations:
${observations}
Reasoning steps:
${reasoningText}
Please synthesize this information into a clear and helpful answer.`;
Please provide a complete answer based on the information gathered.`;
}
}

View file

@ -3,7 +3,7 @@ import { BaseMessage, HumanMessage, AIMessage, SystemMessage } from '@langchain/
export interface ChatHistoryMetadata {
cypherQuery?: string;
queryResult?: any;
queryResult?: unknown;
executionTime?: number;
timestamp: number;
similarity?: number;

398
src/lib/query-cache.ts Normal file
View file

@ -0,0 +1,398 @@
import { ChatSessionManager, LocalStorageChatHistory, type ChatHistoryMetadata } from './chat-history.ts';
export interface CachedQuery {
question: string;
cypherQuery: string;
confidence: number;
executionTime: number;
resultCount: number;
timestamp: number;
success: boolean;
}
export interface QuerySuggestion {
cypherQuery: string;
confidence: number;
similarity: number;
sourceQuestion: string;
executionTime: number;
}
export interface QueryCacheStats {
totalQueries: number;
successfulQueries: number;
averageExecutionTime: number;
averageConfidence: number;
cacheHitRate: number;
lastUpdated: number;
}
/**
* Query cache service that learns from previous queries and suggests similar ones
*/
export class QueryCacheService {
private static instance: QueryCacheService;
private cache: Map<string, CachedQuery> = new Map();
private questionEmbeddings: Map<string, number[]> = new Map();
private maxCacheSize: number;
private storageKey: string;
private constructor(options: {
maxCacheSize?: number;
storageKey?: string;
} = {}) {
this.maxCacheSize = options.maxCacheSize || 1000;
this.storageKey = options.storageKey || 'gitnexus_query_cache';
this.loadFromStorage();
}
static getInstance(options?: {
maxCacheSize?: number;
storageKey?: string;
}): QueryCacheService {
if (!QueryCacheService.instance) {
QueryCacheService.instance = new QueryCacheService(options);
}
return QueryCacheService.instance;
}
/**
* Add a query to the cache
*/
addQuery(
question: string,
cypherQuery: string,
confidence: number,
executionTime: number,
resultCount: number,
success: boolean = true
): void {
const queryHash = this.hashQuestion(question);
const cachedQuery: CachedQuery = {
question,
cypherQuery,
confidence,
executionTime,
resultCount,
timestamp: Date.now(),
success
};
this.cache.set(queryHash, cachedQuery);
// Maintain cache size
if (this.cache.size > this.maxCacheSize) {
this.evictOldest();
}
this.saveToStorage();
}
/**
* Find similar queries for a given question
*/
findSimilarQueries(
question: string,
options: {
minSimilarity?: number;
maxResults?: number;
minConfidence?: number;
} = {}
): QuerySuggestion[] {
const {
minSimilarity = 0.7,
maxResults = 5,
minConfidence = 0.6
} = options;
const suggestions: QuerySuggestion[] = [];
for (const [hash, cachedQuery] of this.cache.entries()) {
if (!cachedQuery.success || cachedQuery.confidence < minConfidence) {
continue;
}
const similarity = this.calculateSimilarity(question, cachedQuery.question);
if (similarity >= minSimilarity) {
suggestions.push({
cypherQuery: cachedQuery.cypherQuery,
confidence: cachedQuery.confidence,
similarity,
sourceQuestion: cachedQuery.question,
executionTime: cachedQuery.executionTime
});
}
}
// Sort by similarity and confidence
suggestions.sort((a, b) => {
const scoreA = a.similarity * a.confidence;
const scoreB = b.similarity * b.confidence;
return scoreB - scoreA;
});
return suggestions.slice(0, maxResults);
}
/**
* Get the best query for a question
*/
getBestQuery(question: string): CachedQuery | null {
const suggestions = this.findSimilarQueries(question, {
minSimilarity: 0.8,
maxResults: 1,
minConfidence: 0.7
});
if (suggestions.length === 0) {
return null;
}
const suggestion = suggestions[0];
const queryHash = this.hashQuestion(suggestion.sourceQuestion);
return this.cache.get(queryHash) || null;
}
/**
* Load queries from chat history
*/
async loadFromChatHistory(): Promise<void> {
try {
const sessions = ChatSessionManager.getAllSessions();
for (const session of sessions) {
const history = new LocalStorageChatHistory(session.id);
const messages = await history.getMessages();
for (const message of messages) {
const metadata = message.additional_kwargs?.metadata as ChatHistoryMetadata;
if (metadata?.cypherQuery && metadata?.executionTime !== undefined) {
// Determine if this was a successful query based on confidence
const success = (metadata.confidence || 0) > 0.5;
this.addQuery(
message.content.toString(),
metadata.cypherQuery,
metadata.confidence || 0.5,
metadata.executionTime,
0, // We don't have result count in metadata
success
);
}
}
}
} catch (error) {
console.error('Failed to load queries from chat history:', error);
}
}
/**
* Get cache statistics
*/
getStats(): QueryCacheStats {
const queries = Array.from(this.cache.values());
const successfulQueries = queries.filter(q => q.success);
const totalExecutionTime = successfulQueries.reduce((sum, q) => sum + q.executionTime, 0);
const totalConfidence = successfulQueries.reduce((sum, q) => sum + q.confidence, 0);
return {
totalQueries: queries.length,
successfulQueries: successfulQueries.length,
averageExecutionTime: successfulQueries.length > 0 ? totalExecutionTime / successfulQueries.length : 0,
averageConfidence: successfulQueries.length > 0 ? totalConfidence / successfulQueries.length : 0,
cacheHitRate: 0, // Would need to track hits/misses
lastUpdated: Date.now()
};
}
/**
* Clear the cache
*/
clear(): void {
this.cache.clear();
this.questionEmbeddings.clear();
this.saveToStorage();
}
/**
* Export cache data
*/
export(): CachedQuery[] {
return Array.from(this.cache.values());
}
/**
* Import cache data
*/
import(queries: CachedQuery[]): void {
this.cache.clear();
for (const query of queries) {
const hash = this.hashQuestion(query.question);
this.cache.set(hash, query);
}
this.saveToStorage();
}
// Private helper methods
private hashQuestion(question: string): string {
// Simple hash function - in production, you might want a more sophisticated approach
return question.toLowerCase().replace(/\s+/g, ' ').trim();
}
private calculateSimilarity(question1: string, question2: string): number {
const words1 = new Set(question1.toLowerCase().split(/\s+/));
const words2 = new Set(question2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
private evictOldest(): void {
let oldestKey: string | null = null;
let oldestTime = Infinity;
for (const [key, query] of this.cache.entries()) {
if (query.timestamp < oldestTime) {
oldestTime = query.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
private saveToStorage(): void {
try {
const data = {
queries: Array.from(this.cache.entries()),
timestamp: Date.now()
};
localStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (error) {
console.error('Failed to save query cache:', error);
}
}
private loadFromStorage(): void {
try {
const data = localStorage.getItem(this.storageKey);
if (data) {
const parsed = JSON.parse(data);
this.cache.clear();
for (const [key, query] of parsed.queries) {
this.cache.set(key, query as CachedQuery);
}
}
} catch (error) {
console.error('Failed to load query cache:', error);
}
}
}
/**
* Enhanced query cache with learning capabilities
*/
export class LearningQueryCache extends QueryCacheService {
private queryPatterns: Map<string, {
pattern: string;
examples: string[];
successRate: number;
averageExecutionTime: number;
usageCount: number;
}> = new Map();
/**
* Learn from a successful query
*/
learnFromQuery(
question: string,
cypherQuery: string,
executionTime: number,
success: boolean
): void {
super.addQuery(question, cypherQuery, success ? 0.8 : 0.3, executionTime, 0, success);
if (success) {
const pattern = this.extractQueryPattern(cypherQuery);
this.updateQueryPattern(pattern, question, executionTime);
}
}
/**
* Get query suggestions based on learned patterns
*/
getPatternSuggestions(question: string): QuerySuggestion[] {
const suggestions: QuerySuggestion[] = [];
for (const [pattern, patternData] of this.queryPatterns.entries()) {
if (patternData.successRate > 0.7) {
// Find the best example for this pattern
const bestExample = patternData.examples[0];
if (bestExample) {
const similarity = this.calculateSimilarity(question, bestExample);
if (similarity > 0.6) {
suggestions.push({
cypherQuery: pattern,
confidence: patternData.successRate,
similarity,
sourceQuestion: bestExample,
executionTime: patternData.averageExecutionTime
});
}
}
}
}
return suggestions.sort((a, b) => b.confidence - a.confidence);
}
private extractQueryPattern(cypherQuery: string): string {
// Extract the basic pattern by replacing specific values with placeholders
return cypherQuery
.replace(/['"][^'"]*['"]/g, '?') // Replace strings with ?
.replace(/\d+/g, '?') // Replace numbers with ?
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
private updateQueryPattern(
pattern: string,
question: string,
executionTime: number
): void {
if (!this.queryPatterns.has(pattern)) {
this.queryPatterns.set(pattern, {
pattern,
examples: [question],
successRate: 1.0,
averageExecutionTime: executionTime,
usageCount: 1
});
} else {
const patternData = this.queryPatterns.get(pattern)!;
patternData.examples.push(question);
patternData.usageCount++;
patternData.averageExecutionTime =
(patternData.averageExecutionTime * (patternData.usageCount - 1) + executionTime) /
patternData.usageCount;
// Keep only the most recent examples
if (patternData.examples.length > 5) {
patternData.examples = patternData.examples.slice(-5);
}
}
}
}

View file

@ -8,7 +8,9 @@ import { CypherGenerator } from '../../../ai/cypher-generator.ts';
import { ReActAgent, type ReActResult, type ReActOptions } from '../../../ai/react-agent.ts';
import { KuzuQueryEngine } from '../../../core/graph/kuzu-query-engine.ts';
import { sessionManager, type SessionInfo } from '../../../lib/session-manager.ts';
import { ChatSessionManager } from '../../../lib/chat-history.ts';
import { ChatSessionManager, LocalStorageChatHistory, type ChatHistoryMetadata } from '../../../lib/chat-history.ts';
import { AIMessage } from '@langchain/core/messages';
import { QueryCacheService } from '../../../lib/query-cache.ts';
interface ChatMessage {
id: string;
@ -44,6 +46,12 @@ interface ChatMessage {
totalExecutionTime?: number;
queryExecutionTimes?: Array<{ query: string; time: number }>;
};
cacheSuggestions?: Array<{
cypherQuery: string;
confidence: number;
similarity: number;
sourceQuestion: string;
}>;
};
}
@ -78,11 +86,11 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showReasoning, setShowReasoning] = useState(false);
const [debugMode, setDebugMode] = useState(false);
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [showSessionManager, setShowSessionManager] = useState(false);
const [chatHistory, setChatHistory] = useState<LocalStorageChatHistory | null>(null);
const [queryCache] = useState(() => QueryCacheService.getInstance());
// LLM Configuration
const [llmSettings, setLLMSettings] = useState<LLMSettings>({
@ -111,6 +119,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
try {
await ragOrchestrator.initialize();
// Load existing queries from chat history into cache
await queryCache.loadFromChatHistory();
// Only set context if we have valid graph data
if (graph && graph.nodes && graph.nodes.length > 0) {
// Get or create session
@ -119,6 +130,11 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
sessionId = ChatSessionManager.createSession(projectName);
}
// Initialize chat history
const history = new LocalStorageChatHistory(sessionId);
setChatHistory(history);
setCurrentSessionId(sessionId);
// Set context with graph data
const llmConfig: LLMConfig = {
provider: llmSettings.provider,
@ -138,18 +154,11 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
sessionId
}, llmConfig);
setCurrentSessionId(sessionId);
// Set chat history for conversation context
ragOrchestrator.setChatHistory(history);
// Load conversation history
const conversationHistory = await ragOrchestrator.getConversationHistory();
const chatMessages = conversationHistory.map((msg, index) => ({
id: `history_${index}`,
role: msg.constructor.name === 'HumanMessage' ? 'user' as const : 'assistant' as const,
content: msg.content.toString(),
timestamp: new Date((msg.additional_kwargs?.metadata as any)?.timestamp || Date.now())
}));
setMessages(chatMessages);
// Load conversation history from chat history
await loadConversationHistory(history);
// Load sessions list
refreshSessions();
@ -164,6 +173,24 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
initializeOrchestrator();
}, [graph, fileContents, projectName, ragOrchestrator]);
// Load conversation history from chat history
const loadConversationHistory = async (history: LocalStorageChatHistory) => {
try {
const langchainMessages = await history.getMessages();
const chatMessages = langchainMessages.map((msg, index) => ({
id: `history_${index}`,
role: msg.constructor.name === 'HumanMessage' ? 'user' as const : 'assistant' as const,
content: msg.content.toString(),
timestamp: new Date((msg.additional_kwargs?.metadata as ChatHistoryMetadata)?.timestamp || Date.now()),
metadata: (msg.additional_kwargs?.metadata as ChatHistoryMetadata) || undefined
}));
setMessages(chatMessages);
} catch (error) {
console.error('Failed to load conversation history:', error);
}
};
// Load settings from localStorage on mount
useEffect(() => {
const savedProvider = localStorage.getItem('llm_provider') as LLMProvider;
@ -171,7 +198,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
const savedAzureEndpoint = localStorage.getItem('azure_openai_endpoint');
const savedAzureDeployment = localStorage.getItem('azure_openai_deployment');
const savedAzureApiVersion = localStorage.getItem('azure_openai_api_version');
const savedDebugMode = localStorage.getItem('debug_mode') === 'true';
if (savedProvider || savedApiKey || savedAzureEndpoint) {
setLLMSettings(prev => ({
@ -187,8 +213,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
: (savedProvider ? llmService.getAvailableModels(savedProvider)[0] : prev.model)
}));
}
setDebugMode(savedDebugMode);
}, [llmService]);
// Auto-scroll to bottom when new messages arrive
@ -199,7 +223,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim() || isLoading) return;
if (!inputValue.trim() || isLoading || !chatHistory) return;
// Validate API key
if (!llmSettings.apiKey.trim()) {
@ -241,6 +265,15 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
timestamp: new Date()
};
// Check for cached query suggestions
const cachedSuggestions = queryCache.findSimilarQueries(userMessage.content, {
minSimilarity: 0.8,
maxResults: 3,
minConfidence: 0.7
});
// Save user message to chat history
await chatHistory.addUserMessage(userMessage.content);
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setIsLoading(true);
@ -262,7 +295,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
const ragOptions: ReActOptions = {
maxIterations: 5,
includeReasoning: debugMode || showReasoning, // Always include reasoning in debug mode
includeReasoning: true, // Always include reasoning for better UX
temperature: llmSettings.temperature,
enableQueryCaching: true,
similarityThreshold: 0.8
@ -290,15 +323,15 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
})),
sources: response.sources,
confidence: response.confidence,
reasoning: (debugMode || showReasoning) ? response.reasoning.map(r => ({
reasoning: response.reasoning.map(r => ({
step: r.step,
thought: r.thought,
action: r.action,
actionInput: r.actionInput,
observation: r.observation,
toolResult: r.toolResult
})) : undefined,
debugInfo: debugMode ? {
})),
debugInfo: {
llmConfig,
ragOptions,
contextInfo: {
@ -311,10 +344,51 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
query: q.cypher,
time: 0 // We'd need to instrument the query engine for this
}))
} : undefined
}
}
};
// Save assistant message with metadata to chat history for learning and caching
const metadata: ChatHistoryMetadata = {
cypherQuery: response.cypherQueries.length > 0 ? response.cypherQueries[0].cypher : undefined,
queryResult: response.cypherQueries.length > 0 ? response.cypherQueries as unknown : undefined,
executionTime: executionTime,
timestamp: Date.now(),
confidence: response.confidence,
sources: response.sources
};
await chatHistory.addMessageWithMetadata(
new AIMessage(response.answer),
metadata
);
// Learn from this query for future improvements
if (response.cypherQueries.length > 0) {
const success = response.confidence > 0.5;
queryCache.addQuery(
userMessage.content,
response.cypherQueries[0].cypher,
response.confidence,
executionTime,
0, // We don't have result count
success
);
}
// Add cache suggestions to the message if available
if (cachedSuggestions.length > 0) {
assistantMessage.metadata = {
...assistantMessage.metadata,
cacheSuggestions: cachedSuggestions.map(s => ({
cypherQuery: s.cypherQuery,
confidence: s.confidence,
similarity: s.similarity,
sourceQuestion: s.sourceQuestion
}))
};
}
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
@ -325,6 +399,8 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
timestamp: new Date()
};
// Save error message to chat history
await chatHistory.addAIChatMessage(errorMessage.content);
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
@ -360,6 +436,10 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
if (sessionManager.switchToSession(sessionId)) {
setCurrentSessionId(sessionId);
// Initialize new chat history
const history = new LocalStorageChatHistory(sessionId);
setChatHistory(history);
// Load conversation history for the new session
const llmConfig: LLMConfig = {
provider: llmSettings.provider,
@ -379,15 +459,11 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
sessionId
}, llmConfig);
const conversationHistory = await ragOrchestrator.getConversationHistory();
const chatMessages = conversationHistory.map((msg, index) => ({
id: `history_${index}`,
role: msg.constructor.name === 'HumanMessage' ? 'user' as const : 'assistant' as const,
content: msg.content.toString(),
timestamp: new Date((msg.additional_kwargs?.metadata as any)?.timestamp || Date.now())
}));
// Set chat history for conversation context
ragOrchestrator.setChatHistory(history);
setMessages(chatMessages);
// Load conversation history from chat history
await loadConversationHistory(history);
refreshSessions();
}
};
@ -411,301 +487,153 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
// Clear conversation
const clearConversation = async () => {
if (confirm('Are you sure you want to clear this conversation?')) {
await ragOrchestrator.clearConversationHistory();
if (confirm('Are you sure you want to clear this conversation?') && chatHistory) {
await chatHistory.clear();
setMessages([]);
}
};
// Toggle debug mode and save to localStorage
const toggleDebugMode = () => {
const newDebugMode = !debugMode;
setDebugMode(newDebugMode);
localStorage.setItem('debug_mode', newDebugMode.toString());
};
// Reasoning Component for Assistant Messages
const ReasoningSection: React.FC<{ reasoning: NonNullable<ChatMessage['metadata']>['reasoning'] }> = ({ reasoning }) => {
const [isExpanded, setIsExpanded] = useState(false);
// Graph diagnostics
const runGraphDiagnostics = () => {
const contextInfo = {
nodeCount: 0, // TODO: Implement getContextInfo
fileCount: fileContents.size,
hasContext: true
};
if (!contextInfo.hasContext) {
alert('No graph loaded yet. Please load a repository first.');
return;
}
// Basic statistics
const stats = [
`📊 Graph Statistics:`,
`• Nodes: ${contextInfo.nodeCount}`,
`• Files: ${contextInfo.fileCount}`,
``,
`🔍 Diagnostic Tips:`,
`• Check browser console for detailed ingestion logs`,
`• Look for warnings about isolated nodes or parsing failures`,
`• Verify that source files contain recognizable functions/classes`,
``,
`If you see isolated nodes:`,
`1. Check if files failed to parse (console warnings)`,
`2. Ensure files contain valid code syntax`,
`3. Check if file extensions are supported (.js, .ts, .py, etc.)`,
`4. Look for import/export syntax errors`
].join('\n');
alert(stats);
// Also log to console for more details
console.log('🔍 Graph Diagnostics Requested');
console.log('Context Info:', contextInfo);
};
// Debug Panel Component
const DebugPanel: React.FC<{ message: ChatMessage }> = ({ message }) => {
if (!message.metadata?.debugInfo && !message.metadata?.reasoning && !message.metadata?.cypherQueries) {
if (!reasoning || reasoning.length === 0) {
return null;
}
const [activeTab, setActiveTab] = useState<'reasoning' | 'queries' | 'config' | 'context'>('reasoning');
return (
<div className="mt-4 border border-gray-200 rounded-lg bg-gray-50">
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-4" aria-label="Tabs">
{message.metadata?.reasoning && (
<button
onClick={() => setActiveTab('reasoning')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'reasoning'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Reasoning Steps ({message.metadata.reasoning.length})
</button>
)}
{message.metadata?.cypherQueries && message.metadata.cypherQueries.length > 0 && (
<button
onClick={() => setActiveTab('queries')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'queries'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Cypher Queries ({message.metadata.cypherQueries.length})
</button>
)}
{message.metadata?.debugInfo && (
<>
<button
onClick={() => setActiveTab('config')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'config'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Configuration
</button>
<button
onClick={() => setActiveTab('context')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'context'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Context Info
</button>
</>
)}
</nav>
</div>
<div className="p-4">
{/* Reasoning Steps Tab */}
{activeTab === 'reasoning' && message.metadata?.reasoning && (
<div className="space-y-4">
{message.metadata.reasoning.map((step, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4 bg-white">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900">Step {step.step}</h4>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
step.toolResult?.success
? 'bg-green-100 text-green-800'
: step.toolResult?.success === false
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-800'
}`}>
{step.action}
</span>
<div style={{
marginTop: '12px',
borderTop: '1px solid rgba(0,0,0,0.1)',
paddingTop: '12px'
}}>
<button
onClick={() => setIsExpanded(!isExpanded)}
style={{
background: 'none',
border: 'none',
color: '#007bff',
cursor: 'pointer',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '0',
textDecoration: 'underline'
}}
>
{isExpanded ? '▼' : '▶'} Show thought process ({reasoning.length} steps)
</button>
{isExpanded && (
<div style={{
marginTop: '8px',
padding: '12px',
backgroundColor: 'rgba(0,123,255,0.05)',
borderRadius: '6px',
border: '1px solid rgba(0,123,255,0.2)'
}}>
<div style={{ fontSize: '11px', color: '#666', marginBottom: '8px', fontWeight: '500' }}>
🤔 Reasoning Process:
</div>
{reasoning.map((step, index: number) => (
<div key={index} style={{
marginBottom: index < reasoning.length - 1 ? '12px' : '0',
padding: '8px',
backgroundColor: 'rgba(255,255,255,0.7)',
borderRadius: '4px',
border: '1px solid rgba(0,0,0,0.1)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '6px',
fontSize: '11px'
}}>
<span style={{
backgroundColor: '#007bff',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontWeight: 'bold'
}}>
{step.step}
</span>
<span style={{
backgroundColor: step.toolResult?.success
? 'rgba(40,167,69,0.2)'
: step.toolResult?.success === false
? 'rgba(220,53,69,0.2)'
: 'rgba(108,117,125,0.2)',
color: step.toolResult?.success
? '#28a745'
: step.toolResult?.success === false
? '#dc3545'
: '#6c757d',
padding: '2px 6px',
borderRadius: '12px',
fontSize: '10px',
fontWeight: '500'
}}>
{step.action}
</span>
</div>
<div style={{ fontSize: '12px', lineHeight: '1.4' }}>
<div style={{ marginBottom: '4px' }}>
<strong>Thought:</strong> {step.thought}
</div>
<div className="space-y-3">
<div>
<h5 className="font-medium text-gray-700 mb-1">Thought:</h5>
<p className="text-gray-600 text-sm bg-blue-50 p-2 rounded">{step.thought}</p>
{step.actionInput && (
<div style={{ marginBottom: '4px' }}>
<strong>Input:</strong>
<code style={{
backgroundColor: 'rgba(0,0,0,0.1)',
padding: '1px 4px',
borderRadius: '2px',
fontSize: '11px',
marginLeft: '4px'
}}>
{step.actionInput}
</code>
</div>
{step.actionInput && (
<div>
<h5 className="font-medium text-gray-700 mb-1">Action Input:</h5>
<p className="text-gray-600 text-sm bg-yellow-50 p-2 rounded font-mono">{step.actionInput}</p>
</div>
)}
{step.observation && (
<div>
<h5 className="font-medium text-gray-700 mb-1">Observation:</h5>
<div className="text-gray-600 text-sm bg-green-50 p-2 rounded overflow-x-auto">
<MarkdownContent content={step.observation} role="assistant" />
</div>
</div>
)}
{step.toolResult && (
<div className="border-t pt-3">
<h5 className="font-medium text-gray-700 mb-2">Tool Result:</h5>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Tool:</span> {step.toolResult.toolName}
</div>
<div>
<span className="font-medium">Success:</span>
<span className={`ml-1 ${step.toolResult.success ? 'text-green-600' : 'text-red-600'}`}>
{step.toolResult.success ? 'Yes' : 'No'}
</span>
</div>
{step.toolResult.error && (
<div className="col-span-2">
<span className="font-medium text-red-600">Error:</span>
<p className="text-red-600 bg-red-50 p-2 rounded mt-1">{step.toolResult.error}</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Cypher Queries Tab */}
{activeTab === 'queries' && message.metadata?.cypherQueries && (
<div className="space-y-4">
{message.metadata.cypherQueries.map((query, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4 bg-white">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-gray-900">Query {index + 1}</h4>
{query.confidence && (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
query.confidence > 0.8
? 'bg-green-100 text-green-800'
: query.confidence > 0.6
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
Confidence: {(query.confidence * 100).toFixed(0)}%
</span>
)}
</div>
)}
<div className="space-y-3">
<div>
<h5 className="font-medium text-gray-700 mb-2">Cypher Query:</h5>
<pre className="bg-gray-900 text-green-400 p-3 rounded-lg text-sm overflow-x-auto font-mono">
{query.cypher}
</pre>
{step.observation && (
<div style={{
marginTop: '6px',
padding: '6px',
backgroundColor: 'rgba(40,167,69,0.1)',
borderRadius: '3px',
fontSize: '11px'
}}>
<strong>Result:</strong> {step.observation}
</div>
<div>
<h5 className="font-medium text-gray-700 mb-2">Explanation:</h5>
<div className="text-gray-600 text-sm bg-blue-50 p-3 rounded">
<MarkdownContent content={query.explanation} role="assistant" />
</div>
)}
{step.toolResult && step.toolResult.error && (
<div style={{
marginTop: '6px',
padding: '6px',
backgroundColor: 'rgba(220,53,69,0.1)',
borderRadius: '3px',
fontSize: '11px',
color: '#dc3545'
}}>
<strong>Error:</strong> {step.toolResult.error}
</div>
</div>
</div>
))}
</div>
)}
{/* Configuration Tab */}
{activeTab === 'config' && message.metadata?.debugInfo && (
<div className="space-y-6">
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<h4 className="font-semibold text-gray-900 mb-3">LLM Configuration</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div><span className="font-medium">Provider:</span> {message.metadata.debugInfo.llmConfig.provider}</div>
<div><span className="font-medium">Model:</span> {message.metadata.debugInfo.llmConfig.model}</div>
<div><span className="font-medium">Temperature:</span> {message.metadata.debugInfo.llmConfig.temperature}</div>
<div><span className="font-medium">Max Tokens:</span> {message.metadata.debugInfo.llmConfig.maxTokens}</div>
)}
</div>
</div>
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<h4 className="font-semibold text-gray-900 mb-3">RAG Options</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div><span className="font-medium">Max Iterations:</span> {message.metadata.debugInfo.ragOptions.maxIterations}</div>
<div><span className="font-medium">Include Reasoning:</span> {message.metadata.debugInfo.ragOptions.includeReasoning ? 'Yes' : 'No'}</div>
<div><span className="font-medium">Strict Mode:</span> {message.metadata.debugInfo.ragOptions.strictMode ? 'Yes' : 'No'}</div>
<div><span className="font-medium">Temperature:</span> {message.metadata.debugInfo.ragOptions.temperature}</div>
</div>
</div>
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<h4 className="font-semibold text-gray-900 mb-3">Performance</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div><span className="font-medium">Total Execution Time:</span> {message.metadata.debugInfo.totalExecutionTime}ms</div>
<div><span className="font-medium">Confidence Score:</span> {message.metadata.confidence ? (message.metadata.confidence * 100).toFixed(1) + '%' : 'N/A'}</div>
</div>
</div>
</div>
)}
{/* Context Info Tab */}
{activeTab === 'context' && message.metadata?.debugInfo && (
<div className="space-y-4">
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<h4 className="font-semibold text-gray-900 mb-3">Knowledge Graph Context</h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="text-center p-4 bg-blue-50 rounded">
<div className="text-2xl font-bold text-blue-600">{message.metadata.debugInfo.contextInfo.nodeCount}</div>
<div className="text-gray-600">Graph Nodes</div>
</div>
<div className="text-center p-4 bg-green-50 rounded">
<div className="text-2xl font-bold text-green-600">{message.metadata.debugInfo.contextInfo.fileCount}</div>
<div className="text-gray-600">Files Indexed</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded">
<div className="text-2xl font-bold text-purple-600">{message.metadata.sources?.length || 0}</div>
<div className="text-gray-600">Sources Used</div>
</div>
</div>
</div>
{message.metadata.sources && message.metadata.sources.length > 0 && (
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<h4 className="font-semibold text-gray-900 mb-3">Sources Referenced</h4>
<div className="space-y-2">
{message.metadata.sources.map((source, index) => (
<div key={index} className="flex items-center space-x-2 text-sm">
<span className="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center text-xs font-medium">
{index + 1}
</span>
<code className="bg-gray-100 px-2 py-1 rounded text-gray-800">{source}</code>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
);
};
@ -952,43 +880,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
>
New
</button>
<button
onClick={() => setShowReasoning(!showReasoning)}
style={{
...buttonStyle,
backgroundColor: showReasoning ? '#28a745' : '#6c757d',
fontSize: '12px',
padding: '6px 12px'
}}
title="Toggle reasoning display"
>
🧠 Reasoning
</button>
<button
onClick={toggleDebugMode}
style={{
...buttonStyle,
backgroundColor: debugMode ? '#17a2b8' : '#6c757d',
fontSize: '12px',
padding: '6px 12px'
}}
title="Toggle debug mode - shows detailed internal workings"
>
🔍 Debug
</button>
<button
onClick={runGraphDiagnostics}
style={{
...buttonStyle,
backgroundColor: '#ffc107',
color: '#000',
fontSize: '12px',
padding: '6px 12px'
}}
title="Run graph diagnostics to check for issues"
>
🩺 Diagnose
</button>
<button
onClick={() => setShowSettings(!showSettings)}
style={{ ...buttonStyle, fontSize: '12px', padding: '6px 12px' }}
@ -1340,14 +1231,14 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
<MarkdownContent content={message.content} role={message.role} />
</div>
{/* Debug Panel (when debug mode is enabled) */}
{debugMode && message.role === 'assistant' && (
<DebugPanel message={message} />
{/* Reasoning Section for Assistant Messages */}
{message.role === 'assistant' && message.metadata?.reasoning && (
<ReasoningSection reasoning={message.metadata.reasoning} />
)}
{/* Simple Metadata (when debug mode is disabled) */}
{!debugMode && message.metadata && (
<div style={{ fontSize: '12px', opacity: 0.8 }}>
{/* Simple Metadata */}
{message.metadata && (
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '8px' }}>
{message.metadata.confidence && (
<div style={{ marginBottom: '4px' }}>
Confidence: {Math.round(message.metadata.confidence * 100)}%
@ -1362,7 +1253,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
{message.metadata.cypherQueries && message.metadata.cypherQueries.length > 0 && (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>View Queries ({message.metadata.cypherQueries.length})</summary>
<summary style={{ cursor: 'pointer', fontSize: '11px' }}>
View Queries ({message.metadata.cypherQueries.length})
</summary>
{message.metadata.cypherQueries.map((query, index) => (
<div key={index} style={{
marginTop: '4px',
@ -1378,24 +1271,6 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
))}
</details>
)}
{message.metadata.reasoning && message.metadata.reasoning.length > 0 && (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>View Reasoning ({message.metadata.reasoning.length} steps)</summary>
{message.metadata.reasoning.map((step, index) => (
<div key={index} style={{
marginTop: '4px',
padding: '8px',
backgroundColor: 'rgba(0,0,0,0.1)',
borderRadius: '4px',
fontSize: '11px'
}}>
<div><strong>Step {step.step}:</strong> {step.thought}</div>
<div><strong>Action:</strong> {step.action}</div>
</div>
))}
</details>
)}
</div>
)}
</div>