Add AI categorization with Ollama: settings, auto-classify on sync/import, and manual classify from Transactions
Adds local LLM-based transaction categorization using Ollama: - Settings UI to configure Ollama URL, model, confidence threshold - Auto-categorization during bank sync and file import - Manual "Classify Uncategorized" button on Transactions screen that respects active filters - Payee default category learning loop for classified transactions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ public class BankSyncService : IBankSyncService
|
||||
private readonly SimpleFinSyncProvider _simpleFin;
|
||||
private readonly IDuplicateDetectionService _duplicateService;
|
||||
private readonly IPayeeService _payeeService;
|
||||
private readonly IAiCategorizationService _aiCategorization;
|
||||
private readonly IHubContext<NotificationHub> _hub;
|
||||
private readonly ILogger<BankSyncService> _logger;
|
||||
|
||||
@@ -30,6 +31,7 @@ public class BankSyncService : IBankSyncService
|
||||
SimpleFinSyncProvider simpleFin,
|
||||
IDuplicateDetectionService duplicateService,
|
||||
IPayeeService payeeService,
|
||||
IAiCategorizationService aiCategorization,
|
||||
IHubContext<NotificationHub> hub,
|
||||
ILogger<BankSyncService> logger)
|
||||
{
|
||||
@@ -39,6 +41,7 @@ public class BankSyncService : IBankSyncService
|
||||
_simpleFin = simpleFin;
|
||||
_duplicateService = duplicateService;
|
||||
_payeeService = payeeService;
|
||||
_aiCategorization = aiCategorization;
|
||||
_hub = hub;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -500,6 +503,39 @@ public class BankSyncService : IBankSyncService
|
||||
}
|
||||
}
|
||||
|
||||
// AI auto-categorization for uncategorized transactions
|
||||
try
|
||||
{
|
||||
var uncategorized = _db.ChangeTracker.Entries<Transaction>()
|
||||
.Where(e => e.State == EntityState.Added && e.Entity.CategoryId == null)
|
||||
.Select(e => e.Entity)
|
||||
.ToList();
|
||||
|
||||
if (uncategorized.Count > 0)
|
||||
{
|
||||
var classifications = await _aiCategorization.ClassifyTransactionsAsync(userId, uncategorized);
|
||||
foreach (var c in classifications)
|
||||
{
|
||||
if (c.CategoryId.HasValue && c.TransactionIndex >= 0 && c.TransactionIndex < uncategorized.Count)
|
||||
{
|
||||
uncategorized[c.TransactionIndex].CategoryId = c.CategoryId.Value;
|
||||
|
||||
var txn = uncategorized[c.TransactionIndex];
|
||||
if (txn.PayeeId.HasValue)
|
||||
{
|
||||
var payee = await _db.Payees.FindAsync(txn.PayeeId.Value);
|
||||
if (payee != null && payee.DefaultCategoryId == null)
|
||||
payee.DefaultCategoryId = c.CategoryId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "AI categorization failed during sync, continuing without");
|
||||
}
|
||||
|
||||
connection.LastSyncAt = DateTime.UtcNow;
|
||||
connection.LastSyncError = accountResults.Any(r => r.Error != null)
|
||||
? accountResults.First(r => r.Error != null).Error
|
||||
|
||||
Reference in New Issue
Block a user