feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
using Health.Application.HealthArchives;
|
||||
using Health.Application.HealthRecords;
|
||||
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
@@ -21,38 +24,33 @@ public static class CommonAgentHandler
|
||||
|
||||
public static List<ToolDefinition> Tools => [QueryHealthRecordsTool, CheckArchiveTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
public static async Task<object> Execute(
|
||||
string toolName,
|
||||
JsonElement args,
|
||||
Guid userId,
|
||||
IHealthRecordService healthRecords,
|
||||
IHealthArchiveService archives,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" => await ExecuteQueryHealthRecords(db, userId, args),
|
||||
"check_archive" => await ExecuteCheckArchive(db, userId),
|
||||
"query_health_records" => await ExecuteQueryHealthRecords(healthRecords, userId, args, ct),
|
||||
"check_archive" => await ExecuteCheckArchive(archives, userId, ct),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteQueryHealthRecords(AppDbContext db, Guid userId, JsonElement args)
|
||||
private static async Task<object> ExecuteQueryHealthRecords(IHealthRecordService healthRecords, Guid userId, JsonElement args, CancellationToken ct)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString() : null;
|
||||
var days = args.TryGetProperty("days", out var d) ? d.GetInt32() : 7;
|
||||
|
||||
var query = db.HealthRecords.Where(r => r.UserId == userId);
|
||||
if (!string.IsNullOrEmpty(type) && Enum.TryParse<HealthMetricType>(type, ignoreCase: true, out var mt))
|
||||
query = query.Where(r => r.MetricType == mt);
|
||||
|
||||
query = query.Where(r => r.RecordedAt >= DateTime.UtcNow.AddDays(-days));
|
||||
|
||||
var records = await query.OrderByDescending(r => r.RecordedAt).Take(30).Select(r => new
|
||||
{
|
||||
r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit, r.IsAbnormal, r.RecordedAt,
|
||||
}).ToListAsync();
|
||||
|
||||
var records = await healthRecords.GetRecordsAsync(userId, type, days, ct);
|
||||
return new { count = records.Count, records };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteCheckArchive(AppDbContext db, Guid userId)
|
||||
private static async Task<object> ExecuteCheckArchive(IHealthArchiveService archives, Guid userId, CancellationToken ct)
|
||||
{
|
||||
var archive = await db.HealthArchives.FirstOrDefaultAsync(a => a.UserId == userId);
|
||||
var archive = await archives.GetAsync(userId, ct);
|
||||
if (archive == null) return new { found = false };
|
||||
return new
|
||||
{
|
||||
@@ -85,49 +83,26 @@ public static class CommonAgentHandler
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<object> ExecuteManageArchive(AppDbContext db, Guid userId, JsonElement args)
|
||||
public static Task<object> ExecuteManageArchive(
|
||||
IHealthArchiveService archives,
|
||||
Guid userId,
|
||||
JsonElement args,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
var archive = await db.HealthArchives.FirstOrDefaultAsync(x => x.UserId == userId);
|
||||
if (archive == null)
|
||||
{
|
||||
archive = new HealthArchive { Id = Guid.NewGuid(), UserId = userId };
|
||||
db.HealthArchives.Add(archive);
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "update_diagnosis":
|
||||
archive.Diagnosis = args.TryGetProperty("diagnosis", out var d) ? d.GetString() : archive.Diagnosis;
|
||||
break;
|
||||
case "update_surgery":
|
||||
archive.SurgeryType = args.TryGetProperty("surgery_type", out var st) ? st.GetString() : archive.SurgeryType;
|
||||
if (args.TryGetProperty("surgery_date", out var sd))
|
||||
archive.SurgeryDate = DateOnly.TryParse(sd.GetString(), out var date) ? date : archive.SurgeryDate;
|
||||
break;
|
||||
case "update_allergies":
|
||||
if (args.TryGetProperty("allergies", out var al) && al.ValueKind == JsonValueKind.Array)
|
||||
archive.Allergies = [.. al.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
case "update_chronic_diseases":
|
||||
if (args.TryGetProperty("chronic_diseases", out var cd) && cd.ValueKind == JsonValueKind.Array)
|
||||
archive.ChronicDiseases = [.. cd.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
case "update_diet_restrictions":
|
||||
if (args.TryGetProperty("diet_restrictions", out var dr) && dr.ValueKind == JsonValueKind.Array)
|
||||
archive.DietRestrictions = [.. dr.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
default: // query
|
||||
return new
|
||||
{
|
||||
found = true, archive.Diagnosis, archive.SurgeryType,
|
||||
SurgeryDate = archive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
archive.Allergies, archive.DietRestrictions, archive.ChronicDiseases, archive.FamilyHistory,
|
||||
};
|
||||
}
|
||||
|
||||
archive.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
var request = new HealthArchiveUpdateRequest(
|
||||
args.TryGetProperty("diagnosis", out var diagnosis) ? diagnosis.GetString() : null,
|
||||
args.TryGetProperty("surgery_type", out var surgeryType) ? surgeryType.GetString() : null,
|
||||
args.TryGetProperty("surgery_date", out var surgeryDate) && DateOnly.TryParse(surgeryDate.GetString(), out var date) ? date : null,
|
||||
ReadStringArray(args, "allergies"),
|
||||
ReadStringArray(args, "diet_restrictions"),
|
||||
ReadStringArray(args, "chronic_diseases"),
|
||||
args.TryGetProperty("family_history", out var familyHistory) ? familyHistory.GetString() : null);
|
||||
return archives.ExecuteAiActionAsync(userId, action, request, ct);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string>? ReadStringArray(JsonElement args, string propertyName) =>
|
||||
args.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.Array
|
||||
? value.EnumerateArray().Select(x => x.GetString()).Where(x => !string.IsNullOrWhiteSpace(x)).Cast<string>().ToList()
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,4 @@ public static class ConsultationAgentHandler
|
||||
{
|
||||
public static List<ToolDefinition> Tools => [CommonAgentHandler.QueryHealthRecordsTool, CommonAgentHandler.CheckArchiveTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" or "check_archive" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,4 @@ public static class DietAgentHandler
|
||||
{
|
||||
public static List<ToolDefinition> Tools => [CommonAgentHandler.CheckArchiveTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"check_archive" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,83 @@
|
||||
using Health.Application.Exercises;
|
||||
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 运动计划 Agent 工具处理器
|
||||
/// </summary>
|
||||
public static class ExerciseAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition ManageExerciseTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_exercise", Description = "运动计划管理(创建/查询/打卡)",
|
||||
Parameters = new { type = "object", properties = new {
|
||||
action = new { type = "string", description = "create/query/checkin" },
|
||||
week_start_date = new { type = "string", description = "开始日期 yyyy-MM-dd" },
|
||||
items = new { type = "array", description = "每天的运动项目", items = new { type = "object", properties = new {
|
||||
day_of_week = new { type = "integer", description = "0=周日 1=周一...6=周六" },
|
||||
exercise_type = new { type = "string", description = "运动类型,如散步/慢跑/游泳/太极" },
|
||||
duration_minutes = new { type = "integer", description = "时长(分钟)" },
|
||||
is_rest_day = new { type = "boolean", description = "是否休息日" }
|
||||
}}}
|
||||
}, required = new[] { "action" } }
|
||||
}
|
||||
Name = "manage_exercise",
|
||||
Description = "运动计划管理(创建/查询/打卡)",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
action = new { type = "string", description = "create/query/checkin" },
|
||||
start_date = new { type = "string", description = "开始日期 yyyy-MM-dd,默认今天" },
|
||||
duration_days = new { type = "integer", description = "连续运动天数,如一周为7天" },
|
||||
exercise_type = new { type = "string", description = "运动类型,如散步、慢跑、游泳" },
|
||||
duration_minutes = new { type = "integer", description = "每天运动时长,单位分钟" },
|
||||
reminder_time = new { type = "string", description = "每天提醒时间 HH:mm,默认19:00" },
|
||||
item_id = new { type = "string", description = "打卡条目 ID" },
|
||||
},
|
||||
required = new[] { "action" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [ManageExerciseTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
public static async Task<object> Execute(
|
||||
string toolName,
|
||||
JsonElement args,
|
||||
AppDbContext db,
|
||||
Guid userId,
|
||||
IExerciseService? exercises = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return toolName switch
|
||||
if (toolName != "manage_exercise" || exercises == null)
|
||||
return new { success = false, message = $"未知工具: {toolName}" };
|
||||
|
||||
var action = args.TryGetProperty("action", out var actionValue) ? actionValue.GetString() : "query";
|
||||
if (action == "create")
|
||||
{
|
||||
"manage_exercise" => await ExecuteManageExercise(db, userId, args),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageExercise(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
switch (action)
|
||||
{
|
||||
case "create":
|
||||
var weekStart = args.TryGetProperty("week_start_date", out var wsd) ? DateOnly.Parse(wsd.GetString()!) : DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8));
|
||||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = weekStart };
|
||||
if (args.TryGetProperty("items", out var items))
|
||||
{
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
plan.Items.Add(new ExercisePlanItem
|
||||
{
|
||||
Id = Guid.NewGuid(), DayOfWeek = item.GetProperty("day_of_week").GetInt32(),
|
||||
ExerciseType = item.GetProperty("exercise_type").GetString() ?? "散步",
|
||||
DurationMinutes = item.GetProperty("duration_minutes").GetInt32(),
|
||||
IsRestDay = item.TryGetProperty("is_rest_day", out var rd) && rd.GetBoolean(),
|
||||
});
|
||||
}
|
||||
}
|
||||
db.ExercisePlans.Add(plan);
|
||||
await db.SaveChangesAsync();
|
||||
var firstItem = plan.Items.FirstOrDefault();
|
||||
return new {
|
||||
success = true, plan_id = plan.Id,
|
||||
exercise_type = firstItem?.ExerciseType ?? "运动",
|
||||
duration_minutes = firstItem?.DurationMinutes ?? 30,
|
||||
day_count = plan.Items.Count,
|
||||
};
|
||||
|
||||
case "checkin":
|
||||
var itemId = args.TryGetProperty("item_id", out var iid) ? iid.GetGuid() : Guid.Empty;
|
||||
var exerciseItem = await db.ExercisePlanItems.FindAsync([itemId]);
|
||||
if (exerciseItem == null) return new { success = false, message = "条目不存在" };
|
||||
exerciseItem.IsCompleted = true;
|
||||
exerciseItem.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
|
||||
default:
|
||||
var existingPlan = await db.ExercisePlans.Where(p => p.UserId == userId)
|
||||
.OrderByDescending(p => p.WeekStartDate).FirstOrDefaultAsync();
|
||||
if (existingPlan == null) return new { found = false };
|
||||
var exerciseItems = await db.ExercisePlanItems.Where(i => i.PlanId == existingPlan.Id).OrderBy(i => i.DayOfWeek).ToListAsync();
|
||||
return new { found = true, plan_id = existingPlan.Id, items = exerciseItems.Select(i => new { i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes, i.IsCompleted }) };
|
||||
var startDate = args.TryGetProperty("start_date", out var startValue) && DateOnly.TryParse(startValue.GetString(), out var parsedStart)
|
||||
? parsedStart
|
||||
: DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8));
|
||||
var days = args.TryGetProperty("duration_days", out var daysValue) && daysValue.TryGetInt32(out var parsedDays)
|
||||
? Math.Clamp(parsedDays, 1, 366)
|
||||
: 7;
|
||||
var exerciseType = args.TryGetProperty("exercise_type", out var typeValue) ? typeValue.GetString() : "散步";
|
||||
var minutes = args.TryGetProperty("duration_minutes", out var minutesValue) && minutesValue.TryGetInt32(out var parsedMinutes) ? parsedMinutes : 30;
|
||||
var reminder = args.TryGetProperty("reminder_time", out var reminderValue) && TimeOnly.TryParse(reminderValue.GetString(), out var parsedReminder)
|
||||
? parsedReminder
|
||||
: new TimeOnly(19, 0);
|
||||
var planId = await exercises.CreateAsync(userId, new ExercisePlanCreateRequest(
|
||||
startDate, startDate.AddDays(days - 1), exerciseType, minutes, reminder), ct);
|
||||
return new { success = true, plan_id = planId, exercise_type = exerciseType, duration_minutes = minutes, day_count = days, start_date = startDate, end_date = startDate.AddDays(days - 1), reminder_time = reminder };
|
||||
}
|
||||
|
||||
if (action == "checkin")
|
||||
{
|
||||
var itemId = args.TryGetProperty("item_id", out var itemValue) && itemValue.TryGetGuid(out var parsedId) ? parsedId : Guid.Empty;
|
||||
var success = await exercises.CheckInAsync(userId, itemId, ct);
|
||||
return success ? new { success = true } : new { success = false, message = "条目不存在" };
|
||||
}
|
||||
|
||||
var plan = await exercises.GetLatestAsync(userId, ct);
|
||||
return plan == null
|
||||
? new { found = false }
|
||||
: new
|
||||
{
|
||||
found = true,
|
||||
plan_id = plan.Id,
|
||||
plan.StartDate,
|
||||
plan.EndDate,
|
||||
plan.ReminderTime,
|
||||
items = plan.Items.Select(i => new { i.Id, i.ScheduledDate, i.ExerciseType, i.DurationMinutes, i.IsCompleted }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Health.Application.HealthRecords;
|
||||
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,16 +18,50 @@ public static class HealthDataAgentHandler
|
||||
|
||||
public static List<ToolDefinition> Tools => [RecordHealthDataTool, CommonAgentHandler.QueryHealthRecordsTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId, IHealthRecordService? healthRecords = null)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"record_health_data" => await ExecuteRecordHealthData(db, userId, args),
|
||||
"query_health_records" => await CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
"record_health_data" => healthRecords == null
|
||||
? await ExecuteRecordHealthData(db, userId, args)
|
||||
: await ExecuteRecordHealthData(healthRecords, userId, args),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteRecordHealthData(IHealthRecordService healthRecords, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString()! : "";
|
||||
var (metricType, value, unit, systolic, diastolic) = ParseMetric(type, args);
|
||||
if (metricType == null)
|
||||
return new { success = false, message = $"未知指标类型: {type}" };
|
||||
|
||||
var recordedAt = args.TryGetProperty("recorded_at", out var ra) && ra.TryGetDateTime(out var dt)
|
||||
? dt
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var request = new HealthRecordUpsertRequest(
|
||||
metricType.Value,
|
||||
systolic,
|
||||
diastolic,
|
||||
value,
|
||||
unit,
|
||||
HealthRecordSource.AiEntry,
|
||||
recordedAt);
|
||||
|
||||
var id = await healthRecords.CreateAsync(userId, request, CancellationToken.None);
|
||||
var valStr = metricType == HealthMetricType.BloodPressure ? $"{systolic}/{diastolic}" : value?.ToString() ?? "";
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
record_id = id,
|
||||
type = metricType.Value.ToString(),
|
||||
value = valStr,
|
||||
unit,
|
||||
isAbnormal = HealthRecordRules.CheckAbnormal(request)
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteRecordHealthData(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString()! : "";
|
||||
@@ -82,4 +118,42 @@ public static class HealthDataAgentHandler
|
||||
};
|
||||
return new { success = true, record_id = record.Id, type = record.MetricType.ToString(), value = valStr, unit = record.Unit, isAbnormal = record.IsAbnormal };
|
||||
}
|
||||
|
||||
private static (HealthMetricType? Type, decimal? Value, string Unit, int? Systolic, int? Diastolic) ParseMetric(string type, JsonElement args)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"blood_pressure" => (
|
||||
HealthMetricType.BloodPressure,
|
||||
null,
|
||||
"mmHg",
|
||||
args.TryGetProperty("systolic", out var s) ? s.GetInt32() : null,
|
||||
args.TryGetProperty("diastolic", out var d) ? d.GetInt32() : null),
|
||||
"heart_rate" => (
|
||||
HealthMetricType.HeartRate,
|
||||
args.TryGetProperty("heart_rate", out var hr) ? hr.GetDecimal() : null,
|
||||
"次/分",
|
||||
null,
|
||||
null),
|
||||
"glucose" => (
|
||||
HealthMetricType.Glucose,
|
||||
args.TryGetProperty("glucose", out var g) ? g.GetDecimal() : null,
|
||||
"mmol/L",
|
||||
null,
|
||||
null),
|
||||
"spo2" => (
|
||||
HealthMetricType.SpO2,
|
||||
args.TryGetProperty("spo2", out var o) ? o.GetDecimal() : null,
|
||||
"%",
|
||||
null,
|
||||
null),
|
||||
"weight" => (
|
||||
HealthMetricType.Weight,
|
||||
args.TryGetProperty("weight", out var w) ? w.GetDecimal() : null,
|
||||
"kg",
|
||||
null,
|
||||
null),
|
||||
_ => (null, null, "", null, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Health.Application.Medications;
|
||||
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 药管家 Agent 工具处理器——用药管理
|
||||
/// 药管家 Agent 工具处理器。
|
||||
/// </summary>
|
||||
public static class MedicationAgentHandler
|
||||
{
|
||||
@@ -9,31 +11,105 @@ public static class MedicationAgentHandler
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_medication", Description = "用药管理(创建/查询/确认服药)",
|
||||
Parameters = new { type = "object", properties = new {
|
||||
action = new { type = "string", description = "create/query/confirm" },
|
||||
name = new { type = "string", description = "药品名称" },
|
||||
dosage = new { type = "string", description = "剂量,如 100mg" },
|
||||
frequency = new { type = "string", description = "频率,如 Daily/EveryOtherDay/Weekly" },
|
||||
time_of_day = new { type = "array", items = new { type = "string" }, description = "服药时间,如 [\"08:00\",\"20:00\"]" },
|
||||
duration_days = new { type = "integer", description = "服用天数" },
|
||||
start_date = new { type = "string", description = "开始日期 yyyy-MM-dd" },
|
||||
}, required = new[] { "action" } }
|
||||
Name = "manage_medication",
|
||||
Description = "用药管理(创建/查询/确认服药)",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
action = new { type = "string", description = "create/query/confirm" },
|
||||
medication_id = new { type = "string", description = "药品 ID,确认服药时使用" },
|
||||
name = new { type = "string", description = "药品名称" },
|
||||
dosage = new { type = "string", description = "剂量,如 100mg" },
|
||||
frequency = new { type = "string", description = "频率,如 Daily/EveryOtherDay/Weekly" },
|
||||
time_of_day = new { type = "array", items = new { type = "string" }, description = "服药时间,如 [\"08:00\",\"20:00\"]" },
|
||||
duration_days = new { type = "integer", description = "服用天数" },
|
||||
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, AppDbContext db, Guid userId)
|
||||
public static async Task<object> Execute(
|
||||
string toolName,
|
||||
JsonElement args,
|
||||
AppDbContext db,
|
||||
Guid userId,
|
||||
IMedicationService? medications = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"manage_medication" => await ExecuteManageMedication(db, userId, args),
|
||||
"check_archive" => await CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
"manage_medication" => medications != null
|
||||
? await ExecuteManageMedication(medications, userId, args, ct)
|
||||
: await ExecuteManageMedication(db, userId, args),
|
||||
_ => 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()! : "query";
|
||||
return action switch
|
||||
{
|
||||
"create" => await CreateMedication(medications, userId, args, ct),
|
||||
"query" => await QueryMedications(medications, userId, 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 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 startDate = ReadStartDate(args);
|
||||
var endDate = durationDays > 0 ? startDate.AddDays(durationDays) : (DateOnly?)null;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> QueryMedications(IMedicationService medications, Guid userId, CancellationToken ct)
|
||||
{
|
||||
var meds = await medications.ListActiveAsync(userId, ct);
|
||||
return new { count = meds.Count, medications = meds.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay }) };
|
||||
}
|
||||
|
||||
private static async Task<object> ConfirmMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct)
|
||||
{
|
||||
var medId = args.TryGetProperty("medication_id", out var mid) ? mid.GetGuid() : Guid.Empty;
|
||||
var success = await medications.ConfirmNowAsync(userId, medId, ct);
|
||||
return success ? new { success = true } : new { success = false, message = "药品不存在或今天已经打卡" };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
@@ -50,38 +126,36 @@ public static class MedicationAgentHandler
|
||||
{
|
||||
var name = args.TryGetProperty("name", out var n) ? n.GetString()! : "";
|
||||
var dosage = args.TryGetProperty("dosage", out var dg) ? dg.GetString() : null;
|
||||
var frequencyStr = args.TryGetProperty("frequency", out var f) ? f.GetString() : "Daily";
|
||||
var frequency = Enum.TryParse<MedicationFrequency>(frequencyStr, out var fr) ? fr : MedicationFrequency.Daily;
|
||||
|
||||
List<TimeOnly> times = [];
|
||||
if (args.TryGetProperty("time_of_day", out var tod) && tod.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var t in tod.EnumerateArray())
|
||||
{
|
||||
if (TimeOnly.TryParse(t.GetString(), out var parsed)) times.Add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
var frequency = ReadFrequency(args);
|
||||
var times = ReadTimes(args);
|
||||
var durationDays = args.TryGetProperty("duration_days", out var dd) ? dd.GetInt32() : 0;
|
||||
var startDateStr = args.TryGetProperty("start_date", out var sd) && sd.GetString() is string sds ? sds : null;
|
||||
var startDate = ReadStartDate(args);
|
||||
|
||||
var med = new Medication
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId,
|
||||
Name = name, Dosage = dosage, Frequency = frequency,
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = name,
|
||||
Dosage = dosage,
|
||||
Frequency = frequency,
|
||||
TimeOfDay = times.Count > 0 ? times : [new TimeOnly(8, 0)],
|
||||
Source = MedicationSource.AiEntry, IsActive = true,
|
||||
StartDate = DateOnly.TryParse(startDateStr, out var parsedDate) ? parsedDate : DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)),
|
||||
EndDate = durationDays > 0 ? DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)).AddDays(durationDays) : null,
|
||||
Source = MedicationSource.AiEntry,
|
||||
IsActive = true,
|
||||
StartDate = startDate,
|
||||
EndDate = durationDays > 0 ? startDate.AddDays(durationDays) : null,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var timeLabels = times.Count > 0 ? string.Join(", ", times.Select(t => t.ToString("HH:mm"))) : "08:00";
|
||||
return new {
|
||||
success = true, medication_id = med.Id,
|
||||
name = med.Name, dosage = med.Dosage,
|
||||
frequency = med.Frequency.ToString(), time = timeLabels,
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
medication_id = med.Id,
|
||||
name = med.Name,
|
||||
dosage = med.Dosage,
|
||||
frequency = med.Frequency.ToString(),
|
||||
time = timeLabels,
|
||||
start_date = med.StartDate?.ToString("yyyy-MM-dd"),
|
||||
duration_days = durationDays,
|
||||
};
|
||||
@@ -97,12 +171,47 @@ public static class MedicationAgentHandler
|
||||
private static async Task<object> ConfirmMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var medId = args.TryGetProperty("medication_id", out var mid) ? mid.GetGuid() : Guid.Empty;
|
||||
var med = await db.Medications.FirstOrDefaultAsync(m => m.Id == medId && m.UserId == userId);
|
||||
if (med == null) return new { success = false, message = "药品不存在" };
|
||||
|
||||
db.MedicationLogs.Add(new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(), MedicationId = medId, UserId = userId,
|
||||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.UtcNow.AddHours(8)), ConfirmedAt = DateTime.UtcNow,
|
||||
Id = Guid.NewGuid(),
|
||||
MedicationId = medId,
|
||||
UserId = userId,
|
||||
Status = MedicationLogStatus.Taken,
|
||||
ScheduledTime = TimeOnly.FromDateTime(DateTime.UtcNow.AddHours(8)),
|
||||
ConfirmedAt = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
private static MedicationFrequency ReadFrequency(JsonElement args)
|
||||
{
|
||||
var frequencyStr = args.TryGetProperty("frequency", out var f) ? f.GetString() : "Daily";
|
||||
return Enum.TryParse<MedicationFrequency>(frequencyStr, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,6 @@ public static class ReportAgentHandler
|
||||
|
||||
public static List<ToolDefinition> Tools => [AnalyzeReportTool, CommonAgentHandler.QueryHealthRecordsTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<object> AnalyzeReport(AppDbContext db, Guid userId, JsonElement args, VisionClient visionClient)
|
||||
{
|
||||
var imageUrl = args.TryGetProperty("image_url", out var u) ? u.GetString()! : "";
|
||||
@@ -28,13 +19,13 @@ public static class ReportAgentHandler
|
||||
return new { success = false, message = "缺少报告图片" };
|
||||
|
||||
var prompt = """
|
||||
你是一个医学报告解读专家。请分析以下检查报告图片,以JSON格式返回:
|
||||
你是患者端医学报告预解读助手。请分析以下检查报告图片,以JSON格式返回:
|
||||
{
|
||||
"reportType": "报告类型(血常规/生化全项/心电图/彩超/出院小结/其他)",
|
||||
"indicators": [
|
||||
{"name":"指标名","value":"数值","unit":"单位","range":"参考范围","status":"normal/high/low"}
|
||||
],
|
||||
"summary": "初步分析摘要",
|
||||
"summary": "面向患者的初步预解读,说明异常指标的可能方向,不作确定诊断,不给出处方、停药、换药或调药建议;必要时建议咨询医生",
|
||||
"needsDoctorReview": true/false
|
||||
}
|
||||
只返回JSON,不要其他内容。
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json;
|
||||
using Health.Application.AI;
|
||||
using Health.Application.Exercises;
|
||||
using Health.Application.HealthArchives;
|
||||
using Health.Application.HealthRecords;
|
||||
using Health.Application.Medications;
|
||||
using Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
public sealed class AiToolExecutionService(
|
||||
AppDbContext db,
|
||||
VisionClient visionClient,
|
||||
IHealthArchiveService healthArchives,
|
||||
IHealthRecordService healthRecords,
|
||||
IExerciseService exercises,
|
||||
IMedicationService medications,
|
||||
IAiWriteConfirmationStore confirmations) : IAiToolExecutionService
|
||||
{
|
||||
private readonly AppDbContext _db = db;
|
||||
private readonly VisionClient _visionClient = visionClient;
|
||||
private readonly IHealthArchiveService _healthArchives = healthArchives;
|
||||
private readonly IHealthRecordService _healthRecords = healthRecords;
|
||||
private readonly IExerciseService _exercises = exercises;
|
||||
private readonly IMedicationService _medications = medications;
|
||||
private readonly IAiWriteConfirmationStore _confirmations = confirmations;
|
||||
|
||||
public async Task<object> ExecuteAsync(string toolName, string arguments, Guid userId, CancellationToken ct)
|
||||
{
|
||||
using var json = JsonDocument.Parse(arguments);
|
||||
var root = json.RootElement;
|
||||
return await (toolName switch
|
||||
{
|
||||
"record_health_data" => HealthDataAgentHandler.Execute(toolName, root, _db, userId, _healthRecords),
|
||||
"query_health_records" => CommonAgentHandler.Execute(toolName, root, userId, _healthRecords, _healthArchives, ct),
|
||||
"check_archive" => CommonAgentHandler.Execute(toolName, root, userId, _healthRecords, _healthArchives, ct),
|
||||
"manage_medication" => MedicationAgentHandler.Execute(toolName, root, _db, userId, _medications, ct),
|
||||
"analyze_report" => ReportAgentHandler.AnalyzeReport(_db, userId, root, _visionClient),
|
||||
"manage_exercise" => ExerciseAgentHandler.Execute(toolName, root, _db, userId, _exercises, ct),
|
||||
"manage_archive" => CommonAgentHandler.ExecuteManageArchive(_healthArchives, userId, root, ct),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" }),
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AiConfirmedWriteResult> ConfirmAsync(Guid commandId, Guid userId, CancellationToken ct)
|
||||
{
|
||||
await using var transaction = await _db.Database.BeginTransactionAsync(ct);
|
||||
var command = await _confirmations.TakeAsync(commandId, userId, ct);
|
||||
if (command == null)
|
||||
return new AiConfirmedWriteResult(40004, null, "确认请求不存在、已执行或已过期");
|
||||
|
||||
try
|
||||
{
|
||||
var result = await ExecuteAsync(command.ToolName, command.Arguments, userId, ct);
|
||||
if (!Succeeded(result, out var error))
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
return new AiConfirmedWriteResult(40001, result, error ?? "写入失败,请检查内容后重试");
|
||||
}
|
||||
await _confirmations.CompleteAsync(command, ct);
|
||||
await transaction.CommitAsync(ct);
|
||||
return new AiConfirmedWriteResult(0, result, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
return new AiConfirmedWriteResult(50001, null, $"写入失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Succeeded(object result, out string? error)
|
||||
{
|
||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(result));
|
||||
var root = json.RootElement;
|
||||
error = root.TryGetProperty("message", out var message) ? message.GetString() : null;
|
||||
return !root.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.False;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Health.Application.AI;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
public sealed class EfAiConversationRepository(AppDbContext db) : IAiConversationRepository
|
||||
{
|
||||
private readonly AppDbContext _db = db;
|
||||
|
||||
public Task<Conversation?> GetOwnedAsync(Guid userId, Guid conversationId, CancellationToken ct) =>
|
||||
_db.Conversations.FirstOrDefaultAsync(c => c.Id == conversationId && c.UserId == userId, ct);
|
||||
|
||||
public Task<Conversation?> GetAsync(Guid conversationId, CancellationToken ct) =>
|
||||
_db.Conversations.FirstOrDefaultAsync(c => c.Id == conversationId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Conversation>> ListAsync(Guid userId, CancellationToken ct) =>
|
||||
await _db.Conversations
|
||||
.Where(c => c.UserId == userId)
|
||||
.OrderByDescending(c => c.UpdatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<ConversationMessage>> GetMessagesAsync(Guid conversationId, CancellationToken ct) =>
|
||||
await _db.ConversationMessages
|
||||
.Where(m => m.ConversationId == conversationId)
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<ConversationMessage>> GetRecentMessagesAsync(Guid conversationId, int limit, CancellationToken ct) =>
|
||||
await _db.ConversationMessages
|
||||
.Where(m => m.ConversationId == conversationId)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task AddConversationAsync(Conversation conversation, CancellationToken ct) =>
|
||||
await _db.Conversations.AddAsync(conversation, ct);
|
||||
|
||||
public async Task AddMessageAsync(ConversationMessage message, CancellationToken ct) =>
|
||||
await _db.ConversationMessages.AddAsync(message, ct);
|
||||
|
||||
public void Delete(Conversation conversation) =>
|
||||
_db.Conversations.Remove(conversation);
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct) =>
|
||||
_db.SaveChangesAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Health.Application.AI;
|
||||
using Health.Infrastructure.Data.Records;
|
||||
|
||||
namespace Health.Infrastructure.AI;
|
||||
|
||||
public sealed class EfAiWriteConfirmationStore(AppDbContext db) : IAiWriteConfirmationStore
|
||||
{
|
||||
private readonly AppDbContext _db = db;
|
||||
|
||||
public async Task<PendingAiWriteCommand> CreateAsync(
|
||||
Guid userId,
|
||||
string toolName,
|
||||
string arguments,
|
||||
TimeSpan lifetime,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (IsInMemory())
|
||||
{
|
||||
var expired = await _db.AiWriteCommands.Where(x => x.Status == "Pending" && x.ExpiresAt <= now).ToListAsync(ct);
|
||||
foreach (var item in expired) { item.Status = "Expired"; item.UpdatedAt = now; }
|
||||
}
|
||||
else
|
||||
{
|
||||
await _db.AiWriteCommands
|
||||
.Where(x => x.Status == "Pending" && x.ExpiresAt <= now)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Expired")
|
||||
.SetProperty(x => x.UpdatedAt, now), ct);
|
||||
}
|
||||
|
||||
var command = new AiWriteCommandRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
ToolName = toolName,
|
||||
Arguments = arguments,
|
||||
Status = "Pending",
|
||||
ExpiresAt = now.Add(lifetime),
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
await _db.AiWriteCommands.AddAsync(command, ct);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return ToCommand(command);
|
||||
}
|
||||
|
||||
public async Task<PendingAiWriteCommand?> TakeAsync(Guid commandId, Guid userId, CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (IsInMemory())
|
||||
{
|
||||
var tracked = await _db.AiWriteCommands.FirstOrDefaultAsync(
|
||||
x => x.Id == commandId && x.UserId == userId && x.Status == "Pending" && x.ExpiresAt > now, ct);
|
||||
if (tracked == null) return null;
|
||||
tracked.Status = "Processing";
|
||||
tracked.UpdatedAt = now;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
var updated = await _db.AiWriteCommands
|
||||
.Where(x => x.Id == commandId && x.UserId == userId && x.Status == "Pending" && x.ExpiresAt > now)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Processing")
|
||||
.SetProperty(x => x.UpdatedAt, now), ct);
|
||||
if (updated == 0) return null;
|
||||
}
|
||||
|
||||
var record = await _db.AiWriteCommands.AsNoTracking().FirstAsync(x => x.Id == commandId, ct);
|
||||
return ToCommand(record);
|
||||
}
|
||||
|
||||
public async Task CompleteAsync(PendingAiWriteCommand command, CancellationToken ct)
|
||||
{
|
||||
if (IsInMemory())
|
||||
{
|
||||
var tracked = await _db.AiWriteCommands.FirstOrDefaultAsync(
|
||||
x => x.Id == command.Id && x.UserId == command.UserId && x.Status == "Processing", ct);
|
||||
if (tracked != null) { tracked.Status = "Completed"; tracked.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); }
|
||||
return;
|
||||
}
|
||||
await _db.AiWriteCommands
|
||||
.Where(x => x.Id == command.Id && x.UserId == command.UserId && x.Status == "Processing")
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Completed")
|
||||
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);
|
||||
}
|
||||
|
||||
private bool IsInMemory() => _db.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory";
|
||||
|
||||
private static PendingAiWriteCommand ToCommand(AiWriteCommandRecord record) => new(
|
||||
record.Id,
|
||||
record.UserId,
|
||||
record.ToolName,
|
||||
record.Arguments,
|
||||
record.ExpiresAt);
|
||||
}
|
||||
@@ -8,18 +8,33 @@ public sealed class PromptManager
|
||||
/// <summary>
|
||||
/// 获取指定 Agent 的 System Prompt
|
||||
/// </summary>
|
||||
public string GetSystemPrompt(AgentType agentType) => agentType switch
|
||||
public string GetSystemPrompt(AgentType agentType)
|
||||
{
|
||||
AgentType.Default => DefaultPrompt,
|
||||
AgentType.Consultation => ConsultationPrompt,
|
||||
AgentType.Health => HealthDataPrompt,
|
||||
AgentType.Diet => DietPrompt,
|
||||
AgentType.Medication => MedicationPrompt,
|
||||
AgentType.Report => ReportPrompt,
|
||||
AgentType.Exercise => ExercisePrompt,
|
||||
AgentType.Unified => UnifiedPrompt,
|
||||
_ => DefaultPrompt
|
||||
};
|
||||
var prompt = agentType switch
|
||||
{
|
||||
AgentType.Default => DefaultPrompt,
|
||||
AgentType.Consultation => ConsultationPrompt,
|
||||
AgentType.Health => HealthDataPrompt,
|
||||
AgentType.Diet => DietPrompt,
|
||||
AgentType.Medication => MedicationPrompt,
|
||||
AgentType.Report => ReportPrompt,
|
||||
AgentType.Exercise => ExercisePrompt,
|
||||
AgentType.Unified => UnifiedPrompt,
|
||||
_ => DefaultPrompt
|
||||
};
|
||||
|
||||
return $"{prompt}\n\n{MedicalBoundaryRules}";
|
||||
}
|
||||
|
||||
private const string MedicalBoundaryRules = """
|
||||
医疗边界(必须遵守):
|
||||
- 你的定位是患者端 AI 健康解释与预问诊助手,不是医生,不能替代医生诊断、处方或治疗决策。
|
||||
- 可以解释健康数据、报告指标和症状可能方向,但不要给出确定诊断,不要说“你就是/一定是/已经确诊”。
|
||||
- 不要要求用户自行新增、停用、更换药物或调整剂量;涉及用药变化时,必须建议咨询医生或药师。
|
||||
- 对胸痛、胸闷憋气、呼吸困难、意识异常、晕厥、说话不清、一侧肢体无力、血氧明显偏低、血压或血糖严重异常等情况,优先建议及时就医或急诊评估。
|
||||
- 回答用语使用“可能、建议、需要结合医生判断”等表达,避免绝对化结论。
|
||||
- 医疗相关分析末尾用一句自然的话提醒:以上为 AI 预分析,不能替代医生诊断和治疗建议。
|
||||
""";
|
||||
|
||||
private const string DefaultPrompt = """
|
||||
你是一位专业的AI健康管家,专注于为心脏术后康复患者提供贴心的健康管理服务。
|
||||
@@ -38,15 +53,15 @@ public sealed class PromptManager
|
||||
""";
|
||||
|
||||
private const string ConsultationPrompt = """
|
||||
你是一个心血管内科医生助手,负责对心脏术后患者进行多轮问诊。
|
||||
你是一个患者端 AI 预问诊助手,负责帮助心脏术后患者梳理症状和就医前信息。
|
||||
|
||||
规则:
|
||||
1. 每次只问一个问题,不要一次问多个
|
||||
2. 给出 2-3 个快捷选项让患者点击
|
||||
3. 问诊步骤:先问感受 → 持续时间 → 伴随症状 → 近期用药 → 给出初步分析
|
||||
4. 遇到以下情况建议立即就医:剧烈胸痛、呼吸困难、心悸
|
||||
5. 遇到以下情况建议转医生:血压持续>160/100、心率>120或<50
|
||||
6. 所有分析末尾标注"以上为AI分析,具体请咨询医生"
|
||||
5. 遇到以下情况建议咨询医生:血压持续>160/100、心率>120或<50
|
||||
6. 只给出初步分析和下一步建议,不作诊断结论
|
||||
7. 问诊结束给出结构化小结
|
||||
""";
|
||||
|
||||
@@ -55,13 +70,13 @@ public sealed class PromptManager
|
||||
|
||||
规则:
|
||||
1. 解析用户消息中的指标和数值(血压/心率/血糖/血氧/体重)
|
||||
2. 指标明确+数值明确→直接调用 record_health_data 录入
|
||||
2. 指标明确+数值明确→调用 record_health_data 生成待确认写入命令,不要直接声称已经录入
|
||||
3. 用户可以一次性说多个指标(如"血压120/80,血糖6.2,血氧97")→多次调用 record_health_data
|
||||
4. 用户可以分时段说(如"早上血压120,下午血压130")→分别录入,recorded_at 参数用不同时间
|
||||
4. 用户可以分时段说(如"早上血压120,下午血压130")→分别生成待确认命令,recorded_at 参数用不同时间
|
||||
5. 数值明确但指标模糊(如只说"120")→追问是"收缩压还是血糖?"
|
||||
6. 时间模糊→取当前时间
|
||||
7. 数值超出正常范围→附带异常提醒
|
||||
8. 每次调用 record_health_data 后,继续调用下一个,全部录入完成后生成确认卡片格式的回复
|
||||
8. 每次调用 record_health_data 后继续处理下一个;工具返回仅表示等待确认,必须提示用户点击确认卡片,不能说“录入成功”
|
||||
|
||||
正常值参考范围:
|
||||
- 收缩压 90-139 mmHg,舒张压 60-89 mmHg
|
||||
@@ -86,24 +101,24 @@ public sealed class PromptManager
|
||||
""";
|
||||
|
||||
private const string MedicationPrompt = """
|
||||
你是一个用药管理专家。
|
||||
你是一个用药记录与提醒助手。
|
||||
|
||||
规则:
|
||||
1. 用户询问当前用药时,必须先调用 manage_medication(action="query") 查询已有用药记录
|
||||
2. 解析用户口中的药品信息(药名/剂量/频次/时间)
|
||||
3. "早饭后"等模糊时间→追问具体几点
|
||||
4. 解析完成后展示确认卡片
|
||||
4. 解析完成后调用 manage_medication(action="create") 生成待确认命令并展示确认卡片;用户点击确认前不得声称已保存
|
||||
5. 处方拍照→提取药品信息→生成用药计划→让用户确认
|
||||
6. 回答用药相关疑问
|
||||
6. 可以解释常见用药注意事项,但不要建议用户自行新增、停用、更换药物或调整剂量
|
||||
""";
|
||||
|
||||
private const string ReportPrompt = """
|
||||
你是一个医学报告解读专家。
|
||||
你是一个患者端医学报告预解读助手。
|
||||
|
||||
规则:
|
||||
1. 收到报告图片后,提取所有指标及其数值
|
||||
2. 标注异常指标(偏高/偏低/正常)
|
||||
3. 给出初步分析
|
||||
3. 给出初步分析和可能方向,不作诊断结论
|
||||
4. 所有内容标注"AI预解读,待医生确认"
|
||||
5. 图像类报告(彩超/CT)注明"需医生人工审阅"
|
||||
""";
|
||||
@@ -120,7 +135,7 @@ public sealed class PromptManager
|
||||
""";
|
||||
|
||||
private const string UnifiedPrompt = """
|
||||
你是一个全能的AI健康管家,为心脏术后康复患者提供全方位服务。
|
||||
你是一个患者端 AI 健康管家,为心脏术后康复患者提供健康解释、记录和预问诊辅助服务。
|
||||
|
||||
核心规则:首先理解用户意图属于哪个领域,然后调用对应的工具。
|
||||
|
||||
@@ -133,20 +148,21 @@ public sealed class PromptManager
|
||||
6. 历史数据查询 → 调用 query_health_records
|
||||
|
||||
运动计划参数格式(manage_exercise):
|
||||
- action="create", week_start_date="今天日期(YYYY-MM-DD)"
|
||||
- items: [{"day_of_week":0-6(周日=0),"exercise_type":"散步","duration_minutes":30,"is_rest_day":false}, ...]
|
||||
- action="create", start_date="开始日期(YYYY-MM-DD,默认今天)"
|
||||
- duration_days=连续天数,exercise_type="散步",duration_minutes=30,reminder_time="19:00"
|
||||
- 时长必须识别中文数字:半小时=30 一小时=60 三十分钟=30 一个半小时=90 一小时二十分钟=80,中文数字必须转为阿拉伯数字
|
||||
- 天数:"一个月"=30天 "一周"=7天 "10天"=10天,必须识别中文,默认7天
|
||||
- 用户说"坚持X天/月/周"则创建对应天数
|
||||
- items 数量必须等于持续天数
|
||||
- 结束日期由 start_date + duration_days - 1 自动计算,不要按星期几生成条目
|
||||
|
||||
用药管理参数格式(manage_medication):
|
||||
- action="create", name="药名", dosage="剂量", frequency="Daily", time_of_day=["08:00","20:00"], duration_days=7
|
||||
|
||||
规则:
|
||||
- 用户可能一句话涉及多个领域,全部处理
|
||||
- 数值明确+指标明确→直接录入,不要追问
|
||||
- 录入完成后生成确认卡片
|
||||
- 数值明确+指标明确→直接生成待确认命令,不要追问
|
||||
- 所有写入工具调用只生成待确认命令;用户点击卡片确认后才真正写入数据库
|
||||
- 工具返回 pendingConfirmation=true 时,不得回复“已录入”或“保存成功”,应提示用户核对并点击确认
|
||||
- 遇到紧急症状(剧烈胸痛、呼吸困难)立即建议就医
|
||||
- 回复语气温暖、专业
|
||||
""";
|
||||
|
||||
Reference in New Issue
Block a user