@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.
@@ -1,5 +1,5 @@
1
- import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CDKxdzQc.cjs';
2
- export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CDKxdzQc.cjs';
1
+ import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CQntnyDJ.cjs';
2
+ export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CQntnyDJ.cjs';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
5
  import 'zod';
@@ -398,7 +398,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
398
398
  /**
399
399
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
400
400
  *
401
- * Three patchable fields on the singleton settings row:
401
+ * Five patchable fields on the singleton settings row:
402
402
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
403
403
  * against the registered `toolProviders` registry passed in via ctx.
404
404
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -408,6 +408,15 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
408
408
  * against the `chatInterfaces` registry passed in via ctx (the actual
409
409
  * registry lives in `@firstlovecenter/ai-chat/ui` so the host wires it through;
410
410
  * the route stays free of UI imports).
411
+ * - `max_output_tokens` — caps the agent loop's per-turn output AND each
412
+ * narrator's prose pass. Bounded `[256, 64000]` — anything below 256 can't
413
+ * fit a useful response, anything above 64000 exceeds the headroom of any
414
+ * model currently routed through Vertex.
415
+ * - `role_prompt` — admin-editable persona string. Empty string and the
416
+ * explicit JSON `null` both clear the override back to the host's static
417
+ * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
+ * whitespace-only string to `null` on write so the "no override" state has
419
+ * a single representation in storage.
411
420
  *
412
421
  * Wire format is snake_case to preserve byte-for-byte parity with the
413
422
  * host route the package replaces — existing host UIs keep working
@@ -1,5 +1,5 @@
1
- import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CDKxdzQc.js';
2
- export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CDKxdzQc.js';
1
+ import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CQntnyDJ.js';
2
+ export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CQntnyDJ.js';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
5
  import 'zod';
@@ -398,7 +398,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
398
398
  /**
399
399
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
400
400
  *
401
- * Three patchable fields on the singleton settings row:
401
+ * Five patchable fields on the singleton settings row:
402
402
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
403
403
  * against the registered `toolProviders` registry passed in via ctx.
404
404
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -408,6 +408,15 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
408
408
  * against the `chatInterfaces` registry passed in via ctx (the actual
409
409
  * registry lives in `@firstlovecenter/ai-chat/ui` so the host wires it through;
410
410
  * the route stays free of UI imports).
411
+ * - `max_output_tokens` — caps the agent loop's per-turn output AND each
412
+ * narrator's prose pass. Bounded `[256, 64000]` — anything below 256 can't
413
+ * fit a useful response, anything above 64000 exceeds the headroom of any
414
+ * model currently routed through Vertex.
415
+ * - `role_prompt` — admin-editable persona string. Empty string and the
416
+ * explicit JSON `null` both clear the override back to the host's static
417
+ * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
+ * whitespace-only string to `null` on write so the "no override" state has
419
+ * a single representation in storage.
411
420
  *
412
421
  * Wire format is snake_case to preserve byte-for-byte parity with the
413
422
  * host route the package replaces — existing host UIs keep working
@@ -650,7 +650,7 @@ async function* streamClaudeNarration(opts) {
650
650
  });
651
651
  const stream = await client.messages.stream({
652
652
  model: opts.modelId,
653
- max_tokens: 400,
653
+ max_tokens: opts.maxTokens,
654
654
  system: NARRATIVE_SYSTEM,
655
655
  messages: [{ role: "user", content: buildNarrativeUserMessage(opts.input) }]
656
656
  });
@@ -710,7 +710,7 @@ async function* streamGeminiNarration(opts) {
710
710
  parts: [{ text: buildNarrativeUserMessage(opts.input) }]
711
711
  }
712
712
  ],
713
- generationConfig: { maxOutputTokens: 400, temperature: 0 }
713
+ generationConfig: { maxOutputTokens: opts.maxTokens, temperature: 0 }
714
714
  })
715
715
  });
716
716
  if (!res.ok || !res.body) {
@@ -786,7 +786,7 @@ async function* streamGrokNarration(opts) {
786
786
  },
787
787
  body: JSON.stringify({
788
788
  model: opts.modelId,
789
- max_tokens: 400,
789
+ max_tokens: opts.maxTokens,
790
790
  stream: true,
791
791
  messages: [
792
792
  { role: "system", content: NARRATIVE_SYSTEM3 },
@@ -1009,7 +1009,8 @@ data: ${JSON.stringify(data)}
1009
1009
  ctx: toolContext,
1010
1010
  tools: tools.tools,
1011
1011
  systemBlocks,
1012
- provider
1012
+ provider,
1013
+ maxOutputTokens: aiSettings.maxOutputTokens
1013
1014
  });
1014
1015
  if (!agentResult.ok) {
1015
1016
  persistedError = agentResult.error;
@@ -1035,6 +1036,7 @@ data: ${JSON.stringify(data)}
1035
1036
  projectId: vertex.projectId,
1036
1037
  location: aiSettings.gcpLocation,
1037
1038
  modelId: narratorModelId,
1039
+ maxTokens: aiSettings.maxOutputTokens,
1038
1040
  input: {
1039
1041
  question,
1040
1042
  structured,
@@ -1320,7 +1322,7 @@ function createAgentVercelRoutes(ctx) {
1320
1322
  messages: [{ role: "user", content: question }],
1321
1323
  tools: vercelTools,
1322
1324
  maxSteps: 12,
1323
- maxTokens: 4096,
1325
+ maxTokens: aiSettings.maxOutputTokens,
1324
1326
  onFinish: async ({ text }) => {
1325
1327
  try {
1326
1328
  let blocks = presentPayload?.blocks ?? [];
@@ -1609,6 +1611,8 @@ function createChatSessionsRoutes(ctx) {
1609
1611
 
1610
1612
  // src/server/routes/admin-settings.ts
1611
1613
  var VALID_LOCATIONS = ["us-east5", "global"];
1614
+ var MIN_MAX_OUTPUT_TOKENS = 256;
1615
+ var MAX_MAX_OUTPUT_TOKENS = 64e3;
1612
1616
  function isStringRecord(v) {
1613
1617
  return typeof v === "object" && v !== null && !Array.isArray(v);
1614
1618
  }
@@ -1623,6 +1627,8 @@ function toWire(settings) {
1623
1627
  tool_provider: settings.toolProvider,
1624
1628
  gcp_location: settings.gcpLocation,
1625
1629
  chat_interface: settings.chatInterface,
1630
+ max_output_tokens: settings.maxOutputTokens,
1631
+ role_prompt: settings.rolePrompt,
1626
1632
  updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
1627
1633
  updated_by_user_id: settings.updatedByUserId
1628
1634
  };
@@ -1702,11 +1708,29 @@ function createAdminSettingsRoutes(ctx) {
1702
1708
  }
1703
1709
  patch.chatInterface = v;
1704
1710
  }
1705
- if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0) {
1711
+ if ("max_output_tokens" in body) {
1712
+ const v = body.max_output_tokens;
1713
+ if (typeof v !== "number" || !Number.isInteger(v) || v < MIN_MAX_OUTPUT_TOKENS || v > MAX_MAX_OUTPUT_TOKENS) {
1714
+ return jsonResponse({ error: "invalid_max_output_tokens" }, 400);
1715
+ }
1716
+ patch.maxOutputTokens = v;
1717
+ }
1718
+ if ("role_prompt" in body) {
1719
+ const v = body.role_prompt;
1720
+ if (v === null) {
1721
+ patch.rolePrompt = null;
1722
+ } else if (typeof v === "string") {
1723
+ const trimmed = v.trim();
1724
+ patch.rolePrompt = trimmed === "" ? null : trimmed;
1725
+ } else {
1726
+ return jsonResponse({ error: "invalid_role_prompt" }, 400);
1727
+ }
1728
+ }
1729
+ if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
1706
1730
  return jsonResponse(
1707
1731
  {
1708
1732
  error: "empty_patch",
1709
- message: "Body must set at least one of tool_provider, gcp_location, chat_interface."
1733
+ message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
1710
1734
  },
1711
1735
  400
1712
1736
  );
@@ -1731,16 +1755,27 @@ function configureAiChat(opts) {
1731
1755
  ];
1732
1756
  const getProvider = (id) => toolProviders2.find((p) => p.id === id) ?? getToolProvider(id);
1733
1757
  const chatInterfaces = opts.chatInterfaces ?? BUILTIN_CHAT_INTERFACE_IDS.map((id) => ({ id }));
1734
- const tools = opts.rolePrompt ? {
1758
+ const staticRolePrompt = opts.rolePrompt;
1759
+ const tools = {
1735
1760
  tools: opts.tools.tools,
1736
1761
  async buildSystemBlocks(ctx) {
1737
1762
  const inner = await opts.tools.buildSystemBlocks(ctx);
1738
- const rolePrompt = opts.rolePrompt;
1739
- const role = typeof rolePrompt === "function" ? await rolePrompt(ctx) : rolePrompt;
1740
- if (!role || !role.trim()) return inner;
1763
+ let role = null;
1764
+ try {
1765
+ const settings = await opts.persistence.getAiSettings();
1766
+ if (settings.rolePrompt && settings.rolePrompt.trim()) {
1767
+ role = settings.rolePrompt;
1768
+ }
1769
+ } catch {
1770
+ }
1771
+ if (!role && staticRolePrompt) {
1772
+ const resolved = typeof staticRolePrompt === "function" ? await staticRolePrompt(ctx) : staticRolePrompt;
1773
+ if (resolved && resolved.trim()) role = resolved;
1774
+ }
1775
+ if (!role) return inner;
1741
1776
  return [{ text: role, cached: true }, ...inner];
1742
1777
  }
1743
- } : opts.tools;
1778
+ };
1744
1779
  const runAgentBound = async ({
1745
1780
  question,
1746
1781
  ctx,