mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
structured output fix wip, ChatInterface better UX
This commit is contained in:
parent
adf8810101
commit
889beff56f
7 changed files with 913 additions and 28098 deletions
27658
repomix-output.txt
27658
repomix-output.txt
File diff suppressed because it is too large
Load diff
47
src/ai/react-agent-debug.ts
Normal file
47
src/ai/react-agent-debug.ts
Normal 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' };
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
398
src/lib/query-cache.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue