Feature: Add dynamic model selection to ViewReceipt page
Enhance receipt parsing model selection: - Fetch available models from LlamaCpp server dynamically - Show loaded/unloaded status in model dropdown - Persist selected model to appsettings.json - Read default model from AI:ReceiptParsingModel config - Inject LlamaCppVisionClient and IConfiguration dependencies Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,20 +165,25 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="model" class="form-label small">AI Model</label>
|
<label for="model" class="form-label small">AI Model</label>
|
||||||
<select name="model" id="model" class="form-select form-select-sm">
|
<select name="model" id="model" class="form-select form-select-sm">
|
||||||
<optgroup label="Local (llama.cpp)">
|
@if (Model.AvailableModels.Any())
|
||||||
<option value="llamacpp:GLM-4.6V-UD-Q4_K_XL-00001-of-00002">GLM-4.6V (Vision)</option>
|
{
|
||||||
</optgroup>
|
<optgroup label="Local (llama.cpp)">
|
||||||
<optgroup label="Local (Ollama)">
|
@foreach (var m in Model.AvailableModels)
|
||||||
<option value="ollama:llava">LLaVA (Vision)</option>
|
{
|
||||||
<option value="ollama:llava:13b">LLaVA 13B (Vision)</option>
|
var modelValue = $"llamacpp:{m.Id}";
|
||||||
</optgroup>
|
<option value="@modelValue" selected="@(Model.SelectedModel == modelValue)">
|
||||||
|
@(m.IsLoaded ? "[Loaded] " : "")@m.Id
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</optgroup>
|
||||||
|
}
|
||||||
<optgroup label="OpenAI">
|
<optgroup label="OpenAI">
|
||||||
<option value="gpt-4o-mini" selected>GPT-4o Mini (Fast & Cheap)</option>
|
<option value="gpt-4o-mini" selected="@(Model.SelectedModel == "gpt-4o-mini")">GPT-4o Mini (Fast & Cheap)</option>
|
||||||
<option value="gpt-4o">GPT-4o (Smarter)</option>
|
<option value="gpt-4o" selected="@(Model.SelectedModel == "gpt-4o")">GPT-4o (Smarter)</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="Anthropic">
|
<optgroup label="Anthropic">
|
||||||
<option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku (Fast)</option>
|
<option value="claude-3-5-haiku-20241022" selected="@(Model.SelectedModel == "claude-3-5-haiku-20241022")">Claude 3.5 Haiku (Fast)</option>
|
||||||
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet (Best)</option>
|
<option value="claude-3-5-sonnet-20241022" selected="@(Model.SelectedModel == "claude-3-5-sonnet-20241022")">Claude 3.5 Sonnet (Best)</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using MoneyMap.Data;
|
using MoneyMap.Data;
|
||||||
using MoneyMap.Models;
|
using MoneyMap.Models;
|
||||||
using MoneyMap.Services;
|
using MoneyMap.Services;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace MoneyMap.Pages
|
namespace MoneyMap.Pages
|
||||||
{
|
{
|
||||||
@@ -12,22 +14,33 @@ namespace MoneyMap.Pages
|
|||||||
private readonly MoneyMapContext _db;
|
private readonly MoneyMapContext _db;
|
||||||
private readonly IReceiptManager _receiptManager;
|
private readonly IReceiptManager _receiptManager;
|
||||||
private readonly IEnumerable<IReceiptParser> _parsers;
|
private readonly IEnumerable<IReceiptParser> _parsers;
|
||||||
|
private readonly LlamaCppVisionClient _llamaClient;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
|
||||||
public ViewReceiptModel(
|
public ViewReceiptModel(
|
||||||
MoneyMapContext db,
|
MoneyMapContext db,
|
||||||
IReceiptManager receiptManager,
|
IReceiptManager receiptManager,
|
||||||
IEnumerable<IReceiptParser> parsers)
|
IEnumerable<IReceiptParser> parsers,
|
||||||
|
LlamaCppVisionClient llamaClient,
|
||||||
|
IConfiguration config,
|
||||||
|
IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_receiptManager = receiptManager;
|
_receiptManager = receiptManager;
|
||||||
_parsers = parsers;
|
_parsers = parsers;
|
||||||
|
_llamaClient = llamaClient;
|
||||||
|
_config = config;
|
||||||
|
_env = env;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Receipt? Receipt { get; set; }
|
public Receipt? Receipt { get; set; }
|
||||||
public List<ReceiptLineItem> LineItems { get; set; } = new();
|
public List<ReceiptLineItem> LineItems { get; set; } = new();
|
||||||
public List<ReceiptParseLog> ParseLogs { get; set; } = new();
|
public List<ReceiptParseLog> ParseLogs { get; set; } = new();
|
||||||
public List<ParserOption> AvailableParsers { get; set; } = new();
|
public List<ParserOption> AvailableParsers { get; set; } = new();
|
||||||
|
public List<LlamaCppModel> AvailableModels { get; set; } = new();
|
||||||
public string ReceiptUrl { get; set; } = "";
|
public string ReceiptUrl { get; set; } = "";
|
||||||
|
public string SelectedModel => _config["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
|
||||||
|
|
||||||
[TempData]
|
[TempData]
|
||||||
public string? SuccessMessage { get; set; }
|
public string? SuccessMessage { get; set; }
|
||||||
@@ -59,6 +72,9 @@ namespace MoneyMap.Pages
|
|||||||
FullName = p.GetType().Name
|
FullName = p.GetType().Name
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
// Get available LlamaCpp models
|
||||||
|
AvailableModels = await _llamaClient.GetAvailableModelsAsync();
|
||||||
|
|
||||||
return Page();
|
return Page();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +110,12 @@ namespace MoneyMap.Pages
|
|||||||
return RedirectToPage(new { id });
|
return RedirectToPage(new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save selected model to config if it changed
|
||||||
|
if (!string.IsNullOrEmpty(model) && model != SelectedModel)
|
||||||
|
{
|
||||||
|
await SaveSelectedModelAsync(model);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await selectedParser.ParseReceiptAsync(id, model);
|
var result = await selectedParser.ParseReceiptAsync(id, model);
|
||||||
|
|
||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
@@ -108,6 +130,33 @@ namespace MoneyMap.Pages
|
|||||||
return RedirectToPage(new { id });
|
return RedirectToPage(new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveSelectedModelAsync(string model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var appSettingsPath = Path.Combine(_env.ContentRootPath, "appsettings.json");
|
||||||
|
var json = await System.IO.File.ReadAllTextAsync(appSettingsPath);
|
||||||
|
var jsonNode = JsonNode.Parse(json);
|
||||||
|
|
||||||
|
if (jsonNode == null) return;
|
||||||
|
|
||||||
|
// Ensure AI section exists
|
||||||
|
if (jsonNode["AI"] == null)
|
||||||
|
{
|
||||||
|
jsonNode["AI"] = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonNode["AI"]!["ReceiptParsingModel"] = model;
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
await System.IO.File.WriteAllTextAsync(appSettingsPath, jsonNode.ToJsonString(options));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silently fail - not critical if we can't save the preference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class ParserOption
|
public class ParserOption
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace MoneyMap.Services
|
|||||||
private readonly LlamaCppVisionClient _llamaCppClient;
|
private readonly LlamaCppVisionClient _llamaCppClient;
|
||||||
private readonly IMerchantService _merchantService;
|
private readonly IMerchantService _merchantService;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<AIReceiptParser> _logger;
|
private readonly ILogger<AIReceiptParser> _logger;
|
||||||
private string? _promptTemplate;
|
private string? _promptTemplate;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ namespace MoneyMap.Services
|
|||||||
LlamaCppVisionClient llamaCppClient,
|
LlamaCppVisionClient llamaCppClient,
|
||||||
IMerchantService merchantService,
|
IMerchantService merchantService,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<AIReceiptParser> logger)
|
ILogger<AIReceiptParser> logger)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -45,6 +47,7 @@ namespace MoneyMap.Services
|
|||||||
_llamaCppClient = llamaCppClient;
|
_llamaCppClient = llamaCppClient;
|
||||||
_merchantService = merchantService;
|
_merchantService = merchantService;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ namespace MoneyMap.Services
|
|||||||
if (receipt == null)
|
if (receipt == null)
|
||||||
return ReceiptParseResult.Failure("Receipt not found.");
|
return ReceiptParseResult.Failure("Receipt not found.");
|
||||||
|
|
||||||
var selectedModel = model ?? "gpt-4o-mini";
|
var selectedModel = model ?? _configuration["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
|
||||||
var isLlamaCpp = selectedModel.StartsWith("llamacpp:");
|
var isLlamaCpp = selectedModel.StartsWith("llamacpp:");
|
||||||
var isOllama = selectedModel.StartsWith("ollama:");
|
var isOllama = selectedModel.StartsWith("ollama:");
|
||||||
var isClaude = selectedModel.StartsWith("claude-");
|
var isClaude = selectedModel.StartsWith("claude-");
|
||||||
|
|||||||
Reference in New Issue
Block a user