267 lines
13 KiB
C#
267 lines
13 KiB
C#
using Health.Application.Medications;
|
||
|
||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||
|
||
/// <summary>
|
||
/// 药管家 Agent 工具处理器。
|
||
/// </summary>
|
||
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<ToolDefinition> Tools => [ManageMedicationTool, CommonAgentHandler.CheckArchiveTool];
|
||
public static async Task<object> 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<object> 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<object> 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<object> 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<object> 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<MedicationFrequency>(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<MedicationFrequency>(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<TimeOnly> ReadTimes(JsonElement args)
|
||
{
|
||
List<TimeOnly> 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;
|
||
}
|
||
}
|