Files
teatea-pension/modules/ai/types.ts

333 lines
9.8 KiB
TypeScript

import type { Permission } from "@/modules/core/types";
export const AI_KNOWLEDGE_SCOPES = ["platform", "organization"] as const;
export type AiKnowledgeScope = (typeof AI_KNOWLEDGE_SCOPES)[number];
export const AI_KNOWLEDGE_STATUSES = ["enabled", "disabled"] as const;
export type AiKnowledgeStatus = (typeof AI_KNOWLEDGE_STATUSES)[number];
export const ELDER_AI_ANALYSIS_STATUSES = ["completed", "failed"] as const;
export type ElderAiAnalysisStatus = (typeof ELDER_AI_ANALYSIS_STATUSES)[number];
export const ELDER_AI_DATA_SCOPES = [
"elder",
"health",
"care",
"family",
"admission",
"facility",
"alert",
"incident",
"knowledge",
] as const;
export type ElderAiDataScope = (typeof ELDER_AI_DATA_SCOPES)[number];
export const ELDER_AI_RISK_LEVELS = ["low", "medium", "high", "critical", "unknown"] as const;
export type ElderAiRiskLevel = (typeof ELDER_AI_RISK_LEVELS)[number];
export const ELDER_AI_SEVERITIES = ["info", "warning", "critical"] as const;
export type ElderAiSeverity = (typeof ELDER_AI_SEVERITIES)[number];
export const ELDER_AI_RECOMMENDATION_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
export type ElderAiRecommendationPriority = (typeof ELDER_AI_RECOMMENDATION_PRIORITIES)[number];
export const AI_ERROR_CATEGORIES = [
"missing_config",
"provider_error",
"timeout",
"schema_validation_failed",
"retrieval_failed",
"unknown",
] as const;
export type AiErrorCategory = (typeof AI_ERROR_CATEGORIES)[number];
export type AiCitation = {
id: string;
sourceType: "resident_context" | "knowledge";
sourceId: string;
title: string;
excerpt: string;
};
export type ElderAiFinding = {
category: string;
severity: ElderAiSeverity;
evidence: string;
citationIds: string[];
};
export type ElderAiRecommendation = {
title: string;
priority: ElderAiRecommendationPriority;
rationale: string;
suggestedNextStep: string;
citationIds: string[];
};
export type ElderAiModelSummary = {
provider: "openai-compatible";
chatModel: string;
embeddingModel: string;
};
export type ElderAiAnalysisOutput = {
overallRiskLevel: ElderAiRiskLevel;
summary: string;
keyFindings: ElderAiFinding[];
recommendations: ElderAiRecommendation[];
dataGaps: string[];
citations: AiCitation[];
confidence: number;
modelSummary: ElderAiModelSummary;
};
export type ElderAiAnalysisHistoryItem = {
id: string;
elderId: string;
status: ElderAiAnalysisStatus;
dataScopes: ElderAiDataScope[];
createdAt: string;
restricted: boolean;
result?: ElderAiAnalysisOutput;
errorCategory?: AiErrorCategory;
errorReason?: string;
};
export type KnowledgeEntryInput = {
scope: AiKnowledgeScope;
organizationId?: string;
title: string;
category: string;
tags: string;
body: string;
status: AiKnowledgeStatus;
};
export type KnowledgeEntry = KnowledgeEntryInput & {
id: string;
createdAt: string;
updatedAt: string;
};
export type KnowledgeRetrievalResult = {
entryId: string;
chunkId: string;
title: string;
category: string;
content: string;
score: number;
};
const DATA_SCOPE_PERMISSION_MAP: Record<Exclude<ElderAiDataScope, "elder" | "knowledge">, Permission> = {
health: "health:read",
care: "care:read",
family: "family:read",
admission: "admission:read",
facility: "facility:read",
alert: "alert:read",
incident: "incident:read",
};
export function isElderAiDataScope(value: string): value is ElderAiDataScope {
return (ELDER_AI_DATA_SCOPES as readonly string[]).includes(value);
}
export function getPermissionForDataScope(scope: ElderAiDataScope): Permission {
if (scope === "elder") {
return "elder:read";
}
if (scope === "knowledge") {
return "knowledge:read";
}
return DATA_SCOPE_PERMISSION_MAP[scope];
}
export function canViewAnalysisScopes(scopes: readonly ElderAiDataScope[], permissions: readonly Permission[]): boolean {
return scopes.every((scope) => permissions.includes(getPermissionForDataScope(scope)));
}
export function parseDataScopes(value: unknown): ElderAiDataScope[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is ElderAiDataScope => typeof item === "string" && isElderAiDataScope(item));
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function readStringArray(value: unknown): string[] | null {
if (!Array.isArray(value)) {
return null;
}
const items = value.filter((item): item is string => typeof item === "string");
return items.length === value.length ? items : null;
}
function validateCitation(value: unknown): AiCitation | null {
if (!isRecord(value)) {
return null;
}
const sourceType = value.sourceType;
if (sourceType !== "resident_context" && sourceType !== "knowledge") {
return null;
}
if (
typeof value.id !== "string" ||
typeof value.sourceId !== "string" ||
typeof value.title !== "string" ||
typeof value.excerpt !== "string"
) {
return null;
}
return {
id: value.id,
sourceType,
sourceId: value.sourceId,
title: value.title,
excerpt: value.excerpt,
};
}
function validateFinding(value: unknown): ElderAiFinding | null {
if (!isRecord(value)) {
return null;
}
const citationIds = readStringArray(value.citationIds);
if (
typeof value.category !== "string" ||
typeof value.evidence !== "string" ||
!ELDER_AI_SEVERITIES.includes(value.severity as ElderAiSeverity) ||
!citationIds
) {
return null;
}
return {
category: value.category,
severity: value.severity as ElderAiSeverity,
evidence: value.evidence,
citationIds,
};
}
function validateRecommendation(value: unknown): ElderAiRecommendation | null {
if (!isRecord(value)) {
return null;
}
const citationIds = readStringArray(value.citationIds);
if (
typeof value.title !== "string" ||
typeof value.rationale !== "string" ||
typeof value.suggestedNextStep !== "string" ||
!ELDER_AI_RECOMMENDATION_PRIORITIES.includes(value.priority as ElderAiRecommendationPriority) ||
!citationIds
) {
return null;
}
return {
title: value.title,
priority: value.priority as ElderAiRecommendationPriority,
rationale: value.rationale,
suggestedNextStep: value.suggestedNextStep,
citationIds,
};
}
function validateModelSummary(value: unknown): ElderAiModelSummary | null {
if (!isRecord(value)) {
return null;
}
if (
value.provider !== "openai-compatible" ||
typeof value.chatModel !== "string" ||
typeof value.embeddingModel !== "string"
) {
return null;
}
return {
provider: "openai-compatible",
chatModel: value.chatModel,
embeddingModel: value.embeddingModel,
};
}
export function validateElderAiAnalysisOutput(value: unknown): ElderAiAnalysisOutput | null {
if (!isRecord(value)) {
return null;
}
const confidenceValue = typeof value.confidence === "string" ? Number(value.confidence) : value.confidence;
const keyFindings = Array.isArray(value.keyFindings) ? value.keyFindings.map(validateFinding) : null;
const recommendations = Array.isArray(value.recommendations) ? value.recommendations.map(validateRecommendation) : null;
const citations = Array.isArray(value.citations) ? value.citations.map(validateCitation) : null;
const dataGaps = readStringArray(value.dataGaps);
const modelSummary = validateModelSummary(value.modelSummary);
if (
!ELDER_AI_RISK_LEVELS.includes(value.overallRiskLevel as ElderAiRiskLevel) ||
typeof value.summary !== "string" ||
typeof confidenceValue !== "number" ||
!Number.isFinite(confidenceValue) ||
!keyFindings ||
keyFindings.includes(null) ||
!recommendations ||
recommendations.includes(null) ||
!dataGaps ||
!citations ||
citations.includes(null) ||
!modelSummary
) {
return null;
}
return {
overallRiskLevel: value.overallRiskLevel as ElderAiRiskLevel,
summary: value.summary,
keyFindings: keyFindings.filter((item): item is ElderAiFinding => Boolean(item)),
recommendations: recommendations.filter((item): item is ElderAiRecommendation => Boolean(item)),
dataGaps,
citations: citations.filter((item): item is AiCitation => Boolean(item)),
confidence: Math.max(0, Math.min(1, confidenceValue)),
modelSummary,
};
}
export function validateKnowledgeEntryInput(value: unknown): { success: true; input: KnowledgeEntryInput } | { success: false; reason: string } {
if (!value || typeof value !== "object") {
return { success: false, reason: "请求数据格式无效" };
}
const record = value as Record<string, unknown>;
const scope = typeof record.scope === "string" ? record.scope : "";
const title = typeof record.title === "string" ? record.title.trim() : "";
const category = typeof record.category === "string" ? record.category.trim() : "";
const tags = typeof record.tags === "string" ? record.tags.trim() : "";
const body = typeof record.body === "string" ? record.body.trim() : "";
const status = typeof record.status === "string" ? record.status : "enabled";
const organizationId = typeof record.organizationId === "string" && record.organizationId.trim() ? record.organizationId.trim() : undefined;
if (!AI_KNOWLEDGE_SCOPES.includes(scope as AiKnowledgeScope)) {
return { success: false, reason: "知识库范围无效" };
}
if (!AI_KNOWLEDGE_STATUSES.includes(status as AiKnowledgeStatus)) {
return { success: false, reason: "知识库状态无效" };
}
if (!title) {
return { success: false, reason: "知识标题不能为空" };
}
if (!body) {
return { success: false, reason: "知识内容不能为空" };
}
return {
success: true,
input: {
scope: scope as AiKnowledgeScope,
organizationId,
title,
category,
tags,
body,
status: status as AiKnowledgeStatus,
},
};
}