using Health.Application.Medications; namespace Health.Infrastructure.AI.AgentHandlers; /// /// 药提醒 Agent 工具处理器。 /// public static class MedicationAgentHandler { public static readonly ToolDefinition ManageMedicationTool = new() { Function = new() { Name = "manage_medication", Description = "用药管理(创建计划、按日期查询计划与逐顿服药状态、确认某一顿服药)", Parameters = new { type = "object", properties = new { action = new { type = "string", @enum = new[] { "create", "query", "confirm" }, description = "create/query/confirm" }, scope = new { type = "string", @enum = new[] { "today", "scheduled_date", "current_plans", "upcoming_plans", "ended_plans", "inactive_plans", "all_plans" }, description = "查询时必填:今日安排/指定日期安排/当前计划/即将开始/自然结束/已停用/全部计划" }, date = new { type = "string", description = "仅 scope=scheduled_date 时必填,使用北京时间日期 yyyy-MM-dd" }, medication_id = new { type = "string", description = "药品 ID,确认服药时使用" }, scheduled_time = new { type = "string", description = "要确认的计划服药时间 HH:mm" }, name = new { type = "string", description = "药品名称" }, dosage = new { type = "string", description = "剂量,如 100mg" }, frequency = new { type = "string", @enum = new[] { "Daily", "TwiceDaily", "ThreeTimesDaily", "EveryOtherDay", "Weekly", "AsNeeded" }, description = "服药频率" }, time_of_day = new { type = "array", items = new { type = "string" }, description = "服药时间,如 [\"08:00\",\"20:00\"]" }, duration_days = new { type = "integer", description = "服用天数" }, long_term = new { type = "boolean", description = "是否明确为长期服用;与 duration_days 二选一" }, start_date = new { type = "string", description = "开始日期 yyyy-MM-dd" }, }, required = new[] { "action" } } } }; public static List Tools => [ManageMedicationTool, CommonAgentHandler.CheckArchiveTool]; public static async Task Execute( string toolName, JsonElement args, Guid userId, IMedicationService medications, CancellationToken ct = default) { return toolName switch { "manage_medication" => await ExecuteManageMedication(medications, userId, args, ct), _ => new { success = false, message = $"未知工具: {toolName}" } }; } private static async Task ExecuteManageMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct) { var action = args.TryGetProperty("action", out var a) ? a.GetString()?.ToLowerInvariant() : null; return action switch { "create" => await CreateMedication(medications, userId, args, ct), "query" => await QueryMedications(medications, userId, args, ct), "confirm" => await ConfirmMedication(medications, userId, args, ct), _ => new { success = false, message = $"未知操作: {action}" } }; } private static async Task CreateMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct) { var validationError = ValidateWriteArguments(args); if (validationError != null) return new { success = false, message = validationError }; var name = args.TryGetProperty("name", out var n) ? n.GetString()! : ""; var dosage = args.TryGetProperty("dosage", out var dg) ? dg.GetString() : null; var frequency = ReadFrequency(args); var times = ReadTimes(args); var durationDays = args.TryGetProperty("duration_days", out var dd) ? dd.GetInt32() : 0; var longTerm = args.TryGetProperty("long_term", out var longTermValue) && longTermValue.ValueKind == JsonValueKind.True; var startDate = ReadStartDate(args); // 结束日为包含式(IsActiveOn 用 EndDate >= date 判断),故 N 天疗程结束日 = 起始日 + (N-1) DateOnly? endDate = longTerm ? null : startDate.AddDays(durationDays - 1); var medicationId = await medications.CreateAsync(userId, new MedicationUpsertRequest( name, dosage, frequency, times.Count > 0 ? times : [new TimeOnly(8, 0)], startDate, endDate, MedicationSource.AiEntry, null), ct); var timeLabels = times.Count > 0 ? string.Join(", ", times.Select(t => t.ToString("HH:mm"))) : "08:00"; return new { success = true, medication_id = medicationId, name, dosage, frequency = frequency.ToString(), time = timeLabels, start_date = startDate.ToString("yyyy-MM-dd"), duration_days = durationDays, long_term = longTerm, }; } private static async Task QueryMedications( IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct) { var scope = args.TryGetProperty("scope", out var scopeValue) ? scopeValue.GetString()?.ToLowerInvariant() : null; if (scope is not ("today" or "scheduled_date" or "current_plans" or "upcoming_plans" or "ended_plans" or "inactive_plans" or "all_plans")) return new { success = false, message = "查询用药时必须明确 scope:today、scheduled_date、current_plans、upcoming_plans、ended_plans、inactive_plans 或 all_plans", }; if (scope == "scheduled_date" && (!args.TryGetProperty("date", out var requestedDate) || !DateOnly.TryParse(requestedDate.GetString(), out _))) return new { success = false, message = "查询指定日期的用药安排时必须提供有效的 date" }; var date = scope == "scheduled_date" ? DateOnly.Parse(args.GetProperty("date").GetString()!) : AiDateTime.BeijingToday; var isDateScope = scope is "today" or "scheduled_date"; var overview = await medications.GetAiOverviewAsync(userId, date, ct); var plans = scope switch { "all_plans" => overview.ToList(), "current_plans" => overview.Where(plan => plan.Phase == "active").ToList(), "upcoming_plans" => overview .Where(plan => plan.Phase == "upcoming") .OrderBy(plan => plan.StartDate) .ToList(), "ended_plans" => overview .Where(plan => plan.Phase == "ended") .OrderByDescending(plan => plan.EndDate) .ToList(), "inactive_plans" => overview .Where(plan => plan.Phase == "inactive") .OrderByDescending(plan => plan.EndDate) .ToList(), _ => overview.Where(plan => plan.ScheduledOnDate).ToList(), }; var doseStatuses = plans.SelectMany(plan => plan.Doses).ToList(); var nextPlan = overview .Where(plan => plan.Phase == "upcoming" && plan.StartDate.HasValue) .OrderBy(plan => plan.StartDate) .FirstOrDefault(); return new { query_date = date.ToString("yyyy-MM-dd"), scope, count = plans.Count, all_doses_completed = isDateScope ? doseStatuses.Count > 0 && doseStatuses.All(dose => dose.Status == "taken") : (bool?)null, has_upcoming_plans = nextPlan != null, next_start_date = nextPlan?.StartDate?.ToString("yyyy-MM-dd"), days_until_next_start = nextPlan?.StartDate is DateOnly nextStart ? nextStart.DayNumber - date.DayNumber : (int?)null, plans, }; } private static async Task ConfirmMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct) { var validationError = ValidateWriteArguments(args); if (validationError != null) return new { success = false, message = validationError }; var medId = args.TryGetProperty("medication_id", out var mid) && mid.TryGetGuid(out var parsedMedicationId) ? parsedMedicationId : Guid.Empty; if (!args.TryGetProperty("scheduled_time", out var timeValue) || !TimeOnly.TryParse(timeValue.GetString(), out var scheduledTime)) return new { success = false, message = "缺少要确认的计划服药时间" }; var success = await medications.ConfirmDoseAsync( userId, medId, scheduledTime, MedicationLogStatus.Taken, ct); return success == true ? new { success = true, medication_id = medId, scheduled_time = scheduledTime.ToString("HH:mm") } : new { success = false, message = success == null ? "药品不存在" : "该时间不属于今天的服药安排,或这一顿已经记录" }; } public static string? ValidateWriteArguments(JsonElement args) { var action = args.TryGetProperty("action", out var actionValue) ? actionValue.GetString()?.ToLowerInvariant() : null; if (action == "confirm") { if (!args.TryGetProperty("medication_id", out var medicationId) || !medicationId.TryGetGuid(out _)) return "确认服药时缺少有效的药品 ID"; if (!args.TryGetProperty("scheduled_time", out var scheduledTime) || !TimeOnly.TryParse(scheduledTime.GetString(), out _)) return "确认服药时缺少具体的计划服药时间"; return null; } if (action != "create") return null; if (!args.TryGetProperty("name", out var name) || string.IsNullOrWhiteSpace(name.GetString())) return "创建用药计划前需要确认药品名称"; if (!args.TryGetProperty("dosage", out var dosage) || string.IsNullOrWhiteSpace(dosage.GetString())) return "创建用药计划前需要确认每次剂量"; if (!args.TryGetProperty("frequency", out var frequencyValue) || !Enum.TryParse(frequencyValue.GetString(), ignoreCase: true, out _)) return "创建用药计划前需要确认服药频率"; if (!args.TryGetProperty("time_of_day", out var times) || times.ValueKind != JsonValueKind.Array) return "创建用药计划前需要确认具体服药时间"; var timeValues = times.EnumerateArray().ToList(); if (timeValues.Count == 0 || timeValues.Any(value => !TimeOnly.TryParse(value.GetString(), out _))) return "创建用药计划前需要提供有效的具体服药时间"; if (!args.TryGetProperty("start_date", out var startDate) || !DateOnly.TryParse(startDate.GetString(), out _)) return "创建用药计划前需要确认开始日期"; var durationDays = 0; var hasDuration = args.TryGetProperty("duration_days", out var duration) && duration.TryGetInt32(out durationDays) && durationDays > 0; var isLongTerm = args.TryGetProperty("long_term", out var longTerm) && longTerm.ValueKind == JsonValueKind.True; if (hasDuration && durationDays > 3650) return "服用天数不能超过3650天;长期服用请明确设置 long_term=true"; if (hasDuration && isLongTerm) return "服用天数和长期服用不能同时设置"; if (!hasDuration && !isLongTerm) return "创建用药计划前需要确认服用天数,或明确说明长期服用"; return null; } private static MedicationFrequency ReadFrequency(JsonElement args) { var frequencyStr = args.TryGetProperty("frequency", out var f) ? f.GetString() : "Daily"; return Enum.TryParse(frequencyStr, ignoreCase: true, out var frequency) ? frequency : MedicationFrequency.Daily; } private static DateOnly ReadStartDate(JsonElement args) { var startDateStr = args.TryGetProperty("start_date", out var sd) && sd.GetString() is string sds ? sds : null; return DateOnly.TryParse(startDateStr, out var parsedDate) ? parsedDate : DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)); } private static List ReadTimes(JsonElement args) { List times = []; if (!args.TryGetProperty("time_of_day", out var tod) || tod.ValueKind != JsonValueKind.Array) return times; foreach (var t in tod.EnumerateArray()) { if (TimeOnly.TryParse(t.GetString(), out var parsed)) times.Add(parsed); } return times; } }