feat: AI Agent 处理器扩展 + 服药打卡确认链路 + AI 气泡配色微调 + 数据刷新 providers 抽离 + 多端页面细节调整
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
using Health.Application.Diets;
|
||||
using Health.Application.Notifications;
|
||||
using Health.Application.Reports;
|
||||
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 统一助手使用的患者本人业务数据只读工具。
|
||||
/// </summary>
|
||||
public static class PatientReadAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition QueryDietRecordsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_diet_records",
|
||||
Description = "查询当前用户指定北京时间日期的饮食记录和总热量",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
date = new { type = "string", description = "北京时间日期 yyyy-MM-dd,默认今天" },
|
||||
meal_type = new { type = "string", @enum = new[] { "Breakfast", "Lunch", "Dinner", "Snack" }, description = "可选餐次" },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ToolDefinition QueryFollowUpsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_followups",
|
||||
Description = "查询当前用户的复查随访安排",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
scope = new { type = "string", @enum = new[] { "next", "upcoming", "completed", "date", "all" }, description = "查询时必填:下一次/未来安排/已完成/指定日期/全部" },
|
||||
date = new { type = "string", description = "scope=date 时的北京时间日期 yyyy-MM-dd" },
|
||||
limit = new { type = "integer", description = "最多返回条数,1到20" },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ToolDefinition QueryReportsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_reports",
|
||||
Description = "查询当前用户已经保存的检查报告状态、AI摘要和医生审核结论,不读取其他用户报告",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
limit = new { type = "integer", description = "最多返回条数,1到10,默认5" },
|
||||
status = new { type = "string", description = "可选报告状态" },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ToolDefinition QueryNotificationsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_notifications",
|
||||
Description = "查询当前用户的站内通知",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
scope = new { type = "string", @enum = new[] { "unread", "today", "recent" }, description = "默认 unread" },
|
||||
limit = new { type = "integer", description = "最多返回条数,1到20" },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<object> QueryDietAsync(
|
||||
IDietService diets,
|
||||
Guid userId,
|
||||
JsonElement args,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (args.TryGetProperty("date", out var suppliedDate) && !DateOnly.TryParse(suppliedDate.GetString(), out _))
|
||||
return new { success = false, message = "查询饮食记录时提供了无效的 date" };
|
||||
var date = args.TryGetProperty("date", out var dateValue) && DateOnly.TryParse(dateValue.GetString(), out var parsedDate)
|
||||
? parsedDate
|
||||
: AiDateTime.BeijingToday;
|
||||
var mealType = args.TryGetProperty("meal_type", out var mealValue) ? mealValue.GetString() : null;
|
||||
var records = await diets.ListAsync(userId, date.ToString("yyyy-MM-dd"), mealType, ct);
|
||||
return new
|
||||
{
|
||||
query_date = date.ToString("yyyy-MM-dd"),
|
||||
count = records.Count,
|
||||
total_calories = records.Sum(record => record.TotalCalories ?? 0),
|
||||
records = records.Select(record => new
|
||||
{
|
||||
record.Id,
|
||||
record.MealType,
|
||||
record.TotalCalories,
|
||||
record.HealthScore,
|
||||
record.RecordedAt,
|
||||
record.FoodItems,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<object> QueryFollowUpsAsync(
|
||||
AppDbContext db,
|
||||
Guid userId,
|
||||
JsonElement args,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var scope = args.TryGetProperty("scope", out var scopeValue)
|
||||
? scopeValue.GetString()?.ToLowerInvariant()
|
||||
: null;
|
||||
if (scope is not ("next" or "upcoming" or "completed" or "date" or "all"))
|
||||
return new
|
||||
{
|
||||
success = false,
|
||||
message = "查询复查安排时必须明确 scope:next、upcoming、completed、date 或 all",
|
||||
};
|
||||
var limit = ReadLimit(args, 10, 20);
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
var query = db.FollowUps.AsNoTracking().Where(item => item.UserId == userId);
|
||||
|
||||
if (scope is "next" or "upcoming")
|
||||
query = query.Where(item => item.Status == FollowUpStatus.Upcoming && item.ScheduledAt >= nowUtc);
|
||||
else if (scope == "completed")
|
||||
query = query.Where(item => item.Status == FollowUpStatus.Completed);
|
||||
else if (scope == "date")
|
||||
{
|
||||
if (!args.TryGetProperty("date", out var dateValue) || !DateOnly.TryParse(dateValue.GetString(), out var date))
|
||||
return new { success = false, message = "按日期查询复查时必须提供有效的 date" };
|
||||
var (startUtc, endUtc) = AiDateTime.BeijingDateWindowUtc(date);
|
||||
query = query.Where(item => item.ScheduledAt >= startUtc && item.ScheduledAt < endUtc);
|
||||
}
|
||||
|
||||
var take = scope == "next" ? 1 : limit;
|
||||
var orderedQuery = scope is "all" or "completed"
|
||||
? query.OrderByDescending(item => item.ScheduledAt)
|
||||
: query.OrderBy(item => item.ScheduledAt);
|
||||
var matchingCount = await query.CountAsync(ct);
|
||||
var items = await orderedQuery
|
||||
.Take(take)
|
||||
.Select(item => new
|
||||
{
|
||||
item.Id,
|
||||
item.Title,
|
||||
item.DoctorName,
|
||||
item.Department,
|
||||
item.ScheduledAt,
|
||||
item.Notes,
|
||||
item.Status,
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
scope,
|
||||
count = items.Count,
|
||||
matching_count = matchingCount,
|
||||
results_truncated = matchingCount > items.Count,
|
||||
followups = items.Select(item => new
|
||||
{
|
||||
item.Id,
|
||||
item.Title,
|
||||
item.DoctorName,
|
||||
item.Department,
|
||||
scheduled_at_beijing = AiDateTime.ToBeijingText(item.ScheduledAt),
|
||||
item.Notes,
|
||||
status = item.Status.ToString(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<object> QueryReportsAsync(
|
||||
IReportService reports,
|
||||
Guid userId,
|
||||
JsonElement args,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var limit = ReadLimit(args, 5, 10);
|
||||
var status = args.TryGetProperty("status", out var statusValue) ? statusValue.GetString() : null;
|
||||
var records = await reports.GetReportsAsync(userId, ct);
|
||||
var filtered = records
|
||||
.Where(report => string.IsNullOrWhiteSpace(status) || report.Status.Equals(status, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(report => report.CreatedAt)
|
||||
.Take(limit)
|
||||
.Select(report => new
|
||||
{
|
||||
report.Id,
|
||||
report.Category,
|
||||
report.Status,
|
||||
report.AiStatus,
|
||||
report.ReviewStatus,
|
||||
report.Severity,
|
||||
ai_summary = Truncate(report.AiSummary, 1800),
|
||||
doctor_comment = Truncate(report.DoctorComment, 1000),
|
||||
doctor_recommendation = Truncate(report.DoctorRecommendation, 1000),
|
||||
report.DoctorName,
|
||||
reviewed_at_beijing = report.ReviewedAt.HasValue ? AiDateTime.ToBeijingText(report.ReviewedAt.Value) : null,
|
||||
created_at_beijing = AiDateTime.ToBeijingText(report.CreatedAt),
|
||||
})
|
||||
.ToList();
|
||||
return new { count = filtered.Count, reports = filtered };
|
||||
}
|
||||
|
||||
public static async Task<object> QueryNotificationsAsync(
|
||||
IInAppNotificationService notifications,
|
||||
Guid userId,
|
||||
JsonElement args,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var scope = args.TryGetProperty("scope", out var scopeValue)
|
||||
? scopeValue.GetString()?.ToLowerInvariant() ?? "unread"
|
||||
: "unread";
|
||||
if (scope is not ("unread" or "today" or "recent")) scope = "unread";
|
||||
var limit = ReadLimit(args, 10, 20);
|
||||
var records = scope == "unread"
|
||||
? await notifications.GetPendingAsync(userId, ct)
|
||||
: await notifications.GetHistoryAsync(userId, ct);
|
||||
var (todayStartUtc, todayEndUtc) = AiDateTime.BeijingDateWindowUtc(AiDateTime.BeijingToday);
|
||||
var filtered = records
|
||||
.Where(item => scope != "today" ||
|
||||
(AiDateTime.ToUtc(item.CreatedAt) >= todayStartUtc && AiDateTime.ToUtc(item.CreatedAt) < todayEndUtc))
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Take(limit)
|
||||
.Select(item => new
|
||||
{
|
||||
item.Id,
|
||||
item.Type,
|
||||
item.Title,
|
||||
item.Message,
|
||||
item.Severity,
|
||||
item.IsRead,
|
||||
created_at_beijing = AiDateTime.ToBeijingText(item.CreatedAt),
|
||||
})
|
||||
.ToList();
|
||||
return new { scope, count = filtered.Count, notifications = filtered };
|
||||
}
|
||||
|
||||
private static int ReadLimit(JsonElement args, int defaultValue, int max) =>
|
||||
args.TryGetProperty("limit", out var value) && value.TryGetInt32(out var parsed)
|
||||
? Math.Clamp(parsed, 1, max)
|
||||
: defaultValue;
|
||||
|
||||
private static string? Truncate(string? value, int maxLength) =>
|
||||
string.IsNullOrWhiteSpace(value) || value.Length <= maxLength ? value : value[..maxLength] + "…";
|
||||
}
|
||||
Reference in New Issue
Block a user