aiAssistant-enhanced.ts•12.9 kB
/**
* Enhanced AI Assistant Service with MCP Integration
* Smart AI that proposes actions with human approval workflow
* Now uses MCP server for reliable Gemini API calls with fallback
*/
import { supabase } from '../src/supabaseConfig';
import { geminiAPIManager } from './geminiAPIManager';
import { offlineDB } from './offlineService';
import { mcpIntegration } from './mcpIntegration';
interface AIProposal {
id: string;
type: 'expense_categorization' | 'stock_adjustment' | 'price_optimization' | 'reorder_suggestion' | 'process_improvement';
title: string;
description: string;
proposed_action: any;
confidence: number;
reasoning: string;
data_source: any;
status: 'pending' | 'approved' | 'rejected' | 'expired';
created_at: string;
created_by: 'ai_assistant' | 'mcp_assistant';
expires_at: string;
mcp_used?: boolean;
}
interface ApprovalRequest {
proposal_id: string;
human_decision: 'approve' | 'reject';
human_notes?: string;
approved_by: string;
approved_at: string;
}
class EnhancedAIAssistantService {
/**
* Main AI Assistant - analyzes data and proposes helpful actions
* Enhanced with MCP server integration for reliability
*/
async analyzeAndPropose(): Promise<AIProposal[]> {
console.log('🤖 Enhanced AI Assistant analyzing business data for improvement opportunities...');
const proposals: AIProposal[] = [];
try {
// Check MCP server health first
const mcpHealth = await mcpIntegration.healthCheck();
console.log(`🔗 MCP server status: ${mcpHealth.healthy ? 'healthy' : 'unhealthy'} (${mcpHealth.latency}ms)`);
// Use MCP for comprehensive analysis if available
if (mcpHealth.healthy) {
const mcpProposals = await this.getMCPProposals();
if (mcpProposals.length > 0) {
proposals.push(...mcpProposals);
console.log(`✨ MCP generated ${mcpProposals.length} proposals`);
}
}
// Always run traditional analysis for comparison/fallback
const traditionalProposals = await this.getTraditionalProposals();
proposals.push(...traditionalProposals);
console.log(`🔧 Traditional analysis generated ${traditionalProposals.length} proposals`);
// Remove duplicates (prefer MCP proposals)
const uniqueProposals = this.deduplicateProposals(proposals);
// Store all proposals for human review
await this.storeProposals(uniqueProposals);
console.log(`✅ Enhanced AI Assistant generated ${uniqueProposals.length} improvement proposals`);
return uniqueProposals;
} catch (error) {
console.error('❌ Enhanced AI Assistant analysis failed:', error);
return [];
}
}
/**
* Get proposals using MCP server
*/
async getMCPProposals(): Promise<AIProposal[]> {
try {
// Gather business context for MCP
const businessContext = await this.gatherBusinessContext();
const { proposals, usedMCP } = await mcpIntegration.getAIProposals({
context: businessContext,
analysis_type: 'comprehensive',
include_confidence: true,
max_proposals: 10
});
if (!usedMCP || !proposals) {
console.warn('MCP proposals failed, falling back to traditional');
return [];
}
// Convert MCP proposals to our format
return proposals.map((mcpProposal: any, index: number) => ({
id: `mcp_${Date.now()}_${index}`,
type: this.mapMCPProposalType(mcpProposal.type),
title: mcpProposal.title || 'MCP Proposal',
description: mcpProposal.description || 'AI-generated proposal via MCP',
proposed_action: mcpProposal.action,
confidence: mcpProposal.confidence || 80,
reasoning: mcpProposal.reasoning || 'Generated by MCP server',
data_source: mcpProposal.data_source,
status: 'pending',
created_at: new Date().toISOString(),
created_by: 'mcp_assistant',
expires_at: this.getExpiryDate(7),
mcp_used: true
}));
} catch (error) {
console.warn('MCP proposals generation failed:', error);
return [];
}
}
/**
* Get proposals using traditional methods (fallback)
*/
async getTraditionalProposals(): Promise<AIProposal[]> {
const proposals: AIProposal[] = [];
try {
// Check for uncategorized expenses
const expenseProposals = await this.proposeExpenseCategorization();
proposals.push(...expenseProposals);
// Check for stock issues
const stockProposals = await this.proposeStockAdjustments();
proposals.push(...stockProposals);
// Check for pricing opportunities
const priceProposals = await this.proposePriceOptimizations();
proposals.push(...priceProposals);
// Check for process improvements
const processProposals = await this.proposeProcessImprovements();
proposals.push(...processProposals);
return proposals;
} catch (error) {
console.error('Traditional proposals failed:', error);
return [];
}
}
/**
* Enhanced expense categorization with MCP fallback
*/
async proposeExpenseCategorization(): Promise<AIProposal[]> {
const proposals: AIProposal[] = [];
try {
// Get uncategorized expenses
const { data: uncategorizedExpenses } = await supabase
.from('expenses')
.select('*')
.or('category.is.null,category.eq.""')
.limit(10);
if (!uncategorizedExpenses || uncategorizedExpenses.length === 0) {
return proposals;
}
for (const expense of uncategorizedExpenses) {
let categorization;
let usedMCP = false;
// Try MCP first
if (mcpIntegration.isAvailable()) {
const mcpResult = await mcpIntegration.callTool('analyze_expense', {
expense_data: expense,
operation: 'categorize'
});
if (mcpResult.success) {
categorization = mcpResult.data;
usedMCP = true;
}
}
// Fallback to traditional method
if (!categorization) {
categorization = await this.suggestExpenseCategory(expense);
}
if (categorization && categorization.confidence > 70) {
proposals.push({
id: `exp_cat_${expense.id}_${Date.now()}`,
type: 'expense_categorization',
title: `Categorize "${expense.description}"`,
description: `AI suggests categorizing this ₱${expense.amount} expense as "${categorization.category}"`,
proposed_action: {
expense_id: expense.id,
new_category: categorization.category,
confidence: categorization.confidence
},
confidence: categorization.confidence,
reasoning: categorization.reasoning,
data_source: expense,
status: 'pending',
created_at: new Date().toISOString(),
created_by: usedMCP ? 'mcp_assistant' : 'ai_assistant',
expires_at: this.getExpiryDate(7),
mcp_used: usedMCP
});
}
}
return proposals;
} catch (error) {
console.warn('Expense categorization proposals failed:', error);
return [];
}
}
/**
* Enhanced stock analysis with MCP integration
*/
async proposeStockAdjustments(): Promise<AIProposal[]> {
const proposals: AIProposal[] = [];
try {
// Get current stock and sales data
const [{ data: products }, { data: recentSales }] = await Promise.all([
supabase.from('products').select('*'),
supabase.from('sales').select('*')
.gte('date', this.getDaysAgo(30))
.order('date', { ascending: false })
]);
if (!products) return proposals;
let stockAnalysis;
let usedMCP = false;
// Try MCP first for comprehensive analysis
if (mcpIntegration.isAvailable()) {
const mcpResult = await mcpIntegration.applyStockPattern('reorder_analysis', {
products,
sales_data: recentSales || [],
timeframe: '30d'
});
if (mcpResult.success) {
stockAnalysis = mcpResult.result;
usedMCP = true;
}
}
// Fallback to traditional analysis
if (!stockAnalysis) {
stockAnalysis = await this.analyzeStockNeeds(products, recentSales || []);
}
for (const suggestion of stockAnalysis.suggestions || []) {
if (suggestion.confidence > 75) {
proposals.push({
id: `stock_adj_${suggestion.product_id}_${Date.now()}`,
type: 'stock_adjustment',
title: `${suggestion.action} ${suggestion.product_name}`,
description: suggestion.description,
proposed_action: suggestion,
confidence: suggestion.confidence,
reasoning: suggestion.reasoning,
data_source: { products, sales: recentSales },
status: 'pending',
created_at: new Date().toISOString(),
created_by: usedMCP ? 'mcp_assistant' : 'ai_assistant',
expires_at: this.getExpiryDate(3),
mcp_used: usedMCP
});
}
}
return proposals;
} catch (error) {
console.warn('Stock adjustment proposals failed:', error);
return [];
}
}
/**
* Business context gathering for MCP
*/
async gatherBusinessContext(): Promise<any> {
try {
const [
{ data: recentSales },
{ data: recentExpenses },
{ data: products },
{ data: recentNotes }
] = await Promise.all([
supabase.from('sales').select('*').gte('date', this.getDaysAgo(30)).limit(100),
supabase.from('expenses').select('*').gte('date', this.getDaysAgo(30)).limit(100),
supabase.from('products').select('*'),
supabase.from('notes').select('*').gte('created_at', this.getDaysAgo(7)).limit(50)
]);
return {
recent_sales: recentSales || [],
recent_expenses: recentExpenses || [],
products: products || [],
recent_notes: recentNotes || [],
timeframe: '30d',
business_type: 'chicken_processing'
};
} catch (error) {
console.warn('Failed to gather business context:', error);
return {};
}
}
/**
* Remove duplicate proposals, preferring MCP ones
*/
deduplicateProposals(proposals: AIProposal[]): AIProposal[] {
const seen = new Map<string, AIProposal>();
for (const proposal of proposals) {
const key = `${proposal.type}_${proposal.title}`;
const existing = seen.get(key);
if (!existing || proposal.mcp_used) {
seen.set(key, proposal);
}
}
return Array.from(seen.values());
}
/**
* Map MCP proposal types to our internal types
*/
mapMCPProposalType(mcpType: string): AIProposal['type'] {
const mapping: Record<string, AIProposal['type']> = {
'expense_categorization': 'expense_categorization',
'stock_adjustment': 'stock_adjustment',
'reorder_suggestion': 'reorder_suggestion',
'price_optimization': 'price_optimization',
'process_improvement': 'process_improvement'
};
return mapping[mcpType] || 'process_improvement';
}
// Traditional methods remain unchanged for fallback
async suggestExpenseCategory(expense: any): Promise<any> {
// Existing implementation...
return null;
}
async analyzeStockNeeds(products: any[], sales: any[]): Promise<any> {
// Existing implementation...
return { suggestions: [] };
}
async proposePriceOptimizations(): Promise<AIProposal[]> {
// Existing implementation...
return [];
}
async proposeProcessImprovements(): Promise<AIProposal[]> {
// Existing implementation...
return [];
}
async storeProposals(proposals: AIProposal[]): Promise<void> {
// Store proposals in database with MCP tracking
for (const proposal of proposals) {
await supabase.from('ai_proposals').insert({
...proposal,
metadata: {
mcp_used: proposal.mcp_used,
created_by: proposal.created_by
}
});
}
}
getDaysAgo(days: number): string {
const date = new Date();
date.setDate(date.getDate() - days);
return date.toISOString();
}
getExpiryDate(days: number): string {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString();
}
}
// Export enhanced service
export const enhancedAIAssistant = new EnhancedAIAssistantService();
// Backward compatibility - export original interface
export { AIProposal, ApprovalRequest };