backend: AI chat agent with Veil PHI safeguard (providers, /chat, import)

Real LLM chat replacing the mock, backend-centric per the plan:

- Multi-provider API-key mode (OpenAI / Anthropic / Gemini via the AI SDK) plus
  local Ollama (OpenAI-compatible endpoint). Provider is derived from the
  picked model id; the matching stored key is used. New user_ai_settings table
  holds per-user config with provider API keys encrypted at rest (AES-256-GCM,
  src/lib/crypto.ts, keyed by AI_CREDENTIALS_KEY).
- POST /api/ai/config (get/put, secrets never returned), POST /api/ai/test
  (Ollama ping / key presence), POST /api/ai/import (approved migration commit,
  re-validated server-side, reuses the audited patient service).
- POST /api/chat: streamText agent with tools (getPatient, getPatientLabs,
  searchPatients, previewImport). Real record data streams to the clinician as
  custom data parts (cards) while the model sees only Veil-redacted results.
- Veil (src/services/ai/veil.ts): de-identifies patient identifiers to tokens
  before external calls, resolves tokens on tool args, and rehydrates the final
  answer. Bypassed for local Ollama. External mode runs non-streamed so the
  rehydrated text is correct. Every call is audited (provider + Veil level).
- Shared role-scoping helpers extracted to src/lib/role-scope.ts (reused by the
  patient routes and chat tools so visibility rules match).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-13 18:35:16 +03:00
parent c6a1b98427
commit 3a7378e00d
21 changed files with 3955 additions and 31 deletions
+33
View File
@@ -0,0 +1,33 @@
// Shared AI-configuration types, mirrored loosely by the frontend Settings →
// AI panel. The chat agent reads these to decide which provider/model to call
// and how strict the Veil de-identification safeguard should be.
// Two inference modes: a user-provided cloud API key, or a local Ollama model.
export type AiMode = "api" | "local";
// The three supported cloud providers for API-key mode.
export type ApiProvider = "openai" | "anthropic" | "gemini";
export type Effort = "low" | "medium" | "high";
// Veil (PHI de-identification) strictness. Only applies on external (API-key)
// calls; local Ollama never leaves the clinic so Veil is bypassed there.
// off — send clinical context as-is (not recommended; logged)
// names — tokenize direct identifiers (name, MRN, provider, DOB)
// full — names + free-text scrubbing of incidental identifiers
export type VeilLevel = "off" | "names" | "full";
// Non-secret AI config returned to the client. API keys are never included;
// `apiKeySet` records which providers have a stored (encrypted) key.
export type AiConfig = {
mode: AiMode;
provider: ApiProvider;
ollamaBaseUrl: string;
ollamaModel: string;
defaultModel: string;
defaultEffort: Effort;
veilLevel: VeilLevel;
apiKeySet: Record<ApiProvider, boolean>;
};
export const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";