Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/src/components/email-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ export function EmailList({ emails, loading }: EmailListProps) {
{email.preview}
</p>
<div className="flex items-center space-x-2 mt-2">
{email.category && (
{email.categoryData && (
<Badge
variant="secondary"
className={`text-xs ${getCategoryBadgeColor(email.category.name)}`}
className={`text-xs ${getCategoryBadgeColor(email.categoryData.name)}`}
>
{email.category.name}
{email.categoryData.name}
</Badge>
)}
{email.labels?.map((label, index) => (
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"postcss": "^8.4.47",
"tailwindcss": "^3.4.17",
"tsx": "^4.19.1",
"typescript": "5.6.3",
"typescript": "^5.6.3",
"vite": "^5.4.14"
},
"optionalDependencies": {
Expand Down
21 changes: 16 additions & 5 deletions server/ai-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class HuggingFaceModel implements FreeLanguageModel {
]);

return {
sentiment: sentiment as "positive" | "negative" | "neutral",
sentiment: sentiment.sentiment,
categories: classification.categories,
confidence: Math.min(sentiment.confidence || 0.5, classification.confidence || 0.5),
keywords: this.extractKeywords(text),
Expand All @@ -61,7 +61,7 @@ class HuggingFaceModel implements FreeLanguageModel {
}
}

private async analyzeSentiment(text: string) {
private async analyzeSentiment(text: string): Promise<{ sentiment: "positive" | "negative" | "neutral", confidence: number }> {
// Simulate sentiment analysis using pattern matching
const positiveWords = ["good", "great", "excellent", "happy", "pleased", "thank", "wonderful"];
const negativeWords = ["bad", "terrible", "awful", "angry", "disappointed", "upset", "problem", "issue"];
Expand All @@ -70,9 +70,20 @@ class HuggingFaceModel implements FreeLanguageModel {
const positiveCount = words.filter(word => positiveWords.some(pos => word.includes(pos))).length;
const negativeCount = words.filter(word => negativeWords.some(neg => word.includes(neg))).length;

if (positiveCount > negativeCount) return "positive";
if (negativeCount > positiveCount) return "negative";
return "neutral";
let sentiment: "positive" | "negative" | "neutral";
let confidence: number;

if (positiveCount > negativeCount) {
sentiment = "positive";
confidence = Math.min(0.5 + (positiveCount - negativeCount) * 0.1, 0.9); // Confidence increases with more positive words
} else if (negativeCount > positiveCount) {
sentiment = "negative";
confidence = Math.min(0.5 + (negativeCount - positiveCount) * 0.1, 0.9); // Confidence increases with more negative words
} else {
sentiment = "neutral";
confidence = 0.6; // Neutral sentiment has a base confidence
}
return { sentiment, confidence };
}

private async classifyText(text: string) {
Expand Down
10 changes: 5 additions & 5 deletions server/gmail-ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,18 @@ class GmailAIService {

return {
success: true,
processedCount: result.newEmails || 0,
emails: [],
processedCount: result.synced || 0, // Use result.synced (number)
emails: result.newEmails, // Populate with actual new emails
batchInfo: {
batchId: `smart_${Date.now()}`,
queryFilter: strategies.join(','),
timestamp: new Date().toISOString()
},
statistics: {
totalProcessed: result.newEmails || 0,
successfulExtractions: result.newEmails || 0,
totalProcessed: result.synced || 0, // Use result.synced
successfulExtractions: result.synced || 0, // Use result.synced
failedExtractions: 0,
aiAnalysesCompleted: result.newEmails || 0,
aiAnalysesCompleted: result.synced || 0, // Use result.synced (assuming all are analyzed)
lastSync: new Date().toISOString()
Comment on lines +218 to 230
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

String interpolation still uses the array, not the count

processedCount now correctly references result.synced, but the activity log above (line 210) still interpolates ${result.newEmails}, which will stringify the full array to "[object Object]".

-        details: `${result.newEmails} emails retrieved using ${strategies.length || 'default'} strategies`,
+        details: `${result.synced} emails retrieved using ${strategies.length || 'default'} strategies`,

Adjust to avoid noisy logs and ensure monitoring dashboards receive the intended numeric value.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In server/gmail-ai-service.ts around lines 218 to 230, the activity log uses
string interpolation with result.newEmails, which is an array, causing it to log
as "[object Object]". Change the interpolation to use the count of new emails
instead by replacing `${result.newEmails}` with `${result.newEmails.length}` or
the appropriate numeric count to ensure the log outputs a meaningful number
rather than the array object.

}
};
Expand Down
51 changes: 41 additions & 10 deletions server/python-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { spawn } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import type { AIAnalysis, AccuracyValidation } from './ai-engine'; // Import backend types

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export interface PythonNLPResult {
// This interface represents the direct output from the Python script
interface PythonScriptOutput {
topic: string;
sentiment: 'positive' | 'negative' | 'neutral';
intent: string;
Expand All @@ -17,21 +19,44 @@ export interface PythonNLPResult {
suggested_labels: string[];
risk_flags: string[];
validation: {
validation_method: string;
validation_method: string; // Note: snake_case from Python
score: number;
reliable: boolean;
feedback: string;
};
}

export type MappedNLPResult = AIAnalysis & { validation: AccuracyValidation };

export class PythonNLPBridge {
private pythonScriptPath: string;

constructor() {
this.pythonScriptPath = path.join(__dirname, 'python_nlp', 'nlp_engine.py');
}

async analyzeEmail(subject: string, content: string): Promise<PythonNLPResult> {
private mapPythonOutputToNLPResult(pyOutput: PythonScriptOutput): MappedNLPResult {
return {
topic: pyOutput.topic,
sentiment: pyOutput.sentiment,
intent: pyOutput.intent,
urgency: pyOutput.urgency,
confidence: pyOutput.confidence,
categories: pyOutput.categories,
keywords: pyOutput.keywords,
reasoning: pyOutput.reasoning,
suggestedLabels: pyOutput.suggested_labels, // map snake_case to camelCase
riskFlags: pyOutput.risk_flags, // map snake_case to camelCase
validation: {
validationMethod: pyOutput.validation.validation_method as AccuracyValidation['validationMethod'], // map snake_case to camelCase and assert type
score: pyOutput.validation.score,
reliable: pyOutput.validation.reliable,
feedback: pyOutput.validation.feedback,
},
};
}

async analyzeEmail(subject: string, content: string): Promise<MappedNLPResult> {
return new Promise((resolve, reject) => {
const python = spawn('python3', [this.pythonScriptPath, subject, content], {
stdio: ['pipe', 'pipe', 'pipe']
Expand All @@ -51,14 +76,13 @@ export class PythonNLPBridge {
python.on('close', (code) => {
if (code !== 0) {
console.error('Python NLP Error:', errorOutput);
// Fallback to JavaScript analysis
resolve(this.getFallbackAnalysis(subject, content));
return;
}

try {
const result = JSON.parse(output.trim());
resolve(result);
const result: PythonScriptOutput = JSON.parse(output.trim());
resolve(this.mapPythonOutputToNLPResult(result));
} catch (parseError) {
console.error('Failed to parse Python NLP output:', parseError);
resolve(this.getFallbackAnalysis(subject, content));
Expand All @@ -72,7 +96,7 @@ export class PythonNLPBridge {
});
}

private getFallbackAnalysis(subject: string, content: string): PythonNLPResult {
private getFallbackAnalysis(subject: string, content: string): MappedNLPResult {
// Enhanced JavaScript fallback with better pattern matching
const fullText = `${subject}\n\n${content}`.toLowerCase();

Expand All @@ -82,6 +106,12 @@ export class PythonNLPBridge {
const urgency = this.assessUrgencyFallback(fullText);
const keywords = this.extractKeywordsFallback(fullText);

// const categories = this.categorizeWithPatterns(fullText); // Removed duplicate
// const sentiment = this.analyzeSentimentFallback(fullText); // Removed duplicate
// const intent = this.detectIntentFallback(fullText); // Removed duplicate
// const urgency = this.assessUrgencyFallback(fullText); // Removed duplicate
// const keywords = this.extractKeywordsFallback(fullText); // Removed duplicate

return {
topic: categories[0] || 'General Communication',
sentiment,
Expand All @@ -91,10 +121,10 @@ export class PythonNLPBridge {
categories,
keywords,
reasoning: 'JavaScript fallback analysis with enhanced pattern matching',
suggested_labels: [...categories, intent.replace('_', ' ')].filter(Boolean),
risk_flags: urgency === 'critical' ? ['urgent_content'] : [],
suggestedLabels: [...categories, intent.replace('_', ' ')].filter(Boolean),
riskFlags: urgency === 'critical' ? ['urgent_content'] : [],
validation: {
validation_method: 'javascript_fallback',
validationMethod: 'javascript_fallback' as AccuracyValidation['validationMethod'],
score: 0.65,
reliable: true,
feedback: 'Analysis completed using JavaScript fallback with good reliability'
Expand Down Expand Up @@ -259,6 +289,7 @@ export class PythonNLPBridge {
async testConnection(): Promise<boolean> {
try {
const result = await this.analyzeEmail('Test Subject', 'Test content for connection verification.');
// Access reliable from the mapped structure
return result.validation.reliable;
} catch (error) {
console.error('Python NLP connection test failed:', error);
Expand Down
36 changes: 20 additions & 16 deletions server/routes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { insertEmailSchema, insertCategorySchema, insertActivitySchema } from "@shared/schema";
import { pythonNLP } from "./python-bridge";
import { insertEmailSchema, insertCategorySchema, insertActivitySchema, type EmailWithCategory } from "@shared/schema";
import { pythonNLP, type MappedNLPResult } from "./python-bridge"; // Import MappedNLPResult
import { gmailAIService } from "./gmail-ai-service";
import { z } from "zod";
import type { AIAnalysis, AccuracyValidation } from "./ai-engine"; // Added AccuracyValidation

export async function registerRoutes(app: Express): Promise<Server> {
// Dashboard stats
Expand Down Expand Up @@ -188,15 +189,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(404).json({ message: "Email not found" });
}

let analysis;
let analysis: MappedNLPResult | undefined; // Updated type to MappedNLPResult
if (autoAnalyze) {
// Use advanced AI analysis
analysis = await pythonNLP.analyzeEmail(email.subject, email.content);

// Find matching category
const categories = await storage.getAllCategories();
// analysis is now MappedNLPResult, which includes 'validation'
// and has camelCase 'suggestedLabels' and 'riskFlags'
const matchingCategory = categories.find(cat =>
analysis.categories.some(aiCat =>
analysis!.categories.some((aiCat: string) =>
cat.name.toLowerCase().includes(aiCat.toLowerCase()) ||
aiCat.toLowerCase().includes(cat.name.toLowerCase())
)
Expand All @@ -206,38 +209,39 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Update email with AI analysis results
const updatedEmail = await storage.updateEmail(emailId, {
categoryId: matchingCategory.id,
confidence: Math.round(analysis.confidence * 100),
labels: analysis.suggested_labels,
confidence: Math.round(analysis!.confidence * 100), // Used non-null assertion
labels: analysis!.suggestedLabels, // Fixed typo: suggested_labels to suggestedLabels
});

// Create detailed activity
await storage.createActivity({
type: "label",
description: "Advanced AI analysis completed",
details: `${analysis.categories.join(", ")} | Confidence: ${Math.round(analysis.confidence * 100)}% | ${analysis.validation.reliable ? 'High' : 'Low'} reliability`,
details: `${analysis!.categories.join(", ")} | Confidence: ${Math.round(analysis!.confidence * 100)}% | ${analysis!.validation.reliable ? 'High' : 'Low'} reliability`,
timestamp: new Date().toISOString(),
icon: "fas fa-brain",
iconBg: analysis.validation.reliable ? "bg-green-50 text-green-600" : "bg-yellow-50 text-yellow-600",
iconBg: analysis!.validation.reliable ? "bg-green-50 text-green-600" : "bg-yellow-50 text-yellow-600",
});

res.json({
success: true,
email: updatedEmail,
analysis,
analysis, // analysis can be undefined here if autoAnalyze is false, but it's guarded by if (autoAnalyze)
categoryAssigned: matchingCategory.name
});
} else {
// No matching category found, suggest creating new one
res.json({
success: false,
analysis,
analysis, // analysis can be undefined here
suggestion: "create_category",
suggestedCategory: analysis.categories[0],
suggestedCategory: analysis!.categories[0], // Used non-null assertion, potentially unsafe if analysis is undefined
message: "No matching category found. Consider creating a new category."
});
}
} else {
// Manual categorization (existing logic)
// analysis remains undefined here
const { categoryId, confidence } = req.body;

const updatedEmail = await storage.updateEmail(emailId, {
Expand Down Expand Up @@ -279,11 +283,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
const email = await storage.getEmailById(emailId);
if (!email) continue;

const analysis = await pythonNLP.analyzeEmail(email.subject, email.content);
const analysis: MappedNLPResult = await pythonNLP.analyzeEmail(email.subject, email.content); // Updated type to MappedNLPResult

// Find best matching category
const matchingCategory = categories.find(cat =>
analysis.categories.some(aiCat =>
analysis.categories.some((aiCat: string) =>
cat.name.toLowerCase().includes(aiCat.toLowerCase()) ||
aiCat.toLowerCase().includes(cat.name.toLowerCase())
)
Expand All @@ -293,7 +297,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
await storage.updateEmail(emailId, {
categoryId: matchingCategory.id,
confidence: Math.round(analysis.confidence * 100),
labels: analysis.suggested_labels,
labels: analysis.suggestedLabels, // Fixed typo: suggested_labels to suggestedLabels
});

results.push({
Expand All @@ -311,12 +315,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
analysis
});
}
} catch (error) {
} catch (error) { // error is unknown here
results.push({
emailId,
success: false,
reason: 'analysis_error',
error: error.message
error: error instanceof Error ? error.message : String(error) // Handled unknown error
});
}
}
Expand Down
Loading