feat: add AI knowledge analysis MVP

This commit is contained in:
2026-07-05 00:47:52 -07:00
parent 6aa72d3b43
commit e204974b57
32 changed files with 8705 additions and 4 deletions

View File

@@ -349,3 +349,96 @@ GOOGLE_GENERATIVE_AI_API_KEY=...
# Anthropic
ANTHROPIC_API_KEY=sk-ant-...
```
## Scenario: Elder AI Analysis MVP With LangChain And pgvector
### 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.
- Keep provider calls under `modules/ai/server/*`; feature modules and API routes must not instantiate model clients directly.
### 2. Signatures
- API:
- `GET /api/ai/elders/[id]/analyses`
- `POST /api/ai/elders/[id]/analyses`
- `GET /api/ai/knowledge`
- `POST /api/ai/knowledge`
- `PATCH /api/ai/knowledge/[id]`
- `DELETE /api/ai/knowledge/[id]`
- DB:
- `ai_knowledge_entries`
- `ai_knowledge_chunks` with `vector(1536)`
- `elder_ai_analyses`
- Runtime env:
- `AI_API_KEY` required for generation and embedding.
- `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`.
### 3. Contracts
- All API responses use the project API shape: `{ success: boolean, reason: string, ...payload }`.
- Knowledge entry input fields:
- `scope`: `"platform" | "organization"`
- `title`: non-empty string
- `category`: string
- `tags`: string
- `body`: non-empty string
- `status`: `"enabled" | "disabled"`
- Elder analysis history stores:
- `status`: `"completed" | "failed"`
- `dataScopes`: JSON array of included families
- `resultJson`: only for completed rows
- `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.
### 4. Validation & Error Matrix
- Missing `ai:read` -> `403 权限不足`.
- Missing `elder:read` -> `403 权限不足`.
- Missing active organization for elder analysis -> `400`.
- Target elder outside active organization -> `404`.
- Missing `knowledge:read` -> knowledge scope is omitted from analysis context.
- Missing `knowledge:manage` -> knowledge mutation routes return `403`.
- Platform knowledge mutation without `platform:manage` -> `403`.
- Missing `AI_API_KEY` -> save failed analysis with `missing_config`, return structured failure.
- Provider failure -> save failed analysis with `provider_error` or `timeout`.
- Invalid model output -> save failed analysis with `schema_validation_failed`.
### 5. Good/Base/Bad Cases
- Good: org manager generates elder analysis; context includes only data families allowed by the manager's current permissions; history stores matching `dataScopes`.
- Base: caregiver generates analysis; caregiver gets `ai:read` and `knowledge:read` by default, but cannot mutate knowledge.
- Bad: viewer has `elder:read` but no `ai:read`; the AI row action must not be exposed and API must return `403`.
- Bad: organization user attempts to retrieve another organization's private knowledge; the retrieval query must not include those chunks.
### 6. Tests Required
- Permission seeding assertions for `ai:*` and `knowledge:*` default role grants.
- Knowledge retrieval tests for platform shared, own organization, other organization, and disabled entry filtering.
- Analysis tests for missing config, schema validation failure, failed-history sanitization, and data-scope redaction.
- Route tests for auth/permission failures and standard response shape.
### 7. Wrong vs Correct
#### Wrong
```typescript
// Do not call a model from a feature route and pass raw resident rows directly.
const model = new ChatOpenAI({ apiKey: process.env.AI_API_KEY });
await model.invoke(JSON.stringify(residentRows));
```
#### Correct
```typescript
// Keep model calls in modules/ai/server and build permission-aware context first.
const result = await generateElderAiAnalysis(auth.context, elderId);
if (!result.success) {
return jsonFailure(result.reason, result.status);
}
return jsonSuccess("AI 分析已生成", { analysis: result.data.analysis }, 201);
```