mirror of
https://github.com/temetro/temetro.git
synced 2026-08-24 08:46:27 +00:00
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:
@@ -0,0 +1,40 @@
|
||||
import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
import type {
|
||||
AiMode,
|
||||
ApiProvider,
|
||||
Effort,
|
||||
VeilLevel,
|
||||
} from "../../types/ai.js";
|
||||
import { user } from "./auth.js";
|
||||
|
||||
// Per-user AI configuration. One row per user (keyed by the Better Auth user
|
||||
// id). Non-secret fields are plain columns; provider API keys are stored
|
||||
// encrypted (src/lib/crypto.ts) in `apiKeysCipher` as a map keyed by provider,
|
||||
// so a user can save more than one provider's key and switch without re-entry.
|
||||
// The encrypted blob is never returned to the client.
|
||||
export const userAiSettings = pgTable("user_ai_settings", {
|
||||
userId: text("user_id")
|
||||
.primaryKey()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
mode: text("mode").$type<AiMode>().notNull().default("local"),
|
||||
provider: text("provider").$type<ApiProvider>().notNull().default("anthropic"),
|
||||
ollamaBaseUrl: text("ollama_base_url")
|
||||
.notNull()
|
||||
.default("http://localhost:11434"),
|
||||
// Local model tag served by Ollama (e.g. "llama3.1"); used in local mode.
|
||||
ollamaModel: text("ollama_model").notNull().default("llama3.1"),
|
||||
defaultModel: text("default_model").notNull().default("claude-sonnet-4-6"),
|
||||
defaultEffort: text("default_effort").$type<Effort>().notNull().default("medium"),
|
||||
veilLevel: text("veil_level").$type<VeilLevel>().notNull().default("full"),
|
||||
// Encrypted per-provider keys: { openai?, anthropic?, gemini? }, each value
|
||||
// an opaque ciphertext string from encryptSecret().
|
||||
apiKeysCipher: jsonb("api_keys_cipher")
|
||||
.$type<Partial<Record<ApiProvider, string>>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
@@ -9,3 +9,4 @@ export * from "./activity.js";
|
||||
export * from "./messaging.js";
|
||||
export * from "./notifications.js";
|
||||
export * from "./settings.js";
|
||||
export * from "./ai.js";
|
||||
|
||||
Reference in New Issue
Block a user