feat: add AI knowledge analysis MVP
This commit is contained in:
330
modules/ai/types.ts
Normal file
330
modules/ai/types.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
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 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 value.confidence !== "number" ||
|
||||
!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, value.confidence)),
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user