@firstlovecenter/ai-chat 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/drizzle/index.cjs +24 -0
- package/dist/drizzle/index.cjs.map +1 -1
- package/dist/drizzle/index.d.cts +35 -1
- package/dist/drizzle/index.d.ts +35 -1
- package/dist/drizzle/index.js +25 -1
- package/dist/drizzle/index.js.map +1 -1
- package/dist/prisma/index.cjs +7 -0
- package/dist/prisma/index.cjs.map +1 -1
- package/dist/prisma/index.d.cts +7 -1
- package/dist/prisma/index.d.ts +7 -1
- package/dist/prisma/index.js +7 -0
- package/dist/prisma/index.js.map +1 -1
- package/dist/server/index.cjs +47 -12
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +12 -3
- package/dist/server/index.d.ts +12 -3
- package/dist/server/index.js +47 -12
- package/dist/server/index.js.map +1 -1
- package/dist/{types-CDKxdzQc.d.cts → types-CQntnyDJ.d.cts} +15 -2
- package/dist/{types-CDKxdzQc.d.ts → types-CQntnyDJ.d.ts} +15 -2
- package/dist/ui/index.cjs +85 -9
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +85 -9
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
- package/prisma/chat-models.prisma +7 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/prisma/adapter.ts","../../src/adapters/prisma/schema.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Prisma `PersistencePort` adapter.\n *\n * Hosts call `createPrismaPersistence(prisma)` with their own configured\n * `PrismaClient` and pass the result to `configureAiChat({ persistence, ... })`.\n *\n * The concrete `PrismaClient` is an OPTIONAL peer dep, so this module never\n * imports a runtime value from `@prisma/client`. Instead it accepts a\n * structural duck-typed shape covering only the delegate methods the port\n * uses. Any object that satisfies `PrismaLike` works — including a real\n * `PrismaClient`, a Prisma extension proxy, or a hand-rolled fake in tests.\n *\n * BigInt note: Prisma returns row IDs as JavaScript `BigInt`. Domain types\n * use plain `number` (sessions/messages are nowhere near 2^53). The adapter\n * widens via `Number(row.id)` on every read; writes accept `number` and\n * Prisma narrows internally.\n */\nimport type {\n AiSettings,\n AiSettingsPatch,\n AppendMessageInput,\n ChatMessage,\n ChatMessageRole,\n ChatSession,\n CreateSessionInput,\n ListSessionsOpts,\n PersistencePort\n} from '../../server/ports/types';\n\n// ---------------------------------------------------------------------------\n// Structural Prisma shape — covers exactly what this adapter touches.\n// Avoids a value-level dependency on `@prisma/client` (optional peer dep).\n// ---------------------------------------------------------------------------\n\ntype ChatSessionRow = {\n id: bigint | number;\n userId: bigint | number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\ntype ChatMessageRow = {\n id: bigint | number;\n sessionId: bigint | number;\n role: string;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\ntype AiSettingsRow = {\n id: number;\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n updatedAt: Date | null;\n updatedByUserId: bigint | number | null;\n};\n\ntype ChatSessionDelegate = {\n create(args: {\n data: { userId: number; title: string };\n }): Promise<ChatSessionRow>;\n findFirst(args: {\n where: { id: number; userId: number };\n }): Promise<ChatSessionRow | null>;\n findMany(args: {\n where: { userId: number };\n orderBy: { updatedAt: 'desc' };\n take?: number;\n }): Promise<ChatSessionRow[]>;\n update(args: {\n where: { id: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<ChatSessionRow>;\n updateMany(args: {\n where: { id: number; userId: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<{ count: number }>;\n deleteMany(args: {\n where: { id: number; userId: number };\n }): Promise<{ count: number }>;\n};\n\ntype ChatMessageDelegate = {\n create(args: {\n data: {\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n };\n }): Promise<ChatMessageRow>;\n findMany(args: {\n where: { sessionId: number };\n orderBy: { createdAt: 'asc' };\n }): Promise<ChatMessageRow[]>;\n};\n\ntype AiSettingsDelegate = {\n findUnique(args: { where: { id: number } }): Promise<AiSettingsRow | null>;\n upsert(args: {\n where: { id: number };\n create: {\n id: number;\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n updatedByUserId: number;\n };\n update: {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n updatedByUserId: number;\n };\n }): Promise<AiSettingsRow>;\n};\n\nexport type PrismaLike = {\n chatSession: ChatSessionDelegate;\n chatMessage: ChatMessageDelegate;\n aiSettings: AiSettingsDelegate;\n};\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nconst toSession = (row: ChatSessionRow): ChatSession => ({\n id: Number(row.id),\n userId: Number(row.userId),\n title: row.title,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt\n});\n\nconst toMessage = (row: ChatMessageRow): ChatMessage => ({\n id: Number(row.id),\n sessionId: Number(row.sessionId),\n // Stored as VARCHAR(16) but constrained to ChatMessageRole at write time.\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: row.createdAt\n});\n\nconst toAiSettings = (row: AiSettingsRow): AiSettings => ({\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n updatedAt: row.updatedAt,\n updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)\n});\n\nconst DEFAULT_AI_SETTINGS: AiSettings = {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n updatedAt: null,\n updatedByUserId: null\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createPrismaPersistence(prisma: PrismaLike): PersistencePort {\n return {\n // Sessions ---------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n const row = await prisma.chatSession.create({\n data: { userId: input.userId, title: input.title }\n });\n return toSession(row);\n },\n\n async getSession(id: number, userId: number): Promise<ChatSession | null> {\n const row = await prisma.chatSession.findFirst({ where: { id, userId } });\n return row ? toSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const rows = await prisma.chatSession.findMany({\n where: { userId },\n orderBy: { updatedAt: 'desc' },\n take: opts?.limit\n });\n return rows.map(toSession);\n },\n\n async updateSession(\n id: number,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n // updateMany so the userId scope filters; no-ops cleanly when the\n // session doesn't exist or belongs to another user.\n await prisma.chatSession.updateMany({\n where: { id, userId },\n data: { ...patch, updatedAt: new Date() }\n });\n },\n\n async deleteSession(id: number, userId: number): Promise<void> {\n await prisma.chatSession.deleteMany({ where: { id, userId } });\n },\n\n // Messages ---------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const row = await prisma.chatMessage.create({\n data: {\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n }\n });\n return toMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: number,\n userId: number\n ): Promise<ChatMessage[]> {\n // Re-assert ownership before returning rows. A caller passing a\n // sessionId they don't own should get an empty list, not someone\n // else's transcript.\n const session = await prisma.chatSession.findFirst({\n where: { id: sessionId, userId }\n });\n if (!session) return [];\n const rows = await prisma.chatMessage.findMany({\n where: { sessionId },\n orderBy: { createdAt: 'asc' }\n });\n return rows.map(toMessage);\n },\n\n // AI settings (singleton row at id=1) -----------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const row = await prisma.aiSettings.findUnique({ where: { id: 1 } });\n return row ? toAiSettings(row) : { ...DEFAULT_AI_SETTINGS };\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n const row = await prisma.aiSettings.upsert({\n where: { id: 1 },\n create: { id: 1, ...patch, updatedByUserId: byUserId },\n update: { ...patch, updatedByUserId: byUserId }\n });\n return toAiSettings(row);\n }\n };\n}\n","/**\n * The Prisma schema fragment hosts paste into their own `schema.prisma`.\n *\n * Kept in lock-step with `prisma/chat-models.prisma` at the package root.\n * The fragment is inlined here (not read from disk) so consumer bundlers\n * never need to resolve a runtime fs path.\n *\n * Codegen tooling can import this constant and write it out, splice it into\n * a host's `schema.prisma`, etc.\n */\nexport const prismaSchemaFragment: string = `// =============================================================================\n// @firstlovecenter/ai-chat — Prisma schema fragment\n// =============================================================================\n//\n// HOSTS: copy the three models below into your own \\`schema.prisma\\`, then run\n//\n// pnpm prisma generate\n// pnpm prisma migrate dev --name flc_ai_chat\n//\n// Column names, SQL types, and indexes match the Drizzle adapter shipped at\n// \\`@firstlovecenter/ai-chat/server/drizzle\\` byte-for-byte, so the underlying MySQL schema\n// is identical regardless of which ORM your host picks. A host could swap\n// from one adapter to the other without touching data.\n// =============================================================================\n\nmodel ChatSession {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n userId BigInt @map(\"user_id\") @db.UnsignedBigInt\n title String @db.VarChar(200)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n\n @@index([userId, updatedAt], map: \"idx_chat_session_user_updated\")\n @@map(\"chat_sessions\")\n}\n\nmodel ChatMessage {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n sessionId BigInt @map(\"session_id\") @db.UnsignedBigInt\n role String @db.VarChar(16)\n question String? @db.Text\n blocks Json?\n prose Json?\n errorJson Json? @map(\"error_json\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n\n @@index([sessionId, createdAt], map: \"idx_chat_msg_session_created\")\n @@map(\"chat_messages\")\n}\n\nmodel AiSettings {\n id Int @id @default(1) @db.UnsignedTinyInt\n toolProvider String @default(\"claude\") @map(\"tool_provider\") @db.VarChar(32)\n gcpLocation String @default(\"us-east5\") @map(\"gcp_location\") @db.VarChar(32)\n chatInterface String @default(\"custom\") @map(\"chat_interface\") @db.VarChar(32)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n updatedByUserId BigInt? @map(\"updated_by_user_id\") @db.UnsignedBigInt\n\n @@map(\"ai_settings\")\n}\n`;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/ports/types.ts","../../src/adapters/prisma/adapter.ts","../../src/adapters/prisma/schema.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\nexport type ChatSession = {\n id: number;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: number;\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: number;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: number, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Prisma `PersistencePort` adapter.\n *\n * Hosts call `createPrismaPersistence(prisma)` with their own configured\n * `PrismaClient` and pass the result to `configureAiChat({ persistence, ... })`.\n *\n * The concrete `PrismaClient` is an OPTIONAL peer dep, so this module never\n * imports a runtime value from `@prisma/client`. Instead it accepts a\n * structural duck-typed shape covering only the delegate methods the port\n * uses. Any object that satisfies `PrismaLike` works — including a real\n * `PrismaClient`, a Prisma extension proxy, or a hand-rolled fake in tests.\n *\n * BigInt note: Prisma returns row IDs as JavaScript `BigInt`. Domain types\n * use plain `number` (sessions/messages are nowhere near 2^53). The adapter\n * widens via `Number(row.id)` on every read; writes accept `number` and\n * Prisma narrows internally.\n */\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\n\n// ---------------------------------------------------------------------------\n// Structural Prisma shape — covers exactly what this adapter touches.\n// Avoids a value-level dependency on `@prisma/client` (optional peer dep).\n// ---------------------------------------------------------------------------\n\ntype ChatSessionRow = {\n id: bigint | number;\n userId: bigint | number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\ntype ChatMessageRow = {\n id: bigint | number;\n sessionId: bigint | number;\n role: string;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\ntype AiSettingsRow = {\n id: number;\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n updatedAt: Date | null;\n updatedByUserId: bigint | number | null;\n};\n\ntype ChatSessionDelegate = {\n create(args: {\n data: { userId: number; title: string };\n }): Promise<ChatSessionRow>;\n findFirst(args: {\n where: { id: number; userId: number };\n }): Promise<ChatSessionRow | null>;\n findMany(args: {\n where: { userId: number };\n orderBy: { updatedAt: 'desc' };\n take?: number;\n }): Promise<ChatSessionRow[]>;\n update(args: {\n where: { id: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<ChatSessionRow>;\n updateMany(args: {\n where: { id: number; userId: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<{ count: number }>;\n deleteMany(args: {\n where: { id: number; userId: number };\n }): Promise<{ count: number }>;\n};\n\ntype ChatMessageDelegate = {\n create(args: {\n data: {\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n };\n }): Promise<ChatMessageRow>;\n findMany(args: {\n where: { sessionId: number };\n orderBy: { createdAt: 'asc' };\n }): Promise<ChatMessageRow[]>;\n};\n\ntype AiSettingsDelegate = {\n findUnique(args: { where: { id: number } }): Promise<AiSettingsRow | null>;\n upsert(args: {\n where: { id: number };\n create: {\n id: number;\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n rolePrompt?: string | null;\n updatedByUserId: number;\n };\n update: {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n rolePrompt?: string | null;\n updatedByUserId: number;\n };\n }): Promise<AiSettingsRow>;\n};\n\nexport type PrismaLike = {\n chatSession: ChatSessionDelegate;\n chatMessage: ChatMessageDelegate;\n aiSettings: AiSettingsDelegate;\n};\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nconst toSession = (row: ChatSessionRow): ChatSession => ({\n id: Number(row.id),\n userId: Number(row.userId),\n title: row.title,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt\n});\n\nconst toMessage = (row: ChatMessageRow): ChatMessage => ({\n id: Number(row.id),\n sessionId: Number(row.sessionId),\n // Stored as VARCHAR(16) but constrained to ChatMessageRole at write time.\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: row.createdAt\n});\n\nconst toAiSettings = (row: AiSettingsRow): AiSettings => ({\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n updatedAt: row.updatedAt,\n updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)\n});\n\nconst DEFAULT_AI_SETTINGS: AiSettings = {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n updatedAt: null,\n updatedByUserId: null\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createPrismaPersistence(prisma: PrismaLike): PersistencePort {\n return {\n // Sessions ---------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n const row = await prisma.chatSession.create({\n data: { userId: input.userId, title: input.title }\n });\n return toSession(row);\n },\n\n async getSession(id: number, userId: number): Promise<ChatSession | null> {\n const row = await prisma.chatSession.findFirst({ where: { id, userId } });\n return row ? toSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const rows = await prisma.chatSession.findMany({\n where: { userId },\n orderBy: { updatedAt: 'desc' },\n take: opts?.limit\n });\n return rows.map(toSession);\n },\n\n async updateSession(\n id: number,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n // updateMany so the userId scope filters; no-ops cleanly when the\n // session doesn't exist or belongs to another user.\n await prisma.chatSession.updateMany({\n where: { id, userId },\n data: { ...patch, updatedAt: new Date() }\n });\n },\n\n async deleteSession(id: number, userId: number): Promise<void> {\n await prisma.chatSession.deleteMany({ where: { id, userId } });\n },\n\n // Messages ---------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const row = await prisma.chatMessage.create({\n data: {\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n }\n });\n return toMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: number,\n userId: number\n ): Promise<ChatMessage[]> {\n // Re-assert ownership before returning rows. A caller passing a\n // sessionId they don't own should get an empty list, not someone\n // else's transcript.\n const session = await prisma.chatSession.findFirst({\n where: { id: sessionId, userId }\n });\n if (!session) return [];\n const rows = await prisma.chatMessage.findMany({\n where: { sessionId },\n orderBy: { createdAt: 'asc' }\n });\n return rows.map(toMessage);\n },\n\n // AI settings (singleton row at id=1) -----------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const row = await prisma.aiSettings.findUnique({ where: { id: 1 } });\n return row ? toAiSettings(row) : { ...DEFAULT_AI_SETTINGS };\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n const row = await prisma.aiSettings.upsert({\n where: { id: 1 },\n create: { id: 1, ...patch, updatedByUserId: byUserId },\n update: { ...patch, updatedByUserId: byUserId }\n });\n return toAiSettings(row);\n }\n };\n}\n","/**\n * The Prisma schema fragment hosts paste into their own `schema.prisma`.\n *\n * Kept in lock-step with `prisma/chat-models.prisma` at the package root.\n * The fragment is inlined here (not read from disk) so consumer bundlers\n * never need to resolve a runtime fs path.\n *\n * Codegen tooling can import this constant and write it out, splice it into\n * a host's `schema.prisma`, etc.\n */\nexport const prismaSchemaFragment: string = `// =============================================================================\n// @firstlovecenter/ai-chat — Prisma schema fragment\n// =============================================================================\n//\n// HOSTS: copy the three models below into your own \\`schema.prisma\\`, then run\n//\n// pnpm prisma generate\n// pnpm prisma migrate dev --name flc_ai_chat\n//\n// Column names, SQL types, and indexes match the Drizzle adapter shipped at\n// \\`@firstlovecenter/ai-chat/server/drizzle\\` byte-for-byte, so the underlying MySQL schema\n// is identical regardless of which ORM your host picks. A host could swap\n// from one adapter to the other without touching data.\n// =============================================================================\n\nmodel ChatSession {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n userId BigInt @map(\"user_id\") @db.UnsignedBigInt\n title String @db.VarChar(200)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n\n @@index([userId, updatedAt], map: \"idx_chat_session_user_updated\")\n @@map(\"chat_sessions\")\n}\n\nmodel ChatMessage {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n sessionId BigInt @map(\"session_id\") @db.UnsignedBigInt\n role String @db.VarChar(16)\n question String? @db.Text\n blocks Json?\n prose Json?\n errorJson Json? @map(\"error_json\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n\n @@index([sessionId, createdAt], map: \"idx_chat_msg_session_created\")\n @@map(\"chat_messages\")\n}\n\nmodel AiSettings {\n id Int @id @default(1) @db.UnsignedTinyInt\n toolProvider String @default(\"claude\") @map(\"tool_provider\") @db.VarChar(32)\n gcpLocation String @default(\"us-east5\") @map(\"gcp_location\") @db.VarChar(32)\n chatInterface String @default(\"custom\") @map(\"chat_interface\") @db.VarChar(32)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n updatedByUserId BigInt? @map(\"updated_by_user_id\") @db.UnsignedBigInt\n\n @@map(\"ai_settings\")\n}\n`;\n"]}
|
package/dist/prisma/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as ChatMessageRole, c as PersistencePort } from '../types-
|
|
1
|
+
import { m as ChatMessageRole, c as PersistencePort } from '../types-CQntnyDJ.cjs';
|
|
2
2
|
import 'google-auth-library';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
@@ -42,6 +42,8 @@ type AiSettingsRow = {
|
|
|
42
42
|
toolProvider: string;
|
|
43
43
|
gcpLocation: string;
|
|
44
44
|
chatInterface: string;
|
|
45
|
+
maxOutputTokens: number;
|
|
46
|
+
rolePrompt: string | null;
|
|
45
47
|
updatedAt: Date | null;
|
|
46
48
|
updatedByUserId: bigint | number | null;
|
|
47
49
|
};
|
|
@@ -132,12 +134,16 @@ type AiSettingsDelegate = {
|
|
|
132
134
|
toolProvider?: string;
|
|
133
135
|
gcpLocation?: string;
|
|
134
136
|
chatInterface?: string;
|
|
137
|
+
maxOutputTokens?: number;
|
|
138
|
+
rolePrompt?: string | null;
|
|
135
139
|
updatedByUserId: number;
|
|
136
140
|
};
|
|
137
141
|
update: {
|
|
138
142
|
toolProvider?: string;
|
|
139
143
|
gcpLocation?: string;
|
|
140
144
|
chatInterface?: string;
|
|
145
|
+
maxOutputTokens?: number;
|
|
146
|
+
rolePrompt?: string | null;
|
|
141
147
|
updatedByUserId: number;
|
|
142
148
|
};
|
|
143
149
|
}): Promise<AiSettingsRow>;
|
package/dist/prisma/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as ChatMessageRole, c as PersistencePort } from '../types-
|
|
1
|
+
import { m as ChatMessageRole, c as PersistencePort } from '../types-CQntnyDJ.js';
|
|
2
2
|
import 'google-auth-library';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
@@ -42,6 +42,8 @@ type AiSettingsRow = {
|
|
|
42
42
|
toolProvider: string;
|
|
43
43
|
gcpLocation: string;
|
|
44
44
|
chatInterface: string;
|
|
45
|
+
maxOutputTokens: number;
|
|
46
|
+
rolePrompt: string | null;
|
|
45
47
|
updatedAt: Date | null;
|
|
46
48
|
updatedByUserId: bigint | number | null;
|
|
47
49
|
};
|
|
@@ -132,12 +134,16 @@ type AiSettingsDelegate = {
|
|
|
132
134
|
toolProvider?: string;
|
|
133
135
|
gcpLocation?: string;
|
|
134
136
|
chatInterface?: string;
|
|
137
|
+
maxOutputTokens?: number;
|
|
138
|
+
rolePrompt?: string | null;
|
|
135
139
|
updatedByUserId: number;
|
|
136
140
|
};
|
|
137
141
|
update: {
|
|
138
142
|
toolProvider?: string;
|
|
139
143
|
gcpLocation?: string;
|
|
140
144
|
chatInterface?: string;
|
|
145
|
+
maxOutputTokens?: number;
|
|
146
|
+
rolePrompt?: string | null;
|
|
141
147
|
updatedByUserId: number;
|
|
142
148
|
};
|
|
143
149
|
}): Promise<AiSettingsRow>;
|
package/dist/prisma/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/server/ports/types.ts
|
|
2
|
+
var DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;
|
|
3
|
+
|
|
1
4
|
// src/adapters/prisma/adapter.ts
|
|
2
5
|
var toSession = (row) => ({
|
|
3
6
|
id: Number(row.id),
|
|
@@ -21,6 +24,8 @@ var toAiSettings = (row) => ({
|
|
|
21
24
|
toolProvider: row.toolProvider,
|
|
22
25
|
gcpLocation: row.gcpLocation,
|
|
23
26
|
chatInterface: row.chatInterface,
|
|
27
|
+
maxOutputTokens: row.maxOutputTokens,
|
|
28
|
+
rolePrompt: row.rolePrompt,
|
|
24
29
|
updatedAt: row.updatedAt,
|
|
25
30
|
updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)
|
|
26
31
|
});
|
|
@@ -28,6 +33,8 @@ var DEFAULT_AI_SETTINGS = {
|
|
|
28
33
|
toolProvider: "claude",
|
|
29
34
|
gcpLocation: "us-east5",
|
|
30
35
|
chatInterface: "custom",
|
|
36
|
+
maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,
|
|
37
|
+
rolePrompt: null,
|
|
31
38
|
updatedAt: null,
|
|
32
39
|
updatedByUserId: null
|
|
33
40
|
};
|
package/dist/prisma/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/prisma/adapter.ts","../../src/adapters/prisma/schema.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Prisma `PersistencePort` adapter.\n *\n * Hosts call `createPrismaPersistence(prisma)` with their own configured\n * `PrismaClient` and pass the result to `configureAiChat({ persistence, ... })`.\n *\n * The concrete `PrismaClient` is an OPTIONAL peer dep, so this module never\n * imports a runtime value from `@prisma/client`. Instead it accepts a\n * structural duck-typed shape covering only the delegate methods the port\n * uses. Any object that satisfies `PrismaLike` works — including a real\n * `PrismaClient`, a Prisma extension proxy, or a hand-rolled fake in tests.\n *\n * BigInt note: Prisma returns row IDs as JavaScript `BigInt`. Domain types\n * use plain `number` (sessions/messages are nowhere near 2^53). The adapter\n * widens via `Number(row.id)` on every read; writes accept `number` and\n * Prisma narrows internally.\n */\nimport type {\n AiSettings,\n AiSettingsPatch,\n AppendMessageInput,\n ChatMessage,\n ChatMessageRole,\n ChatSession,\n CreateSessionInput,\n ListSessionsOpts,\n PersistencePort\n} from '../../server/ports/types';\n\n// ---------------------------------------------------------------------------\n// Structural Prisma shape — covers exactly what this adapter touches.\n// Avoids a value-level dependency on `@prisma/client` (optional peer dep).\n// ---------------------------------------------------------------------------\n\ntype ChatSessionRow = {\n id: bigint | number;\n userId: bigint | number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\ntype ChatMessageRow = {\n id: bigint | number;\n sessionId: bigint | number;\n role: string;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\ntype AiSettingsRow = {\n id: number;\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n updatedAt: Date | null;\n updatedByUserId: bigint | number | null;\n};\n\ntype ChatSessionDelegate = {\n create(args: {\n data: { userId: number; title: string };\n }): Promise<ChatSessionRow>;\n findFirst(args: {\n where: { id: number; userId: number };\n }): Promise<ChatSessionRow | null>;\n findMany(args: {\n where: { userId: number };\n orderBy: { updatedAt: 'desc' };\n take?: number;\n }): Promise<ChatSessionRow[]>;\n update(args: {\n where: { id: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<ChatSessionRow>;\n updateMany(args: {\n where: { id: number; userId: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<{ count: number }>;\n deleteMany(args: {\n where: { id: number; userId: number };\n }): Promise<{ count: number }>;\n};\n\ntype ChatMessageDelegate = {\n create(args: {\n data: {\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n };\n }): Promise<ChatMessageRow>;\n findMany(args: {\n where: { sessionId: number };\n orderBy: { createdAt: 'asc' };\n }): Promise<ChatMessageRow[]>;\n};\n\ntype AiSettingsDelegate = {\n findUnique(args: { where: { id: number } }): Promise<AiSettingsRow | null>;\n upsert(args: {\n where: { id: number };\n create: {\n id: number;\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n updatedByUserId: number;\n };\n update: {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n updatedByUserId: number;\n };\n }): Promise<AiSettingsRow>;\n};\n\nexport type PrismaLike = {\n chatSession: ChatSessionDelegate;\n chatMessage: ChatMessageDelegate;\n aiSettings: AiSettingsDelegate;\n};\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nconst toSession = (row: ChatSessionRow): ChatSession => ({\n id: Number(row.id),\n userId: Number(row.userId),\n title: row.title,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt\n});\n\nconst toMessage = (row: ChatMessageRow): ChatMessage => ({\n id: Number(row.id),\n sessionId: Number(row.sessionId),\n // Stored as VARCHAR(16) but constrained to ChatMessageRole at write time.\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: row.createdAt\n});\n\nconst toAiSettings = (row: AiSettingsRow): AiSettings => ({\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n updatedAt: row.updatedAt,\n updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)\n});\n\nconst DEFAULT_AI_SETTINGS: AiSettings = {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n updatedAt: null,\n updatedByUserId: null\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createPrismaPersistence(prisma: PrismaLike): PersistencePort {\n return {\n // Sessions ---------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n const row = await prisma.chatSession.create({\n data: { userId: input.userId, title: input.title }\n });\n return toSession(row);\n },\n\n async getSession(id: number, userId: number): Promise<ChatSession | null> {\n const row = await prisma.chatSession.findFirst({ where: { id, userId } });\n return row ? toSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const rows = await prisma.chatSession.findMany({\n where: { userId },\n orderBy: { updatedAt: 'desc' },\n take: opts?.limit\n });\n return rows.map(toSession);\n },\n\n async updateSession(\n id: number,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n // updateMany so the userId scope filters; no-ops cleanly when the\n // session doesn't exist or belongs to another user.\n await prisma.chatSession.updateMany({\n where: { id, userId },\n data: { ...patch, updatedAt: new Date() }\n });\n },\n\n async deleteSession(id: number, userId: number): Promise<void> {\n await prisma.chatSession.deleteMany({ where: { id, userId } });\n },\n\n // Messages ---------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const row = await prisma.chatMessage.create({\n data: {\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n }\n });\n return toMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: number,\n userId: number\n ): Promise<ChatMessage[]> {\n // Re-assert ownership before returning rows. A caller passing a\n // sessionId they don't own should get an empty list, not someone\n // else's transcript.\n const session = await prisma.chatSession.findFirst({\n where: { id: sessionId, userId }\n });\n if (!session) return [];\n const rows = await prisma.chatMessage.findMany({\n where: { sessionId },\n orderBy: { createdAt: 'asc' }\n });\n return rows.map(toMessage);\n },\n\n // AI settings (singleton row at id=1) -----------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const row = await prisma.aiSettings.findUnique({ where: { id: 1 } });\n return row ? toAiSettings(row) : { ...DEFAULT_AI_SETTINGS };\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n const row = await prisma.aiSettings.upsert({\n where: { id: 1 },\n create: { id: 1, ...patch, updatedByUserId: byUserId },\n update: { ...patch, updatedByUserId: byUserId }\n });\n return toAiSettings(row);\n }\n };\n}\n","/**\n * The Prisma schema fragment hosts paste into their own `schema.prisma`.\n *\n * Kept in lock-step with `prisma/chat-models.prisma` at the package root.\n * The fragment is inlined here (not read from disk) so consumer bundlers\n * never need to resolve a runtime fs path.\n *\n * Codegen tooling can import this constant and write it out, splice it into\n * a host's `schema.prisma`, etc.\n */\nexport const prismaSchemaFragment: string = `// =============================================================================\n// @firstlovecenter/ai-chat — Prisma schema fragment\n// =============================================================================\n//\n// HOSTS: copy the three models below into your own \\`schema.prisma\\`, then run\n//\n// pnpm prisma generate\n// pnpm prisma migrate dev --name flc_ai_chat\n//\n// Column names, SQL types, and indexes match the Drizzle adapter shipped at\n// \\`@firstlovecenter/ai-chat/server/drizzle\\` byte-for-byte, so the underlying MySQL schema\n// is identical regardless of which ORM your host picks. A host could swap\n// from one adapter to the other without touching data.\n// =============================================================================\n\nmodel ChatSession {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n userId BigInt @map(\"user_id\") @db.UnsignedBigInt\n title String @db.VarChar(200)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n\n @@index([userId, updatedAt], map: \"idx_chat_session_user_updated\")\n @@map(\"chat_sessions\")\n}\n\nmodel ChatMessage {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n sessionId BigInt @map(\"session_id\") @db.UnsignedBigInt\n role String @db.VarChar(16)\n question String? @db.Text\n blocks Json?\n prose Json?\n errorJson Json? @map(\"error_json\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n\n @@index([sessionId, createdAt], map: \"idx_chat_msg_session_created\")\n @@map(\"chat_messages\")\n}\n\nmodel AiSettings {\n id Int @id @default(1) @db.UnsignedTinyInt\n toolProvider String @default(\"claude\") @map(\"tool_provider\") @db.VarChar(32)\n gcpLocation String @default(\"us-east5\") @map(\"gcp_location\") @db.VarChar(32)\n chatInterface String @default(\"custom\") @map(\"chat_interface\") @db.VarChar(32)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n updatedByUserId BigInt? @map(\"updated_by_user_id\") @db.UnsignedBigInt\n\n @@map(\"ai_settings\")\n}\n`;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/ports/types.ts","../../src/adapters/prisma/adapter.ts","../../src/adapters/prisma/schema.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\nexport type ChatSession = {\n id: number;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: number;\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: number;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: number, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Prisma `PersistencePort` adapter.\n *\n * Hosts call `createPrismaPersistence(prisma)` with their own configured\n * `PrismaClient` and pass the result to `configureAiChat({ persistence, ... })`.\n *\n * The concrete `PrismaClient` is an OPTIONAL peer dep, so this module never\n * imports a runtime value from `@prisma/client`. Instead it accepts a\n * structural duck-typed shape covering only the delegate methods the port\n * uses. Any object that satisfies `PrismaLike` works — including a real\n * `PrismaClient`, a Prisma extension proxy, or a hand-rolled fake in tests.\n *\n * BigInt note: Prisma returns row IDs as JavaScript `BigInt`. Domain types\n * use plain `number` (sessions/messages are nowhere near 2^53). The adapter\n * widens via `Number(row.id)` on every read; writes accept `number` and\n * Prisma narrows internally.\n */\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\n\n// ---------------------------------------------------------------------------\n// Structural Prisma shape — covers exactly what this adapter touches.\n// Avoids a value-level dependency on `@prisma/client` (optional peer dep).\n// ---------------------------------------------------------------------------\n\ntype ChatSessionRow = {\n id: bigint | number;\n userId: bigint | number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\ntype ChatMessageRow = {\n id: bigint | number;\n sessionId: bigint | number;\n role: string;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\ntype AiSettingsRow = {\n id: number;\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n updatedAt: Date | null;\n updatedByUserId: bigint | number | null;\n};\n\ntype ChatSessionDelegate = {\n create(args: {\n data: { userId: number; title: string };\n }): Promise<ChatSessionRow>;\n findFirst(args: {\n where: { id: number; userId: number };\n }): Promise<ChatSessionRow | null>;\n findMany(args: {\n where: { userId: number };\n orderBy: { updatedAt: 'desc' };\n take?: number;\n }): Promise<ChatSessionRow[]>;\n update(args: {\n where: { id: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<ChatSessionRow>;\n updateMany(args: {\n where: { id: number; userId: number };\n data: { title?: string; updatedAt?: Date };\n }): Promise<{ count: number }>;\n deleteMany(args: {\n where: { id: number; userId: number };\n }): Promise<{ count: number }>;\n};\n\ntype ChatMessageDelegate = {\n create(args: {\n data: {\n sessionId: number;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n };\n }): Promise<ChatMessageRow>;\n findMany(args: {\n where: { sessionId: number };\n orderBy: { createdAt: 'asc' };\n }): Promise<ChatMessageRow[]>;\n};\n\ntype AiSettingsDelegate = {\n findUnique(args: { where: { id: number } }): Promise<AiSettingsRow | null>;\n upsert(args: {\n where: { id: number };\n create: {\n id: number;\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n rolePrompt?: string | null;\n updatedByUserId: number;\n };\n update: {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n rolePrompt?: string | null;\n updatedByUserId: number;\n };\n }): Promise<AiSettingsRow>;\n};\n\nexport type PrismaLike = {\n chatSession: ChatSessionDelegate;\n chatMessage: ChatMessageDelegate;\n aiSettings: AiSettingsDelegate;\n};\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nconst toSession = (row: ChatSessionRow): ChatSession => ({\n id: Number(row.id),\n userId: Number(row.userId),\n title: row.title,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt\n});\n\nconst toMessage = (row: ChatMessageRow): ChatMessage => ({\n id: Number(row.id),\n sessionId: Number(row.sessionId),\n // Stored as VARCHAR(16) but constrained to ChatMessageRole at write time.\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: row.createdAt\n});\n\nconst toAiSettings = (row: AiSettingsRow): AiSettings => ({\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n updatedAt: row.updatedAt,\n updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)\n});\n\nconst DEFAULT_AI_SETTINGS: AiSettings = {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n updatedAt: null,\n updatedByUserId: null\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createPrismaPersistence(prisma: PrismaLike): PersistencePort {\n return {\n // Sessions ---------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n const row = await prisma.chatSession.create({\n data: { userId: input.userId, title: input.title }\n });\n return toSession(row);\n },\n\n async getSession(id: number, userId: number): Promise<ChatSession | null> {\n const row = await prisma.chatSession.findFirst({ where: { id, userId } });\n return row ? toSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const rows = await prisma.chatSession.findMany({\n where: { userId },\n orderBy: { updatedAt: 'desc' },\n take: opts?.limit\n });\n return rows.map(toSession);\n },\n\n async updateSession(\n id: number,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n // updateMany so the userId scope filters; no-ops cleanly when the\n // session doesn't exist or belongs to another user.\n await prisma.chatSession.updateMany({\n where: { id, userId },\n data: { ...patch, updatedAt: new Date() }\n });\n },\n\n async deleteSession(id: number, userId: number): Promise<void> {\n await prisma.chatSession.deleteMany({ where: { id, userId } });\n },\n\n // Messages ---------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const row = await prisma.chatMessage.create({\n data: {\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n }\n });\n return toMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: number,\n userId: number\n ): Promise<ChatMessage[]> {\n // Re-assert ownership before returning rows. A caller passing a\n // sessionId they don't own should get an empty list, not someone\n // else's transcript.\n const session = await prisma.chatSession.findFirst({\n where: { id: sessionId, userId }\n });\n if (!session) return [];\n const rows = await prisma.chatMessage.findMany({\n where: { sessionId },\n orderBy: { createdAt: 'asc' }\n });\n return rows.map(toMessage);\n },\n\n // AI settings (singleton row at id=1) -----------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const row = await prisma.aiSettings.findUnique({ where: { id: 1 } });\n return row ? toAiSettings(row) : { ...DEFAULT_AI_SETTINGS };\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n const row = await prisma.aiSettings.upsert({\n where: { id: 1 },\n create: { id: 1, ...patch, updatedByUserId: byUserId },\n update: { ...patch, updatedByUserId: byUserId }\n });\n return toAiSettings(row);\n }\n };\n}\n","/**\n * The Prisma schema fragment hosts paste into their own `schema.prisma`.\n *\n * Kept in lock-step with `prisma/chat-models.prisma` at the package root.\n * The fragment is inlined here (not read from disk) so consumer bundlers\n * never need to resolve a runtime fs path.\n *\n * Codegen tooling can import this constant and write it out, splice it into\n * a host's `schema.prisma`, etc.\n */\nexport const prismaSchemaFragment: string = `// =============================================================================\n// @firstlovecenter/ai-chat — Prisma schema fragment\n// =============================================================================\n//\n// HOSTS: copy the three models below into your own \\`schema.prisma\\`, then run\n//\n// pnpm prisma generate\n// pnpm prisma migrate dev --name flc_ai_chat\n//\n// Column names, SQL types, and indexes match the Drizzle adapter shipped at\n// \\`@firstlovecenter/ai-chat/server/drizzle\\` byte-for-byte, so the underlying MySQL schema\n// is identical regardless of which ORM your host picks. A host could swap\n// from one adapter to the other without touching data.\n// =============================================================================\n\nmodel ChatSession {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n userId BigInt @map(\"user_id\") @db.UnsignedBigInt\n title String @db.VarChar(200)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n\n @@index([userId, updatedAt], map: \"idx_chat_session_user_updated\")\n @@map(\"chat_sessions\")\n}\n\nmodel ChatMessage {\n id BigInt @id @default(autoincrement()) @db.UnsignedBigInt\n sessionId BigInt @map(\"session_id\") @db.UnsignedBigInt\n role String @db.VarChar(16)\n question String? @db.Text\n blocks Json?\n prose Json?\n errorJson Json? @map(\"error_json\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.DateTime(0)\n\n @@index([sessionId, createdAt], map: \"idx_chat_msg_session_created\")\n @@map(\"chat_messages\")\n}\n\nmodel AiSettings {\n id Int @id @default(1) @db.UnsignedTinyInt\n toolProvider String @default(\"claude\") @map(\"tool_provider\") @db.VarChar(32)\n gcpLocation String @default(\"us-east5\") @map(\"gcp_location\") @db.VarChar(32)\n chatInterface String @default(\"custom\") @map(\"chat_interface\") @db.VarChar(32)\n updatedAt DateTime @default(now()) @map(\"updated_at\") @db.DateTime(0)\n updatedByUserId BigInt? @map(\"updated_by_user_id\") @db.UnsignedBigInt\n\n @@map(\"ai_settings\")\n}\n`;\n"]}
|
package/dist/server/index.cjs
CHANGED
|
@@ -652,7 +652,7 @@ async function* streamClaudeNarration(opts) {
|
|
|
652
652
|
});
|
|
653
653
|
const stream = await client.messages.stream({
|
|
654
654
|
model: opts.modelId,
|
|
655
|
-
max_tokens:
|
|
655
|
+
max_tokens: opts.maxTokens,
|
|
656
656
|
system: NARRATIVE_SYSTEM,
|
|
657
657
|
messages: [{ role: "user", content: buildNarrativeUserMessage(opts.input) }]
|
|
658
658
|
});
|
|
@@ -712,7 +712,7 @@ async function* streamGeminiNarration(opts) {
|
|
|
712
712
|
parts: [{ text: buildNarrativeUserMessage(opts.input) }]
|
|
713
713
|
}
|
|
714
714
|
],
|
|
715
|
-
generationConfig: { maxOutputTokens:
|
|
715
|
+
generationConfig: { maxOutputTokens: opts.maxTokens, temperature: 0 }
|
|
716
716
|
})
|
|
717
717
|
});
|
|
718
718
|
if (!res.ok || !res.body) {
|
|
@@ -788,7 +788,7 @@ async function* streamGrokNarration(opts) {
|
|
|
788
788
|
},
|
|
789
789
|
body: JSON.stringify({
|
|
790
790
|
model: opts.modelId,
|
|
791
|
-
max_tokens:
|
|
791
|
+
max_tokens: opts.maxTokens,
|
|
792
792
|
stream: true,
|
|
793
793
|
messages: [
|
|
794
794
|
{ role: "system", content: NARRATIVE_SYSTEM3 },
|
|
@@ -1011,7 +1011,8 @@ data: ${JSON.stringify(data)}
|
|
|
1011
1011
|
ctx: toolContext,
|
|
1012
1012
|
tools: tools.tools,
|
|
1013
1013
|
systemBlocks,
|
|
1014
|
-
provider
|
|
1014
|
+
provider,
|
|
1015
|
+
maxOutputTokens: aiSettings.maxOutputTokens
|
|
1015
1016
|
});
|
|
1016
1017
|
if (!agentResult.ok) {
|
|
1017
1018
|
persistedError = agentResult.error;
|
|
@@ -1037,6 +1038,7 @@ data: ${JSON.stringify(data)}
|
|
|
1037
1038
|
projectId: vertex.projectId,
|
|
1038
1039
|
location: aiSettings.gcpLocation,
|
|
1039
1040
|
modelId: narratorModelId,
|
|
1041
|
+
maxTokens: aiSettings.maxOutputTokens,
|
|
1040
1042
|
input: {
|
|
1041
1043
|
question,
|
|
1042
1044
|
structured,
|
|
@@ -1322,7 +1324,7 @@ function createAgentVercelRoutes(ctx) {
|
|
|
1322
1324
|
messages: [{ role: "user", content: question }],
|
|
1323
1325
|
tools: vercelTools,
|
|
1324
1326
|
maxSteps: 12,
|
|
1325
|
-
maxTokens:
|
|
1327
|
+
maxTokens: aiSettings.maxOutputTokens,
|
|
1326
1328
|
onFinish: async ({ text }) => {
|
|
1327
1329
|
try {
|
|
1328
1330
|
let blocks = presentPayload?.blocks ?? [];
|
|
@@ -1611,6 +1613,8 @@ function createChatSessionsRoutes(ctx) {
|
|
|
1611
1613
|
|
|
1612
1614
|
// src/server/routes/admin-settings.ts
|
|
1613
1615
|
var VALID_LOCATIONS = ["us-east5", "global"];
|
|
1616
|
+
var MIN_MAX_OUTPUT_TOKENS = 256;
|
|
1617
|
+
var MAX_MAX_OUTPUT_TOKENS = 64e3;
|
|
1614
1618
|
function isStringRecord(v) {
|
|
1615
1619
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1616
1620
|
}
|
|
@@ -1625,6 +1629,8 @@ function toWire(settings) {
|
|
|
1625
1629
|
tool_provider: settings.toolProvider,
|
|
1626
1630
|
gcp_location: settings.gcpLocation,
|
|
1627
1631
|
chat_interface: settings.chatInterface,
|
|
1632
|
+
max_output_tokens: settings.maxOutputTokens,
|
|
1633
|
+
role_prompt: settings.rolePrompt,
|
|
1628
1634
|
updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
|
|
1629
1635
|
updated_by_user_id: settings.updatedByUserId
|
|
1630
1636
|
};
|
|
@@ -1704,11 +1710,29 @@ function createAdminSettingsRoutes(ctx) {
|
|
|
1704
1710
|
}
|
|
1705
1711
|
patch.chatInterface = v;
|
|
1706
1712
|
}
|
|
1707
|
-
if (
|
|
1713
|
+
if ("max_output_tokens" in body) {
|
|
1714
|
+
const v = body.max_output_tokens;
|
|
1715
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < MIN_MAX_OUTPUT_TOKENS || v > MAX_MAX_OUTPUT_TOKENS) {
|
|
1716
|
+
return jsonResponse({ error: "invalid_max_output_tokens" }, 400);
|
|
1717
|
+
}
|
|
1718
|
+
patch.maxOutputTokens = v;
|
|
1719
|
+
}
|
|
1720
|
+
if ("role_prompt" in body) {
|
|
1721
|
+
const v = body.role_prompt;
|
|
1722
|
+
if (v === null) {
|
|
1723
|
+
patch.rolePrompt = null;
|
|
1724
|
+
} else if (typeof v === "string") {
|
|
1725
|
+
const trimmed = v.trim();
|
|
1726
|
+
patch.rolePrompt = trimmed === "" ? null : trimmed;
|
|
1727
|
+
} else {
|
|
1728
|
+
return jsonResponse({ error: "invalid_role_prompt" }, 400);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
|
|
1708
1732
|
return jsonResponse(
|
|
1709
1733
|
{
|
|
1710
1734
|
error: "empty_patch",
|
|
1711
|
-
message: "Body must set at least one of tool_provider, gcp_location, chat_interface."
|
|
1735
|
+
message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
|
|
1712
1736
|
},
|
|
1713
1737
|
400
|
|
1714
1738
|
);
|
|
@@ -1733,16 +1757,27 @@ function configureAiChat(opts) {
|
|
|
1733
1757
|
];
|
|
1734
1758
|
const getProvider = (id) => toolProviders2.find((p) => p.id === id) ?? getToolProvider(id);
|
|
1735
1759
|
const chatInterfaces = opts.chatInterfaces ?? BUILTIN_CHAT_INTERFACE_IDS.map((id) => ({ id }));
|
|
1736
|
-
const
|
|
1760
|
+
const staticRolePrompt = opts.rolePrompt;
|
|
1761
|
+
const tools = {
|
|
1737
1762
|
tools: opts.tools.tools,
|
|
1738
1763
|
async buildSystemBlocks(ctx) {
|
|
1739
1764
|
const inner = await opts.tools.buildSystemBlocks(ctx);
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1765
|
+
let role = null;
|
|
1766
|
+
try {
|
|
1767
|
+
const settings = await opts.persistence.getAiSettings();
|
|
1768
|
+
if (settings.rolePrompt && settings.rolePrompt.trim()) {
|
|
1769
|
+
role = settings.rolePrompt;
|
|
1770
|
+
}
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
if (!role && staticRolePrompt) {
|
|
1774
|
+
const resolved = typeof staticRolePrompt === "function" ? await staticRolePrompt(ctx) : staticRolePrompt;
|
|
1775
|
+
if (resolved && resolved.trim()) role = resolved;
|
|
1776
|
+
}
|
|
1777
|
+
if (!role) return inner;
|
|
1743
1778
|
return [{ text: role, cached: true }, ...inner];
|
|
1744
1779
|
}
|
|
1745
|
-
}
|
|
1780
|
+
};
|
|
1746
1781
|
const runAgentBound = async ({
|
|
1747
1782
|
question,
|
|
1748
1783
|
ctx,
|