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
+6
View File
@@ -9,8 +9,10 @@ import { env } from "./env.js";
import { errorHandler, notFound } from "./middleware/error.js";
import { initRealtime } from "./realtime.js";
import { activityRouter } from "./routes/activity.js";
import { aiRouter } from "./routes/ai.js";
import { analyticsRouter } from "./routes/analytics.js";
import { appointmentsRouter } from "./routes/appointments.js";
import { chatRouter } from "./routes/chat.js";
import { conversationsRouter } from "./routes/conversations.js";
import { inventoryRouter } from "./routes/inventory.js";
import { notesRouter } from "./routes/notes.js";
@@ -68,6 +70,8 @@ app.use("/api/analytics", analyticsRouter);
app.use("/api/conversations", conversationsRouter);
app.use("/api/notifications", notificationsRouter);
app.use("/api/settings", settingsRouter);
app.use("/api/ai", aiRouter);
app.use("/api/chat", chatRouter);
app.use(notFound);
app.use(errorHandler);
@@ -91,4 +95,6 @@ server.listen(env.PORT, () => {
console.log(` • messages: /api/conversations (+ Socket.io)`);
console.log(` • notifs: /api/notifications`);
console.log(` • settings: /api/settings`);
console.log(` • ai: /api/ai (config + import)`);
console.log(` • chat: /api/chat (LLM agent)`);
});