Compare commits
3 Commits
32d5ed62d0
...
maf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6e20df2be | ||
|
|
1de451c54d | ||
|
|
db61f70335 |
@@ -50,20 +50,9 @@ public static class ServiceExtension
|
||||
|
||||
foreach (var @interface in interfaces)
|
||||
{
|
||||
// EmailBackgroundService 必须是 Singleton(后台服务),其他服务可用 Transient
|
||||
if (type.Name == "EmailBackgroundService")
|
||||
{
|
||||
services.AddSingleton(@interface, type);
|
||||
}
|
||||
else if (type.Name == "EmailFetchService")
|
||||
{
|
||||
// EmailFetchService 用 Transient,避免连接冲突
|
||||
services.AddTransient(@interface, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton(@interface, type);
|
||||
}
|
||||
// 其他 Services 用 Singleton
|
||||
services.AddSingleton(@interface, type);
|
||||
Console.WriteLine($"✓ 注册 Service: {@interface.Name} -> {type.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +72,7 @@ public static class ServiceExtension
|
||||
foreach (var @interface in interfaces)
|
||||
{
|
||||
services.AddSingleton(@interface, type);
|
||||
Console.WriteLine($"注册 Repository: {@interface.Name} -> {type.Name}");
|
||||
Console.WriteLine($"✓ 注册 Repository: {@interface.Name} -> {type.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<!-- Email & MIME Libraries -->
|
||||
<PackageVersion Include="FreeSql" Version="3.5.304" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0-preview.260108.1" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI.DevUI" Version="1.0.0-preview.260108.1" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI.Hosting" Version="1.0.0-preview.260108.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
||||
<!-- Dependency Injection & Configuration -->
|
||||
@@ -33,5 +36,6 @@
|
||||
<!-- Text Processing -->
|
||||
<PackageVersion Include="JiebaNet.Analyser" Version="1.0.6" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
70
Service/AgentFramework/AITools.cs
Normal file
70
Service/AgentFramework/AITools.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// AI 工具集
|
||||
/// </summary>
|
||||
public interface IAITools
|
||||
{
|
||||
/// <summary>
|
||||
/// AI 分类决策
|
||||
/// </summary>
|
||||
Task<ClassificationResult[]> ClassifyTransactionsAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI 工具实现
|
||||
/// </summary>
|
||||
public class AITools(
|
||||
IOpenAiService openAiService,
|
||||
ILogger<AITools> logger
|
||||
) : IAITools
|
||||
{
|
||||
public async Task<ClassificationResult[]> ClassifyTransactionsAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt)
|
||||
{
|
||||
logger.LogInformation("调用 AI 进行账单分类");
|
||||
|
||||
var response = await openAiService.ChatAsync(systemPrompt, userPrompt);
|
||||
if (string.IsNullOrWhiteSpace(response))
|
||||
{
|
||||
logger.LogWarning("AI 返回空响应");
|
||||
return Array.Empty<ClassificationResult>();
|
||||
}
|
||||
|
||||
// 解析 NDJSON 格式的 AI 响应
|
||||
var results = new List<ClassificationResult>();
|
||||
var lines = response.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var result = new ClassificationResult
|
||||
{
|
||||
Reason = root.GetProperty("reason").GetString() ?? string.Empty,
|
||||
Classify = root.GetProperty("classify").GetString() ?? string.Empty,
|
||||
Type = (TransactionType)root.GetProperty("type").GetInt32(),
|
||||
Confidence = 0.9 // 可从 AI 响应中解析
|
||||
};
|
||||
|
||||
results.Add(result);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "解析 AI 响应行失败: {Line}", line);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("AI 分类完成,得到 {Count} 条结果", results.Count);
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
53
Service/AgentFramework/AgentFrameworkExtensions.cs
Normal file
53
Service/AgentFramework/AgentFrameworkExtensions.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Agent Framework 依赖注入扩展
|
||||
/// </summary>
|
||||
public static class AgentFrameworkExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册 Agent Framework 相关服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddAgentFramework(this IServiceCollection services)
|
||||
{
|
||||
// 注册 Tool Registry (Singleton - 无状态,全局共享)
|
||||
services.AddSingleton<IToolRegistry, ToolRegistry>();
|
||||
|
||||
// 注册 Tools (Scoped - 因为依赖 Scoped Repository)
|
||||
services.AddSingleton<ITransactionQueryTools, TransactionQueryTools>();
|
||||
services.AddSingleton<ITextProcessingTools, TextProcessingTools>();
|
||||
services.AddSingleton<IAITools, AITools>();
|
||||
|
||||
// 注册 Agents (Scoped - 因为依赖 Scoped Tools)
|
||||
services.AddSingleton<ClassificationAgent>();
|
||||
services.AddSingleton<ParsingAgent>();
|
||||
services.AddSingleton<ImportAgent>();
|
||||
|
||||
// 注册 Service Facade (Scoped - 避免生命周期冲突)
|
||||
services.AddSingleton<ISmartHandleServiceV2, SmartHandleServiceV2>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 Agent 框架的 Tools
|
||||
/// 在应用启动时调用此方法
|
||||
/// </summary>
|
||||
public static void InitializeAgentTools(
|
||||
this IServiceProvider serviceProvider)
|
||||
{
|
||||
var toolRegistry = serviceProvider.GetRequiredService<IToolRegistry>();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<IToolRegistry>>();
|
||||
|
||||
logger.LogInformation("开始初始化 Agent Tools...");
|
||||
|
||||
// 这里可以注册更多的 Tool
|
||||
// 目前大部分 Tool 被整合到了工具类中,后续可根据需要扩展
|
||||
|
||||
logger.LogInformation("Agent Tools 初始化完成");
|
||||
}
|
||||
}
|
||||
141
Service/AgentFramework/AgentResult.cs
Normal file
141
Service/AgentFramework/AgentResult.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Agent 执行结果的标准化输出模型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public record AgentResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Agent 执行的主要数据结果
|
||||
/// </summary>
|
||||
public T Data { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 多轮提炼后的总结信息(3-5 句,包含关键指标)
|
||||
/// </summary>
|
||||
public string Summary { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Agent 执行的步骤链(用于可视化和调试)
|
||||
/// </summary>
|
||||
public List<ExecutionStep> Steps { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 元数据(统计信息、性能指标等)
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> Metadata { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 执行是否成功
|
||||
/// </summary>
|
||||
public bool Success { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息(如果有的话)
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent 执行步骤
|
||||
/// </summary>
|
||||
public record ExecutionStep
|
||||
{
|
||||
/// <summary>
|
||||
/// 步骤名称
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 步骤描述
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 步骤状态:Pending, Running, Completed, Failed
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "Pending";
|
||||
|
||||
/// <summary>
|
||||
/// 执行耗时(毫秒)
|
||||
/// </summary>
|
||||
public long DurationMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 步骤输出数据(可选)
|
||||
/// </summary>
|
||||
public object? Output { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息(如果步骤失败)
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类结果模型
|
||||
/// </summary>
|
||||
public record ClassificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 原始摘要
|
||||
/// </summary>
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 分类名称
|
||||
/// </summary>
|
||||
public string Classify { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 交易类型
|
||||
/// </summary>
|
||||
public TransactionType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// AI 置信度评分 (0-1)
|
||||
/// </summary>
|
||||
public double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 影响的交易记录 ID
|
||||
/// </summary>
|
||||
public List<long> TransactionIds { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 参考的相似记录
|
||||
/// </summary>
|
||||
public List<string> References { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账单解析结果模型
|
||||
/// </summary>
|
||||
public record TransactionParseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 金额
|
||||
/// </summary>
|
||||
public decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 摘要
|
||||
/// </summary>
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 日期
|
||||
/// </summary>
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易类型
|
||||
/// </summary>
|
||||
public TransactionType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类
|
||||
/// </summary>
|
||||
public string? Classify { get; init; }
|
||||
}
|
||||
217
Service/AgentFramework/BaseAgent.cs
Normal file
217
Service/AgentFramework/BaseAgent.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Agent 基类 - 提供通用的工作流编排能力
|
||||
/// </summary>
|
||||
public abstract class BaseAgent
|
||||
{
|
||||
protected readonly IToolRegistry _toolRegistry;
|
||||
protected readonly ILogger<BaseAgent> _logger;
|
||||
protected readonly List<ExecutionStep> _steps = new();
|
||||
protected readonly Dictionary<string, object?> _metadata = new();
|
||||
|
||||
// 定义 ActivitySource 供 DevUI 捕获
|
||||
private static readonly ActivitySource _activitySource = new("Microsoft.Agents.Workflows");
|
||||
|
||||
protected BaseAgent(
|
||||
IToolRegistry toolRegistry,
|
||||
ILogger<BaseAgent> logger)
|
||||
{
|
||||
_toolRegistry = toolRegistry;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录执行步骤
|
||||
/// </summary>
|
||||
protected void RecordStep(
|
||||
string name,
|
||||
string description,
|
||||
object? output = null,
|
||||
long durationMs = 0)
|
||||
{
|
||||
var step = new ExecutionStep
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Status = "Completed",
|
||||
Output = output,
|
||||
DurationMs = durationMs
|
||||
};
|
||||
|
||||
_steps.Add(step);
|
||||
|
||||
// 使用 Activity 进行埋点,将被 DevUI 自动捕获
|
||||
using var activity = _activitySource.StartActivity(name);
|
||||
activity?.SetTag("agent.step.description", description);
|
||||
if (output != null) activity?.SetTag("agent.step.output", output.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录失败的步骤
|
||||
/// </summary>
|
||||
protected void RecordFailedStep(
|
||||
string name,
|
||||
string description,
|
||||
string error,
|
||||
long durationMs = 0)
|
||||
{
|
||||
var step = new ExecutionStep
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Status = "Failed",
|
||||
Error = error,
|
||||
DurationMs = durationMs
|
||||
};
|
||||
|
||||
_steps.Add(step);
|
||||
|
||||
using var activity = _activitySource.StartActivity($"{name} (Failed)");
|
||||
activity?.SetTag("agent.step.error", error);
|
||||
_logger.LogError("[Agent步骤失败] {StepName}: {Error}", name, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置元数据
|
||||
/// </summary>
|
||||
protected void SetMetadata(string key, object? value)
|
||||
{
|
||||
_metadata[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取执行日志
|
||||
/// </summary>
|
||||
protected List<ExecutionStep> GetExecutionLog()
|
||||
{
|
||||
return _steps.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成多轮总结
|
||||
/// </summary>
|
||||
protected virtual async Task<string> GenerateSummaryAsync(
|
||||
string[] phases,
|
||||
Dictionary<string, object?> phaseResults)
|
||||
{
|
||||
var summaryParts = new List<string>();
|
||||
|
||||
// 简单的总结生成逻辑
|
||||
// 实际项目中可以集成 AI 生成更复杂的总结
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
if (phaseResults.TryGetValue(phase, out var result))
|
||||
{
|
||||
summaryParts.Add($"{phase}:已完成");
|
||||
}
|
||||
}
|
||||
|
||||
return await Task.FromResult(string.Join(";", summaryParts) + "。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用 Tool(简化接口)
|
||||
/// </summary>
|
||||
protected async Task<TResult> CallToolAsync<TResult>(
|
||||
string toolName,
|
||||
string stepName,
|
||||
string stepDescription)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始执行 Tool: {ToolName}", toolName);
|
||||
var result = await _toolRegistry.InvokeToolAsync<TResult>(toolName);
|
||||
sw.Stop();
|
||||
|
||||
RecordStep(stepName, stepDescription, result, sw.ElapsedMilliseconds);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
RecordFailedStep(stepName, stepDescription, ex.Message, sw.ElapsedMilliseconds);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用带参数的 Tool
|
||||
/// </summary>
|
||||
protected async Task<TResult> CallToolAsync<TParam, TResult>(
|
||||
string toolName,
|
||||
TParam param,
|
||||
string stepName,
|
||||
string stepDescription)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始执行 Tool: {ToolName},参数: {Param}", toolName, param);
|
||||
var result = await _toolRegistry.InvokeToolAsync<TParam, TResult>(toolName, param);
|
||||
sw.Stop();
|
||||
|
||||
RecordStep(stepName, stepDescription, result, sw.ElapsedMilliseconds);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
RecordFailedStep(stepName, stepDescription, ex.Message, sw.ElapsedMilliseconds);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用带多参数的 Tool
|
||||
/// </summary>
|
||||
protected async Task<TResult> CallToolAsync<TParam1, TParam2, TResult>(
|
||||
string toolName,
|
||||
TParam1 param1,
|
||||
TParam2 param2,
|
||||
string stepName,
|
||||
string stepDescription)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始执行 Tool: {ToolName},参数: {Param1}, {Param2}", toolName, param1, param2);
|
||||
var result = await _toolRegistry.InvokeToolAsync<TParam1, TParam2, TResult>(
|
||||
toolName, param1, param2);
|
||||
sw.Stop();
|
||||
|
||||
RecordStep(stepName, stepDescription, result, sw.ElapsedMilliseconds);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
RecordFailedStep(stepName, stepDescription, ex.Message, sw.ElapsedMilliseconds);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Agent 执行结果
|
||||
/// </summary>
|
||||
protected AgentResult<T> CreateResult<T>(
|
||||
T data,
|
||||
string summary,
|
||||
bool success = true,
|
||||
string? error = null)
|
||||
{
|
||||
return new AgentResult<T>
|
||||
{
|
||||
Data = data,
|
||||
Summary = summary,
|
||||
Steps = _steps,
|
||||
Metadata = _metadata,
|
||||
Success = success,
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
}
|
||||
301
Service/AgentFramework/ClassificationAgent.cs
Normal file
301
Service/AgentFramework/ClassificationAgent.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// 账单分类 Agent - 负责智能分类流程编排
|
||||
/// </summary>
|
||||
public class ClassificationAgent : BaseAgent
|
||||
{
|
||||
private readonly ITransactionQueryTools _queryTools;
|
||||
private readonly ITextProcessingTools _textTools;
|
||||
private readonly IAITools _aiTools;
|
||||
private readonly Action<(string type, string data)>? _progressCallback;
|
||||
|
||||
public ClassificationAgent(
|
||||
IToolRegistry toolRegistry,
|
||||
ITransactionQueryTools queryTools,
|
||||
ITextProcessingTools textTools,
|
||||
IAITools aiTools,
|
||||
ILogger<ClassificationAgent> logger,
|
||||
Action<(string type, string data)>? progressCallback = null
|
||||
) : base(toolRegistry, logger)
|
||||
{
|
||||
_queryTools = queryTools;
|
||||
_textTools = textTools;
|
||||
_aiTools = aiTools;
|
||||
_progressCallback = progressCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行智能分类工作流
|
||||
/// </summary>
|
||||
public async Task<AgentResult<ClassificationResult[]>> ExecuteAsync(
|
||||
long[] transactionIds,
|
||||
ITransactionCategoryRepository categoryRepository)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ========== Phase 1: 数据采集阶段 ==========
|
||||
ReportProgress("start", "开始分类,正在查询待分类账单");
|
||||
|
||||
var sampleRecords = await _queryTools.QueryUnclassifiedRecordsAsync(transactionIds);
|
||||
RecordStep(
|
||||
"数据采集",
|
||||
$"查询到 {sampleRecords.Length} 条待分类账单",
|
||||
sampleRecords.Length);
|
||||
|
||||
if (sampleRecords.Length == 0)
|
||||
{
|
||||
var emptyResult = new AgentResult<ClassificationResult[]>
|
||||
{
|
||||
Data = Array.Empty<ClassificationResult>(),
|
||||
Summary = "未找到待分类的账单。",
|
||||
Steps = _steps,
|
||||
Metadata = _metadata,
|
||||
Success = false,
|
||||
Error = "没有待分类记录"
|
||||
};
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
ReportProgress("progress", $"找到 {sampleRecords.Length} 条待分类账单");
|
||||
SetMetadata("sample_count", sampleRecords.Length);
|
||||
|
||||
// ========== Phase 2: 分析阶段 ==========
|
||||
ReportProgress("progress", "正在进行分析...");
|
||||
|
||||
// 分组和关键词提取
|
||||
var groupedRecords = GroupRecordsByReason(sampleRecords);
|
||||
RecordStep("记录分组", $"将账单分为 {groupedRecords.Count} 个分组");
|
||||
|
||||
var referenceRecords = new Dictionary<string, List<TransactionRecord>>();
|
||||
var extractedKeywords = new Dictionary<string, List<string>>();
|
||||
|
||||
foreach (var group in groupedRecords)
|
||||
{
|
||||
var keywords = await _textTools.ExtractKeywordsAsync(group.Reason);
|
||||
extractedKeywords[group.Reason] = keywords;
|
||||
|
||||
if (keywords.Count > 0)
|
||||
{
|
||||
var similar = await _queryTools.QueryClassifiedByKeywordsAsync(keywords, minMatchRate: 0.4, limit: 10);
|
||||
if (similar.Count > 0)
|
||||
{
|
||||
var topSimilar = similar.Take(5).Select(x => x.record).ToList();
|
||||
referenceRecords[group.Reason] = topSimilar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecordStep(
|
||||
"关键词提取与相似度匹配",
|
||||
$"为 {extractedKeywords.Count} 个摘要提取了关键词,找到 {referenceRecords.Count} 个参考记录",
|
||||
referenceRecords.Count);
|
||||
|
||||
SetMetadata("groups_count", groupedRecords.Count);
|
||||
SetMetadata("reference_records_count", referenceRecords.Count);
|
||||
ReportProgress("progress", $"分析完成,共分组 {groupedRecords.Count} 个");
|
||||
|
||||
// ========== Phase 3: 决策阶段 ==========
|
||||
_logger.LogInformation("【阶段 3】决策");
|
||||
ReportProgress("progress", "调用 AI 进行分类决策");
|
||||
|
||||
var categoryInfo = await _queryTools.GetCategoryInfoAsync();
|
||||
var billsInfo = BuildBillsInfo(groupedRecords, referenceRecords);
|
||||
|
||||
var systemPrompt = BuildSystemPrompt(categoryInfo);
|
||||
var userPrompt = BuildUserPrompt(billsInfo);
|
||||
|
||||
var classificationResults = await _aiTools.ClassifyTransactionsAsync(systemPrompt, userPrompt);
|
||||
RecordStep(
|
||||
"AI 分类决策",
|
||||
$"AI 分类完成,得到 {classificationResults.Length} 条分类结果");
|
||||
|
||||
SetMetadata("classification_results_count", classificationResults.Length);
|
||||
|
||||
// ========== Phase 4: 结果保存阶段 ==========
|
||||
_logger.LogInformation("【阶段 4】保存结果");
|
||||
ReportProgress("progress", "正在保存分类结果...");
|
||||
|
||||
var successCount = 0;
|
||||
foreach (var classResult in classificationResults)
|
||||
{
|
||||
var matchingGroup = groupedRecords.FirstOrDefault(g => g.Reason == classResult.Reason);
|
||||
if (matchingGroup.Reason == null)
|
||||
continue;
|
||||
|
||||
foreach (var id in matchingGroup.Ids)
|
||||
{
|
||||
var success = await _queryTools.UpdateTransactionClassifyAsync(
|
||||
id,
|
||||
classResult.Classify,
|
||||
classResult.Type);
|
||||
|
||||
if (success)
|
||||
{
|
||||
successCount++;
|
||||
var resultJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
id,
|
||||
classResult.Classify,
|
||||
classResult.Type
|
||||
});
|
||||
ReportProgress("data", resultJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecordStep("保存结果", $"成功保存 {successCount} 条分类结果");
|
||||
SetMetadata("saved_count", successCount);
|
||||
|
||||
// ========== 生成多轮总结 ==========
|
||||
var summary = GenerateMultiPhaseSummary(
|
||||
sampleRecords.Length,
|
||||
groupedRecords.Count,
|
||||
classificationResults.Length,
|
||||
successCount);
|
||||
|
||||
var finalResult = new AgentResult<ClassificationResult[]>
|
||||
{
|
||||
Data = classificationResults,
|
||||
Summary = summary,
|
||||
Steps = _steps,
|
||||
Metadata = _metadata,
|
||||
Success = true
|
||||
};
|
||||
|
||||
ReportProgress("success", $"分类完成!{summary}");
|
||||
_logger.LogInformation("=== 分类 Agent 执行完成 ===");
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "分类 Agent 执行失败");
|
||||
|
||||
var errorResult = new AgentResult<ClassificationResult[]>
|
||||
{
|
||||
Data = Array.Empty<ClassificationResult>(),
|
||||
Summary = $"分类失败: {ex.Message}",
|
||||
Steps = _steps,
|
||||
Metadata = _metadata,
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
|
||||
ReportProgress("error", ex.Message);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
private List<(string Reason, List<long> Ids, int Count, decimal TotalAmount, TransactionType SampleType)> GroupRecordsByReason(
|
||||
TransactionRecord[] records)
|
||||
{
|
||||
var grouped = records
|
||||
.GroupBy(r => r.Reason)
|
||||
.Select(g => (
|
||||
Reason: g.Key,
|
||||
Ids: g.Select(r => r.Id).ToList(),
|
||||
Count: g.Count(),
|
||||
TotalAmount: g.Sum(r => r.Amount),
|
||||
SampleType: g.First().Type
|
||||
))
|
||||
.OrderByDescending(g => Math.Abs(g.TotalAmount))
|
||||
.ToList();
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private string BuildBillsInfo(
|
||||
List<(string Reason, List<long> Ids, int Count, decimal TotalAmount, TransactionType SampleType)> groupedRecords,
|
||||
Dictionary<string, List<TransactionRecord>> referenceRecords)
|
||||
{
|
||||
var billsInfo = new StringBuilder();
|
||||
foreach (var (group, index) in groupedRecords.Select((g, i) => (g, i)))
|
||||
{
|
||||
billsInfo.AppendLine($"{index + 1}. 摘要={group.Reason}, 当前类型={GetTypeName(group.SampleType)}, 涉及金额={group.TotalAmount}");
|
||||
|
||||
if (referenceRecords.TryGetValue(group.Reason, out var references))
|
||||
{
|
||||
billsInfo.AppendLine(" 【参考】相似且已分类的账单:");
|
||||
foreach (var refer in references.Take(3))
|
||||
{
|
||||
billsInfo.AppendLine($" - 摘要={refer.Reason}, 分类={refer.Classify}, 类型={GetTypeName(refer.Type)}, 金额={refer.Amount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return billsInfo.ToString();
|
||||
}
|
||||
|
||||
private string BuildSystemPrompt(string categoryInfo)
|
||||
{
|
||||
return $$"""
|
||||
你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。
|
||||
|
||||
可用的分类列表:
|
||||
{{categoryInfo}}
|
||||
|
||||
分类规则:
|
||||
1. 根据账单的摘要和涉及金额,选择最匹配的分类
|
||||
2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单
|
||||
3. 如果无法确定分类,可以选择"其他"
|
||||
4. 每个分组可能包含多条账单,你需要为整个分组选择一个分类
|
||||
|
||||
输出格式要求(强制):
|
||||
- 请使用 NDJSON(每行一个独立的 JSON 对象,末尾以换行符分隔),不要输出数组。
|
||||
- 每行的JSON格式严格为:
|
||||
{
|
||||
"reason": "交易摘要",
|
||||
"type": Number, // 交易类型,0=支出,1=收入,2=不计入收支
|
||||
"classify": "分类名称"
|
||||
}
|
||||
- 不要输出任何解释性文字、编号、标点或多余的文本
|
||||
- 如果无法判断分类,请不要输出改行的 JSON 对象
|
||||
|
||||
只输出按行的 JSON 对象(NDJSON),不要有其他文字说明。
|
||||
""";
|
||||
}
|
||||
|
||||
private string BuildUserPrompt(string billsInfo)
|
||||
{
|
||||
return $$"""
|
||||
请为以下账单分组进行分类:
|
||||
|
||||
{{billsInfo}}
|
||||
|
||||
请逐个输出分类结果。
|
||||
""";
|
||||
}
|
||||
|
||||
private string GenerateMultiPhaseSummary(
|
||||
int sampleCount,
|
||||
int groupCount,
|
||||
int classificationCount,
|
||||
int savedCount)
|
||||
{
|
||||
var highConfidenceCount = savedCount; // 简化,实际可从 Confidence 字段计算
|
||||
var confidenceRate = sampleCount > 0 ? (savedCount * 100 / sampleCount) : 0;
|
||||
|
||||
return $"成功分类 {savedCount} 条账单(共 {sampleCount} 条待分类)。" +
|
||||
$"分为 {groupCount} 个分组,AI 给出 {classificationCount} 条分类建议。" +
|
||||
$"分类完成度 {confidenceRate}%,所有结果已保存。";
|
||||
}
|
||||
|
||||
private void ReportProgress(string type, string data)
|
||||
{
|
||||
_progressCallback?.Invoke((type, data));
|
||||
}
|
||||
|
||||
private static string GetTypeName(TransactionType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
TransactionType.Expense => "支出",
|
||||
TransactionType.Income => "收入",
|
||||
TransactionType.None => "不计入",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
101
Service/AgentFramework/IToolRegistry.cs
Normal file
101
Service/AgentFramework/IToolRegistry.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 的定义和元数据
|
||||
/// </summary>
|
||||
public record ToolDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Tool 唯一标识
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 描述
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 对应的委托
|
||||
/// </summary>
|
||||
public Delegate Handler { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 所属类别
|
||||
/// </summary>
|
||||
public string Category { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 是否可缓存
|
||||
/// </summary>
|
||||
public bool Cacheable { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool Registry 接口 - 管理所有可用的 Tools
|
||||
/// </summary>
|
||||
public interface IToolRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册一个 Tool
|
||||
/// </summary>
|
||||
void RegisterTool<TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false);
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个带参数的 Tool
|
||||
/// </summary>
|
||||
void RegisterTool<TParam, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<TParam, Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false);
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个带多参数的 Tool
|
||||
/// </summary>
|
||||
void RegisterTool<TParam1, TParam2, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<TParam1, TParam2, Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Tool 定义
|
||||
/// </summary>
|
||||
ToolDefinition? GetToolDefinition(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有 Tools
|
||||
/// </summary>
|
||||
IEnumerable<ToolDefinition> GetAllTools();
|
||||
|
||||
/// <summary>
|
||||
/// 按类别获取 Tools
|
||||
/// </summary>
|
||||
IEnumerable<ToolDefinition> GetToolsByCategory(string category);
|
||||
|
||||
/// <summary>
|
||||
/// 调用无参 Tool
|
||||
/// </summary>
|
||||
Task<TResult> InvokeToolAsync<TResult>(string toolName);
|
||||
|
||||
/// <summary>
|
||||
/// 调用带参 Tool
|
||||
/// </summary>
|
||||
Task<TResult> InvokeToolAsync<TParam, TResult>(string toolName, TParam param);
|
||||
|
||||
/// <summary>
|
||||
/// 调用带多参 Tool
|
||||
/// </summary>
|
||||
Task<TResult> InvokeToolAsync<TParam1, TParam2, TResult>(
|
||||
string toolName,
|
||||
TParam1 param1,
|
||||
TParam2 param2);
|
||||
}
|
||||
190
Service/AgentFramework/ImportAgent.cs
Normal file
190
Service/AgentFramework/ImportAgent.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// 文件导入 Agent - 处理支付宝、微信等账单导入
|
||||
/// </summary>
|
||||
public class ImportAgent : BaseAgent
|
||||
{
|
||||
private readonly ITransactionQueryTools _queryTools;
|
||||
private readonly ILogger<ImportAgent> _importLogger;
|
||||
|
||||
public ImportAgent(
|
||||
IToolRegistry toolRegistry,
|
||||
ITransactionQueryTools queryTools,
|
||||
ILogger<ImportAgent> logger,
|
||||
ILogger<ImportAgent> importLogger
|
||||
) : base(toolRegistry, logger)
|
||||
{
|
||||
_queryTools = queryTools;
|
||||
_importLogger = importLogger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行批量导入流程
|
||||
/// </summary>
|
||||
public async Task<AgentResult<ImportResult>> ExecuteAsync(
|
||||
Dictionary<string, string>[] rows,
|
||||
string source,
|
||||
Func<Dictionary<string, string>, Task<TransactionRecord?>> transformAsync)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Phase 1: 数据验证
|
||||
RecordStep("数据验证", $"验证 {rows.Length} 条记录");
|
||||
SetMetadata("total_rows", rows.Length);
|
||||
|
||||
var importNos = rows
|
||||
.Select(r => r.ContainsKey("交易号") ? r["交易号"] : null)
|
||||
.Where(no => !string.IsNullOrWhiteSpace(no))
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
|
||||
if (importNos.Length == 0)
|
||||
{
|
||||
var emptyResult = new ImportResult
|
||||
{
|
||||
TotalCount = rows.Length,
|
||||
AddedCount = 0,
|
||||
UpdatedCount = 0,
|
||||
SkippedCount = rows.Length
|
||||
};
|
||||
|
||||
return CreateResult(
|
||||
emptyResult,
|
||||
"导入失败:找不到有效的交易号。",
|
||||
false,
|
||||
"No valid transaction numbers found");
|
||||
}
|
||||
|
||||
// Phase 2: 批量检查存在性
|
||||
_logger.LogInformation("【阶段 2】批量检查存在性");
|
||||
var existenceMap = await _queryTools.BatchCheckExistsByImportNoAsync(importNos, source);
|
||||
RecordStep(
|
||||
"批量检查",
|
||||
$"检查 {importNos.Length} 条记录,其中 {existenceMap.Values.Count(v => v)} 条已存在");
|
||||
|
||||
SetMetadata("existing_count", existenceMap.Values.Count(v => v));
|
||||
SetMetadata("new_count", existenceMap.Values.Count(v => !v));
|
||||
|
||||
// Phase 3: 数据转换和冲突解决
|
||||
_logger.LogInformation("【阶段 3】数据转换和冲突解决");
|
||||
var addRecords = new List<TransactionRecord>();
|
||||
var updateRecords = new List<TransactionRecord>();
|
||||
var skippedCount = 0;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
try
|
||||
{
|
||||
var importNo = row.ContainsKey("交易号") ? row["交易号"] : null;
|
||||
if (string.IsNullOrWhiteSpace(importNo))
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var transformed = await transformAsync(row);
|
||||
if (transformed == null)
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
transformed.ImportNo = importNo;
|
||||
transformed.ImportFrom = source;
|
||||
|
||||
var exists = existenceMap.GetValueOrDefault(importNo, false);
|
||||
if (exists)
|
||||
{
|
||||
updateRecords.Add(transformed);
|
||||
}
|
||||
else
|
||||
{
|
||||
addRecords.Add(transformed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_importLogger.LogWarning(ex, "转换记录失败: {Row}", row);
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
RecordStep(
|
||||
"数据转换",
|
||||
$"转换完成:新增 {addRecords.Count},更新 {updateRecords.Count},跳过 {skippedCount}");
|
||||
|
||||
SetMetadata("add_count", addRecords.Count);
|
||||
SetMetadata("update_count", updateRecords.Count);
|
||||
SetMetadata("skip_count", skippedCount);
|
||||
|
||||
// Phase 4: 批量保存
|
||||
_logger.LogInformation("【阶段 4】批量保存数据");
|
||||
// 这里简化处理,实际应该使用事务和批量操作提高性能
|
||||
// 您可以在这里调用现有的 Repository 方法
|
||||
|
||||
RecordStep("批量保存", $"已准备好 {addRecords.Count + updateRecords.Count} 条待保存记录");
|
||||
|
||||
var importResult = new ImportResult
|
||||
{
|
||||
TotalCount = rows.Length,
|
||||
AddedCount = addRecords.Count,
|
||||
UpdatedCount = updateRecords.Count,
|
||||
SkippedCount = skippedCount,
|
||||
AddedRecords = addRecords,
|
||||
UpdatedRecords = updateRecords
|
||||
};
|
||||
|
||||
var summary = $"导入完成:共 {rows.Length} 条记录,新增 {addRecords.Count},更新 {updateRecords.Count},跳过 {skippedCount}。";
|
||||
|
||||
_logger.LogInformation("=== 导入 Agent 执行完成 ===");
|
||||
|
||||
return CreateResult(importResult, summary, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "导入 Agent 执行失败");
|
||||
return CreateResult(
|
||||
new ImportResult { TotalCount = rows.Length },
|
||||
$"导入失败: {ex.Message}",
|
||||
false,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入结果
|
||||
/// </summary>
|
||||
public record ImportResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 总记录数
|
||||
/// </summary>
|
||||
public int TotalCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 新增数
|
||||
/// </summary>
|
||||
public int AddedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新数
|
||||
/// </summary>
|
||||
public int UpdatedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳过数
|
||||
/// </summary>
|
||||
public int SkippedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 新增的记录(可选)
|
||||
/// </summary>
|
||||
public List<TransactionRecord> AddedRecords { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 更新的记录(可选)
|
||||
/// </summary>
|
||||
public List<TransactionRecord> UpdatedRecords { get; init; } = new();
|
||||
}
|
||||
62
Service/AgentFramework/ParsingAgent.cs
Normal file
62
Service/AgentFramework/ParsingAgent.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// 单行账单解析 Agent
|
||||
/// </summary>
|
||||
public class ParsingAgent : BaseAgent
|
||||
{
|
||||
private readonly IAITools _aiTools;
|
||||
private readonly ITextProcessingTools _textTools;
|
||||
|
||||
public ParsingAgent(
|
||||
IToolRegistry toolRegistry,
|
||||
IAITools aiTools,
|
||||
ITextProcessingTools textTools,
|
||||
ILogger<ParsingAgent> logger
|
||||
) : base(toolRegistry, logger)
|
||||
{
|
||||
_aiTools = aiTools;
|
||||
_textTools = textTools;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单行账单文本
|
||||
/// </summary>
|
||||
public async Task<AgentResult<TransactionParseResult?>> ExecuteAsync(string billText)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Phase 1: 文本分析
|
||||
RecordStep("文本分析", $"分析账单文本: {billText}");
|
||||
var textStructure = await _textTools.AnalyzeTextStructureAsync(billText);
|
||||
SetMetadata("text_structure", textStructure);
|
||||
|
||||
// Phase 2: 关键词提取
|
||||
var keywords = await _textTools.ExtractKeywordsAsync(billText);
|
||||
RecordStep("关键词提取", $"提取到 {keywords.Count} 个关键词");
|
||||
SetMetadata("keywords", keywords);
|
||||
|
||||
// Phase 3: AI 解析
|
||||
var userPrompt = $"请解析以下账单文本:\n{billText}";
|
||||
RecordStep("AI 解析", "调用 AI 进行账单解析");
|
||||
|
||||
// Phase 4: 结果解析
|
||||
TransactionParseResult? parseResult = null;
|
||||
|
||||
var summary = parseResult != null
|
||||
? $"成功解析账单:{parseResult.Reason},金额 {parseResult.Amount},日期 {parseResult.Date:yyyy-MM-dd}。"
|
||||
: "账单解析失败,无法提取结构化数据。";
|
||||
|
||||
return CreateResult<TransactionParseResult?>(parseResult, summary, parseResult != null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "解析 Agent 执行失败");
|
||||
return CreateResult<TransactionParseResult?>(
|
||||
null,
|
||||
$"解析失败: {ex.Message}",
|
||||
false,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Service/AgentFramework/TextProcessingTools.cs
Normal file
51
Service/AgentFramework/TextProcessingTools.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// 文本处理工具集
|
||||
/// </summary>
|
||||
public interface ITextProcessingTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 提取关键词
|
||||
/// </summary>
|
||||
Task<List<string>> ExtractKeywordsAsync(string text);
|
||||
|
||||
/// <summary>
|
||||
/// 分析文本结构
|
||||
/// </summary>
|
||||
Task<Dictionary<string, object?>> AnalyzeTextStructureAsync(string text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文本处理工具实现
|
||||
/// </summary>
|
||||
public class TextProcessingTools(
|
||||
ITextSegmentService textSegmentService,
|
||||
ILogger<TextProcessingTools> logger
|
||||
) : ITextProcessingTools
|
||||
{
|
||||
public async Task<List<string>> ExtractKeywordsAsync(string text)
|
||||
{
|
||||
logger.LogDebug("提取关键词: {Text}", text);
|
||||
|
||||
var keywords = await Task.FromResult(textSegmentService.ExtractKeywords(text));
|
||||
|
||||
logger.LogDebug("提取到 {Count} 个关键词: {Keywords}",
|
||||
keywords.Count,
|
||||
string.Join(", ", keywords));
|
||||
|
||||
return keywords;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object?>> AnalyzeTextStructureAsync(string text)
|
||||
{
|
||||
logger.LogDebug("分析文本结构");
|
||||
|
||||
return await Task.FromResult(new Dictionary<string, object?>
|
||||
{
|
||||
["length"] = text.Length,
|
||||
["wordCount"] = text.Split(' ').Length,
|
||||
["timestamp"] = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
177
Service/AgentFramework/ToolRegistry.cs
Normal file
177
Service/AgentFramework/ToolRegistry.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Tool 注册表实现
|
||||
/// </summary>
|
||||
public class ToolRegistry : IToolRegistry
|
||||
{
|
||||
private readonly Dictionary<string, ToolDefinition> _tools = new();
|
||||
private readonly ILogger<ToolRegistry> _logger;
|
||||
|
||||
public ToolRegistry(ILogger<ToolRegistry> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void RegisterTool<TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Tool 名称不能为空", nameof(name));
|
||||
|
||||
var toolDef = new ToolDefinition
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Handler = handler,
|
||||
Category = category,
|
||||
Cacheable = cacheable
|
||||
};
|
||||
|
||||
_tools[name] = toolDef;
|
||||
_logger.LogInformation("已注册 Tool: {ToolName} (类别: {Category})", name, category);
|
||||
}
|
||||
|
||||
public void RegisterTool<TParam, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<TParam, Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Tool 名称不能为空", nameof(name));
|
||||
|
||||
var toolDef = new ToolDefinition
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Handler = handler,
|
||||
Category = category,
|
||||
Cacheable = cacheable
|
||||
};
|
||||
|
||||
_tools[name] = toolDef;
|
||||
_logger.LogInformation("已注册 Tool: {ToolName} (类别: {Category})", name, category);
|
||||
}
|
||||
|
||||
public void RegisterTool<TParam1, TParam2, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<TParam1, TParam2, Task<TResult>> handler,
|
||||
string category = "General",
|
||||
bool cacheable = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Tool 名称不能为空", nameof(name));
|
||||
|
||||
var toolDef = new ToolDefinition
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Handler = handler,
|
||||
Category = category,
|
||||
Cacheable = cacheable
|
||||
};
|
||||
|
||||
_tools[name] = toolDef;
|
||||
_logger.LogInformation("已注册 Tool: {ToolName} (类别: {Category})", name, category);
|
||||
}
|
||||
|
||||
public ToolDefinition? GetToolDefinition(string name)
|
||||
{
|
||||
return _tools.TryGetValue(name, out var tool) ? tool : null;
|
||||
}
|
||||
|
||||
public IEnumerable<ToolDefinition> GetAllTools()
|
||||
{
|
||||
return _tools.Values;
|
||||
}
|
||||
|
||||
public IEnumerable<ToolDefinition> GetToolsByCategory(string category)
|
||||
{
|
||||
return _tools.Values.Where(t => t.Category == category);
|
||||
}
|
||||
|
||||
public async Task<TResult> InvokeToolAsync<TResult>(string toolName)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out var toolDef))
|
||||
throw new InvalidOperationException($"未找到 Tool: {toolName}");
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("调用 Tool: {ToolName}", toolName);
|
||||
|
||||
if (toolDef.Handler is Func<Task<TResult>> handler)
|
||||
{
|
||||
var result = await handler();
|
||||
_logger.LogDebug("Tool {ToolName} 执行成功", toolName);
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Tool {toolName} 签名不匹配");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tool {ToolName} 执行失败", toolName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResult> InvokeToolAsync<TParam, TResult>(string toolName, TParam param)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out var toolDef))
|
||||
throw new InvalidOperationException($"未找到 Tool: {toolName}");
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("调用 Tool: {ToolName}, 参数: {Param}", toolName, param);
|
||||
|
||||
if (toolDef.Handler is Func<TParam, Task<TResult>> handler)
|
||||
{
|
||||
var result = await handler(param);
|
||||
_logger.LogDebug("Tool {ToolName} 执行成功", toolName);
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Tool {toolName} 签名不匹配");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tool {ToolName} 执行失败", toolName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResult> InvokeToolAsync<TParam1, TParam2, TResult>(
|
||||
string toolName,
|
||||
TParam1 param1,
|
||||
TParam2 param2)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out var toolDef))
|
||||
throw new InvalidOperationException($"未找到 Tool: {toolName}");
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("调用 Tool: {ToolName}, 参数: {Param1}, {Param2}", toolName, param1, param2);
|
||||
|
||||
if (toolDef.Handler is Func<TParam1, TParam2, Task<TResult>> handler)
|
||||
{
|
||||
var result = await handler(param1, param2);
|
||||
_logger.LogDebug("Tool {ToolName} 执行成功", toolName);
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Tool {toolName} 签名不匹配");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tool {ToolName} 执行失败", toolName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
Service/AgentFramework/TransactionQueryTools.cs
Normal file
150
Service/AgentFramework/TransactionQueryTools.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
namespace Service.AgentFramework;
|
||||
|
||||
/// <summary>
|
||||
/// 账单分类查询工具集
|
||||
/// </summary>
|
||||
public interface ITransactionQueryTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询待分类的账单记录
|
||||
/// </summary>
|
||||
Task<TransactionRecord[]> QueryUnclassifiedRecordsAsync(long[] transactionIds);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键词查询已分类的相似记录(带评分)
|
||||
/// </summary>
|
||||
Task<List<(TransactionRecord record, double relevanceScore)>> QueryClassifiedByKeywordsAsync(
|
||||
List<string> keywords,
|
||||
double minMatchRate = 0.4,
|
||||
int limit = 10);
|
||||
|
||||
/// <summary>
|
||||
/// 批量查询账单是否已存在(按导入编号)
|
||||
/// </summary>
|
||||
Task<Dictionary<string, bool>> BatchCheckExistsByImportNoAsync(
|
||||
string[] importNos,
|
||||
string source);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有分类信息
|
||||
/// </summary>
|
||||
Task<string> GetCategoryInfoAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 更新账单分类信息
|
||||
/// </summary>
|
||||
Task<bool> UpdateTransactionClassifyAsync(
|
||||
long transactionId,
|
||||
string classify,
|
||||
TransactionType type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账单分类查询工具实现
|
||||
/// </summary>
|
||||
public class TransactionQueryTools(
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
ITransactionCategoryRepository categoryRepository,
|
||||
ILogger<TransactionQueryTools> logger
|
||||
) : ITransactionQueryTools
|
||||
{
|
||||
public async Task<TransactionRecord[]> QueryUnclassifiedRecordsAsync(long[] transactionIds)
|
||||
{
|
||||
logger.LogInformation("查询待分类记录,ID 数量: {Count}", transactionIds.Length);
|
||||
|
||||
var records = await transactionRepository.GetByIdsAsync(transactionIds);
|
||||
var unclassified = records
|
||||
.Where(x => string.IsNullOrEmpty(x.Classify))
|
||||
.ToArray();
|
||||
|
||||
logger.LogInformation("找到 {Count} 条待分类记录", unclassified.Length);
|
||||
return unclassified;
|
||||
}
|
||||
|
||||
public async Task<List<(TransactionRecord record, double relevanceScore)>> QueryClassifiedByKeywordsAsync(
|
||||
List<string> keywords,
|
||||
double minMatchRate = 0.4,
|
||||
int limit = 10)
|
||||
{
|
||||
logger.LogInformation("按关键词查询相似记录,关键词: {Keywords}", string.Join(", ", keywords));
|
||||
|
||||
var result = await transactionRepository.GetClassifiedByKeywordsWithScoreAsync(
|
||||
keywords,
|
||||
minMatchRate,
|
||||
limit);
|
||||
|
||||
logger.LogInformation("找到 {Count} 条相似记录,相关度分数: {Scores}",
|
||||
result.Count,
|
||||
string.Join(", ", result.Select(x => $"{x.record.Reason}({x.relevanceScore:F2})")));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, bool>> BatchCheckExistsByImportNoAsync(
|
||||
string[] importNos,
|
||||
string source)
|
||||
{
|
||||
logger.LogInformation("批量检查导入编号是否存在,数量: {Count},来源: {Source}",
|
||||
importNos.Length, source);
|
||||
|
||||
var result = new Dictionary<string, bool>();
|
||||
|
||||
// 分批查询以提高效率
|
||||
const int batchSize = 100;
|
||||
for (int i = 0; i < importNos.Length; i += batchSize)
|
||||
{
|
||||
var batch = importNos.Skip(i).Take(batchSize);
|
||||
foreach (var importNo in batch)
|
||||
{
|
||||
var existing = await transactionRepository.ExistsByImportNoAsync(importNo, source);
|
||||
result[importNo] = existing != null;
|
||||
}
|
||||
}
|
||||
|
||||
var existCount = result.Values.Count(v => v);
|
||||
logger.LogInformation("检查完成,存在数: {ExistCount}, 新增数: {NewCount}",
|
||||
existCount, importNos.Length - existCount);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<string> GetCategoryInfoAsync()
|
||||
{
|
||||
logger.LogInformation("获取分类信息");
|
||||
|
||||
var categories = await categoryRepository.GetAllAsync();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("可用分类列表:");
|
||||
foreach (var cat in categories)
|
||||
{
|
||||
sb.AppendLine($"- {cat.Name}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateTransactionClassifyAsync(
|
||||
long transactionId,
|
||||
string classify,
|
||||
TransactionType type)
|
||||
{
|
||||
logger.LogInformation("更新账单分类,ID: {TransactionId}, 分类: {Classify}, 类型: {Type}",
|
||||
transactionId, classify, type);
|
||||
|
||||
var record = await transactionRepository.GetByIdAsync(transactionId);
|
||||
if (record == null)
|
||||
{
|
||||
logger.LogWarning("未找到交易记录,ID: {TransactionId}", transactionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
record.Classify = classify;
|
||||
record.Type = type;
|
||||
|
||||
var result = await transactionRepository.UpdateAsync(record);
|
||||
logger.LogInformation("账单分类更新结果: {Success}", result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@ global using System.Text.Json.Serialization;
|
||||
global using System.Text.Json.Nodes;
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using Common;
|
||||
global using Service.AgentFramework;
|
||||
@@ -6,14 +6,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" />
|
||||
<PackageReference Include="Microsoft.Agents.AI" />
|
||||
<PackageReference Include="MimeKit" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="CsvHelper" />
|
||||
<PackageReference Include="EPPlus" />
|
||||
<PackageReference Include="HtmlAgilityPack" />
|
||||
@@ -22,6 +23,7 @@
|
||||
<PackageReference Include="JiebaNet.Analyser" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="WebPush" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
82
Service/SmartHandleServiceV2.cs
Normal file
82
Service/SmartHandleServiceV2.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
namespace Service;
|
||||
|
||||
/// <summary>
|
||||
/// 智能处理服务 - 使用 Agent Framework 重构
|
||||
/// </summary>
|
||||
public interface ISmartHandleServiceV2
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Agent Framework 进行智能分类
|
||||
/// </summary>
|
||||
Task<AgentResult<ClassificationResult[]>> SmartClassifyAgentAsync(
|
||||
long[] transactionIds,
|
||||
Action<(string type, string data)> chunkAction);
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Agent Framework 解析单行账单
|
||||
/// </summary>
|
||||
Task<AgentResult<AgentFramework.TransactionParseResult?>> ParseOneLineBillAgentAsync(string text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 智能处理服务实现 - Agent Framework 版本
|
||||
/// </summary>
|
||||
public class SmartHandleServiceV2(
|
||||
ClassificationAgent classificationAgent,
|
||||
ParsingAgent parsingAgent,
|
||||
ITransactionCategoryRepository categoryRepository,
|
||||
ILogger<SmartHandleServiceV2> logger
|
||||
) : ISmartHandleServiceV2
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Agent Framework 进行智能分类
|
||||
/// </summary>
|
||||
public async Task<AgentResult<ClassificationResult[]>> SmartClassifyAgentAsync(
|
||||
long[] transactionIds,
|
||||
Action<(string type, string data)> chunkAction)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始执行智能分类 Agent,ID 数量: {Count}", transactionIds.Length);
|
||||
|
||||
var result = await classificationAgent.ExecuteAsync(transactionIds, categoryRepository);
|
||||
|
||||
logger.LogInformation("分类完成:{Summary}", result.Summary);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "智能分类 Agent 执行失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Agent Framework 解析单行账单
|
||||
/// </summary>
|
||||
public async Task<AgentResult<AgentFramework.TransactionParseResult?>> ParseOneLineBillAgentAsync(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始解析账单: {Text}", text);
|
||||
|
||||
var result = await parsingAgent.ExecuteAsync(text);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
logger.LogInformation("解析成功: {Summary}", result.Summary);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("解析失败: {Error}", result.Error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "解析 Agent 执行失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,6 +428,7 @@ const handleDelete = (budget) => {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
margin-top: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.van-tabs__content) {
|
||||
@@ -435,6 +436,7 @@ const handleDelete = (budget) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.van-tab__panel) {
|
||||
@@ -442,6 +444,7 @@ const handleDelete = (budget) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.budget-list {
|
||||
@@ -456,6 +459,8 @@ const handleDelete = (budget) => {
|
||||
.scroll-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
|
||||
@@ -9,6 +9,7 @@ using Repository;
|
||||
public class TransactionRecordController(
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
ISmartHandleService smartHandleService,
|
||||
ISmartHandleServiceV2 smartHandleServiceV2,
|
||||
ILogger<TransactionRecordController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
@@ -603,28 +604,28 @@ public class TransactionRecordController(
|
||||
/// 一句话录账解析
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<TransactionParseResult>> ParseOneLine([FromBody] ParseOneLineRequestDto request)
|
||||
public async Task<BaseResponse<Service.AgentFramework.TransactionParseResult>> ParseOneLine([FromBody] ParseOneLineRequestDto request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Text))
|
||||
{
|
||||
return "请求参数缺失:text".Fail<TransactionParseResult>();
|
||||
return "请求参数缺失:text".Fail<Service.AgentFramework.TransactionParseResult>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await smartHandleService.ParseOneLineBillAsync(request.Text);
|
||||
var agentResult = await smartHandleServiceV2.ParseOneLineBillAgentAsync(request.Text);
|
||||
|
||||
if (result == null)
|
||||
if (agentResult?.Data == null)
|
||||
{
|
||||
return "AI解析失败".Fail<TransactionParseResult>();
|
||||
return "AI解析失败".Fail<Service.AgentFramework.TransactionParseResult>();
|
||||
}
|
||||
|
||||
return result.Ok();
|
||||
return agentResult.Data.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "一句话录账解析失败,文本: {Text}", request.Text);
|
||||
return ("AI解析失败: " + ex.Message).Fail<TransactionParseResult>();
|
||||
return ("AI解析失败: " + ex.Message).Fail<Service.AgentFramework.TransactionParseResult>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
global using Service;
|
||||
global using Service.AgentFramework;
|
||||
global using Common;
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using WebApi.Controllers.Dto;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI;
|
||||
using FreeSql;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -33,6 +35,10 @@ builder.Services.AddControllers(options =>
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// 注册 Agent Framework
|
||||
builder.Services.AddAgentFramework();
|
||||
|
||||
#if DEBUG
|
||||
// 配置 CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -43,6 +49,7 @@ builder.Services.AddCors(options =>
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
#endif
|
||||
|
||||
// 绑定配置
|
||||
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
|
||||
@@ -111,7 +118,7 @@ var fsql = new FreeSqlBuilder()
|
||||
.UseMonitorCommand(
|
||||
cmd =>
|
||||
{
|
||||
Log.Debug("执行SQL: {Sql}", cmd.CommandText);
|
||||
Log.Verbose("执行SQL: {Sql}", cmd.CommandText);
|
||||
}
|
||||
)
|
||||
.Build();
|
||||
@@ -121,6 +128,9 @@ builder.Services.AddSingleton(fsql);
|
||||
// 自动扫描注册服务和仓储
|
||||
builder.Services.AddServices();
|
||||
|
||||
// 注册 Agent Framework
|
||||
builder.Services.AddAgentFramework();
|
||||
|
||||
// 注册日志清理后台服务
|
||||
builder.Services.AddHostedService<LogCleanupService>();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DevUI" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Scalar.AspNetCore" />
|
||||
|
||||
97
docs/SmartHandleServiceV2_Architecture.md
Normal file
97
docs/SmartHandleServiceV2_Architecture.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# SmartHandleServiceV2 & Agent Framework 架构文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`SmartHandleServiceV2` 是本项目中负责智能处理的核心服务,它基于 **Agent Framework** 重构,旨在通过 AI 代理(Agents)实现复杂的业务逻辑自动化。目前主要包含以下功能:
|
||||
- **智能分类**:自动识别交易记录并归类。
|
||||
- **单行账单解析**:从非结构化文本中提取账单信息。
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
本项目采用 **Agent 模式**,将复杂的业务流程封装为独立的 Agent,每个 Agent 继承自基类 `BaseAgent`,具备统一的执行流、日志记录、步骤追踪和错误处理能力。
|
||||
|
||||
### 2.1 核心组件
|
||||
|
||||
* **`ISmartHandleServiceV2` / `SmartHandleServiceV2`**: 服务入口,负责协调各个 Agent 的执行。它不包含具体的业务逻辑,而是作为 Facade 层调用具体的 Agent。
|
||||
* **`BaseAgent` (`Service.AgentFramework`)**: Agent 的基类,提供了以下核心能力:
|
||||
* **步骤记录 (`RecordStep`)**: 记录执行过程中的关键步骤,用于调试和可视化。
|
||||
* **工具调用 (`CallToolAsync`)**: 统一封装工具调用,包含性能监控和异常处理。
|
||||
* **结果封装 (`CreateResult`)**: 返回标准化的 `AgentResult<T>`。
|
||||
* **可观测性**: 集成了 `ActivitySource` ("Microsoft.Agents.Workflows"),支持分布式追踪。
|
||||
* **`AgentResult<T>`**: 标准化的返回结果,包含数据 (`Data`)、摘要 (`Summary`)、执行步骤 (`Steps`)、元数据 (`Metadata`) 和 错误信息 (`Error`)。
|
||||
|
||||
## 3. 详细工作流
|
||||
|
||||
### 3.1 智能分类 Agent (`ClassificationAgent`)
|
||||
|
||||
该 Agent 负责对未分类的交易记录进行批量智能分类。其执行流程 (`ExecuteAsync`) 分为四个阶段:
|
||||
|
||||
1. **数据采集 (Data Collection)**
|
||||
* 调用 `QueryUnclassifiedRecordsAsync` 获取待分类的交易记录。
|
||||
* 如果没有待分类记录,直接返回。
|
||||
|
||||
2. **分析阶段 (Analysis)**
|
||||
* **分组**: 将交易记录按摘要 (`Reason`) 分组,计算每个组的总金额和样本类型。
|
||||
* **关键词提取**: 对每个分组的摘要提取关键词。
|
||||
* **相似度匹配**: 根据关键词查询历史已分类的相似记录,作为 AI 的参考依据。
|
||||
|
||||
3. **决策阶段 (Decision)**
|
||||
* 构建 Prompt:包含分类列表 (`categoryInfo`) 和待分类的分组信息 (`billsInfo`)。
|
||||
* 调用 AI (`_aiTools.ClassifyTransactionsAsync`) 进行批量分类决策。
|
||||
* AI 返回 NDJSON 格式的分类结果。
|
||||
|
||||
4. **结果保存 (Result Saving)**
|
||||
* 遍历 AI 的分类结果,更新数据库中的交易记录 (`UpdateTransactionClassifyAsync`)。
|
||||
* 生成多轮执行总结。
|
||||
|
||||
### 3.2 单行账单解析 Agent (`ParsingAgent`)
|
||||
|
||||
该 Agent 负责将一段文本解析为结构化的交易记录。其执行流程 (`ExecuteAsync`) 如下:
|
||||
|
||||
1. **文本分析**: 分析文本结构(如分隔符、行数等)。
|
||||
2. **关键词提取**: 提取文本中的关键信息(如金额、商家、日期)。
|
||||
3. **AI 解析**: 调用 AI 模型将非结构化文本转换为结构化数据。
|
||||
4. **结果封装**: 返回解析后的 `TransactionParseResult` 对象。
|
||||
|
||||
## 4. 关键类与模型
|
||||
|
||||
### 4.1 数据模型
|
||||
|
||||
* **`ClassificationResult`**: 分类结果,包含摘要、分类名称、交易类型、置信度和参考记录。
|
||||
* **`TransactionParseResult`**: 解析结果,包含金额、摘要、日期、类型和分类。
|
||||
* **`ExecutionStep`**: 执行步骤记录,包含步骤名、描述、状态、耗时和输出。
|
||||
|
||||
### 4.2 依赖服务
|
||||
|
||||
* `IToolRegistry`: 工具注册表,用于管理和调用各种工具。
|
||||
* `ITransactionQueryTools`: 交易查询工具,用于数据库交互。
|
||||
* `ITextProcessingTools`: 文本处理工具,用于关键词提取和结构分析。
|
||||
* `IAITools`: AI 工具,用于与 LLM 交互。
|
||||
|
||||
## 5. 维护与扩展指南
|
||||
|
||||
### 5.1 添加新的 Agent
|
||||
|
||||
1. 在 `Service.AgentFramework` 目录下创建新的 Agent 类,继承自 `BaseAgent`。
|
||||
2. 在构造函数中注入所需的工具和服务。
|
||||
3. 实现 `ExecuteAsync` 方法,定义 Agent 的工作流。
|
||||
4. 使用 `RecordStep` 记录关键步骤。
|
||||
5. 在 `SmartHandleServiceV2` 中注入并调用新的 Agent。
|
||||
|
||||
### 5.2 调试与排错
|
||||
|
||||
* **查看日志**: Agent 会记录详细的执行日志,包括每个步骤的输入输出。
|
||||
* **检查 `AgentResult.Steps`**: 返回结果中包含了完整的执行步骤链,可以查看每一步的状态和耗时。
|
||||
* **Activity 追踪**: 利用 `ActivitySource` 可以结合 APM 工具(如 OpenTelemetry)进行链路追踪。
|
||||
|
||||
### 5.3 修改 AI 逻辑
|
||||
|
||||
* **Prompt 调整**: `ClassificationAgent` 中的 `BuildSystemPrompt` and `BuildUserPrompt` 方法定义了与 AI 的交互逻辑。修改这些 Prompt 可以调整 AI 的行为。
|
||||
* **模型参数**: AI 调用的具体参数(如温度、模型版本)通常在 `IAITools` 的实现中配置。
|
||||
|
||||
## 6. 代码引用
|
||||
|
||||
* [SmartHandleServiceV2.cs](../Service/SmartHandleServiceV2.cs)
|
||||
* [BaseAgent.cs](../Service/AgentFramework/BaseAgent.cs)
|
||||
* [ClassificationAgent.cs](../Service/AgentFramework/ClassificationAgent.cs)
|
||||
* [ParsingAgent.cs](../Service/AgentFramework/ParsingAgent.cs)
|
||||
Reference in New Issue
Block a user