From 6a1add34224d0a449874403d86c1c5452d89dd37 Mon Sep 17 00:00:00 2001 From: TalexDreamSoul Date: Wed, 8 Jul 2026 04:38:22 -0700 Subject: [PATCH] fix: harden AI analysis and seed defaults --- .trellis/spec/backend/ai-sdk-integration.md | 16 +- app/api/ai/ai-routes.test.ts | 6 +- app/api/organizations/route.ts | 2 + compose.yaml | 2 +- .../components/KnowledgeManagementClient.tsx | 14 +- modules/ai/server/analysis.test.ts | 6 +- modules/ai/server/analysis.ts | 2 +- modules/ai/server/config.ts | 6 - modules/ai/server/knowledge.test.ts | 181 +++++--- modules/ai/server/knowledge.ts | 114 ++--- modules/ai/types.test.ts | 9 +- modules/ai/types.ts | 6 +- .../server/default-workspace-data.test.ts | 64 +++ modules/core/server/default-workspace-data.ts | 390 +++++++++++++++++- modules/core/server/schema.ts | 2 - 15 files changed, 653 insertions(+), 167 deletions(-) create mode 100644 modules/core/server/default-workspace-data.test.ts diff --git a/.trellis/spec/backend/ai-sdk-integration.md b/.trellis/spec/backend/ai-sdk-integration.md index 08977b4..0177cfe 100644 --- a/.trellis/spec/backend/ai-sdk-integration.md +++ b/.trellis/spec/backend/ai-sdk-integration.md @@ -350,13 +350,14 @@ GOOGLE_GENERATIVE_AI_API_KEY=... ANTHROPIC_API_KEY=sk-ant-... ``` -## Scenario: Elder AI Analysis MVP With LangChain And PostgreSQL JSONB Embeddings +## Scenario: Elder AI Analysis MVP With LangChain And Keyword Knowledge Retrieval ### 1. Scope / Trigger - Trigger: AI features that inspect resident, care, health, family, admission, alert, incident, or knowledge data. -- Current project contract: the elder analysis MVP uses LangChain on the server with an OpenAI-compatible provider, not the Vercel AI SDK runtime path. +- Current project contract: the elder analysis MVP uses LangChain on the server with an OpenAI-compatible chat provider, not the Vercel AI SDK runtime path. - Keep provider calls under `modules/ai/server/*`; feature modules and API routes must not instantiate model clients directly. +- Knowledge retrieval is local keyword scoring over persisted chunks; it must not require an embedding provider or `pgvector`. ### 2. Signatures @@ -369,13 +370,13 @@ ANTHROPIC_API_KEY=sk-ant-... - `DELETE /api/ai/knowledge/[id]` - DB: - `ai_knowledge_entries` - - `ai_knowledge_chunks` with `jsonb` `embedding` arrays + - `ai_knowledge_chunks` with text `content`; the legacy JSONB `embedding` column remains defaulted for compatibility and is not read or written by retrieval. - `elder_ai_analyses` - Runtime env: - - `AI_API_KEY` required for generation and embedding. + - `AI_API_KEY` required for chat generation only. - `AI_BASE_URL` optional OpenAI-compatible endpoint. - `AI_CHAT_MODEL` optional, defaults in `modules/ai/server/config.ts`. - - `AI_EMBEDDING_MODEL` optional, defaults in `modules/ai/server/config.ts`. + - Do not introduce `AI_EMBEDDING_MODEL`; knowledge retrieval must work without embedding config. ### 3. Contracts @@ -394,7 +395,8 @@ ANTHROPIC_API_KEY=sk-ant-... - `errorCategory` / `errorReason`: sanitized only for failed rows - Failed rows must not store prompts, full resident context snapshots, API keys, or raw provider errors. - Knowledge retrieval may return enabled platform knowledge and enabled active-organization knowledge only. -- Knowledge retrieval first filters enabled platform / active-organization chunks in SQL, then computes cosine similarity in `modules/ai/server/knowledge.ts`; this keeps the MVP deployable on plain PostgreSQL without the `vector` extension. +- Knowledge retrieval first filters enabled platform / active-organization chunks in SQL, then scores chunks with local lexical overlap in `modules/ai/server/knowledge.ts`; this keeps the MVP deployable on plain PostgreSQL without the `vector` extension or embedding API calls. +- Completed elder analysis `resultJson.modelSummary` must include `{ provider: "openai-compatible", chatModel, knowledgeRetrieval: "keyword" }`. - Completed elder analysis `resultJson.citations` must be derived from citation IDs actually referenced by `keyFindings[].citationIds` and `recommendations[].citationIds`; do not append unused available citations. ### 4. Validation & Error Matrix @@ -421,7 +423,7 @@ ANTHROPIC_API_KEY=sk-ant-... ### 6. Tests Required - Permission seeding assertions for `ai:*` and `knowledge:*` default role grants. -- Knowledge retrieval tests for platform shared, own organization, other organization, disabled entry filtering, and cosine-score ordering from JSONB embeddings. +- Knowledge retrieval tests for platform shared, own organization, other organization, disabled entry filtering, keyword-score ordering, nonmatching chunk exclusion, and no embedding-provider calls. - Analysis tests for missing config, schema validation failure, failed-history sanitization, and data-scope redaction. - Analysis tests for unknown citation rejection and referenced-only citation persistence. - Route tests for auth/permission failures and standard response shape. diff --git a/app/api/ai/ai-routes.test.ts b/app/api/ai/ai-routes.test.ts index 2eee8eb..1fb0f43 100644 --- a/app/api/ai/ai-routes.test.ts +++ b/app/api/ai/ai-routes.test.ts @@ -140,7 +140,7 @@ const analysis: ElderAiAnalysisHistoryItem = { modelSummary: { provider: "openai-compatible", chatModel: "gpt-test", - embeddingModel: "embedding-test", + knowledgeRetrieval: "keyword", }, }, }; @@ -293,14 +293,14 @@ describe("AI API routes", () => { }); it("propagates knowledge service failures with the service status", async () => { - vi.mocked(createKnowledgeEntry).mockResolvedValue({ success: false, reason: "知识库向量生成失败", status: 500 }); + vi.mocked(createKnowledgeEntry).mockResolvedValue({ success: false, reason: "知识库创建失败", status: 500 }); const { POST } = await import("./knowledge/route"); const response = await POST(jsonRequest({ ...knowledgeInput })); const body = await readJson(response); expect(response.status).toBe(500); - expect(body).toEqual({ success: false, reason: "知识库向量生成失败" }); + expect(body).toEqual({ success: false, reason: "知识库创建失败" }); expect(recordAuditLog).not.toHaveBeenCalled(); }); diff --git a/app/api/organizations/route.ts b/app/api/organizations/route.ts index 9b831d8..b306fe4 100644 --- a/app/api/organizations/route.ts +++ b/app/api/organizations/route.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; +import { seedDefaultWorkspaceData } from "@/modules/core/server/default-workspace-data"; import { getDatabase } from "@/modules/core/server/db"; import { seedOrganizationRoles } from "@/modules/core/server/permissions"; import { organizations } from "@/modules/core/server/schema"; @@ -95,6 +96,7 @@ export async function POST(request: Request): Promise { } await seedOrganizationRoles(organization.id); + await seedDefaultWorkspaceData(organization.id); await recordAuditLog({ actor: auth.context.account, organizationId: organization.id, diff --git a/compose.yaml b/compose.yaml index 101cc03..7cd047e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,6 @@ services: postgres: - image: pgvector/pgvector:pg17 + image: postgres:17-alpine container_name: teatea-postgres restart: unless-stopped environment: diff --git a/modules/ai/components/KnowledgeManagementClient.tsx b/modules/ai/components/KnowledgeManagementClient.tsx index 1d0e096..62f7270 100644 --- a/modules/ai/components/KnowledgeManagementClient.tsx +++ b/modules/ai/components/KnowledgeManagementClient.tsx @@ -66,12 +66,6 @@ function matchesEntry(entry: KnowledgeEntry, query: string): boolean { return [entry.title, entry.category, entry.tags, entry.body].join(" ").toLowerCase().includes(normalized); } -function formatKnowledgeFailureReason(reason: string): string { - if (reason.includes("AI_API_KEY") || reason.includes("向量生成失败")) { - return `保存知识需要可用的 AI 向量配置:${reason}`; - } - return reason; -} export function KnowledgeManagementClient({ initialEntries, permissions }: KnowledgeManagementClientProps): React.ReactElement { const [entries, setEntries] = useState(initialEntries); @@ -142,7 +136,7 @@ export function KnowledgeManagementClient({ initialEntries, permissions }: Knowl const result = (await response.json()) as ApiResult<{ entry: KnowledgeEntry }>; setIsPending(false); if (!result.success) { - setMessage(formatKnowledgeFailureReason(result.reason)); + setMessage(result.reason); return; } @@ -180,7 +174,7 @@ export function KnowledgeManagementClient({ initialEntries, permissions }: Knowl }); const result = (await response.json()) as ApiResult<{ entry: KnowledgeEntry }>; if (!result.success) { - setMessage(formatKnowledgeFailureReason(result.reason)); + setMessage(result.reason); return; } await refreshEntries(); @@ -192,7 +186,7 @@ export function KnowledgeManagementClient({ initialEntries, permissions }: Knowl

知识库

-

维护平台共享和机构私有知识,保存后会重建检索向量。

+

维护平台共享和机构私有知识,保存后会重建本地关键词检索分块。