@objectstack/service-ai 6.7.1 → 6.8.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/dist/index.cjs CHANGED
@@ -766,7 +766,20 @@ function createDeleteFieldHandler(ctx) {
766
766
  function createListObjectsHandler(ctx) {
767
767
  return async (args) => {
768
768
  const { filter, includeFields } = args ?? {};
769
- const objects = await ctx.metadataService.listObjects();
769
+ let objects = [];
770
+ if (ctx.protocol?.getMetaItems) {
771
+ try {
772
+ const fromProtocol = await ctx.protocol.getMetaItems({ type: "object" });
773
+ const arr = Array.isArray(fromProtocol) ? fromProtocol : fromProtocol && typeof fromProtocol === "object" && Array.isArray(fromProtocol.items) ? fromProtocol.items : null;
774
+ objects = arr ?? await ctx.metadataService.listObjects();
775
+ } catch {
776
+ objects = await ctx.metadataService.listObjects();
777
+ }
778
+ } else {
779
+ objects = await ctx.metadataService.listObjects();
780
+ }
781
+ if (!Array.isArray(objects)) objects = [];
782
+ if (!Array.isArray(objects)) objects = [];
770
783
  let result = objects.map((o) => {
771
784
  const base = {
772
785
  name: o.name,
@@ -803,7 +816,15 @@ function createDescribeObjectHandler(ctx) {
803
816
  if (!isSnakeCase(objectName)) {
804
817
  return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
805
818
  }
806
- const objectDef = await ctx.metadataService.getObject(objectName);
819
+ let objectDef = await ctx.metadataService.getObject(objectName);
820
+ if (!objectDef && ctx.protocol?.getMetaItems) {
821
+ try {
822
+ const all = await ctx.protocol.getMetaItems({ type: "object" });
823
+ const arr = Array.isArray(all) ? all : all && typeof all === "object" && Array.isArray(all.items) ? all.items : [];
824
+ objectDef = arr.find((o) => o?.name === objectName);
825
+ } catch {
826
+ }
827
+ }
807
828
  if (!objectDef) {
808
829
  return JSON.stringify({ error: `Object "${objectName}" not found` });
809
830
  }
@@ -1403,6 +1424,16 @@ var InMemoryConversationService = class {
1403
1424
  conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1404
1425
  return conversation;
1405
1426
  }
1427
+ async update(conversationId, patch) {
1428
+ const conversation = this.store.get(conversationId);
1429
+ if (!conversation) {
1430
+ throw new Error(`Conversation "${conversationId}" not found`);
1431
+ }
1432
+ if (patch.title !== void 0) conversation.title = patch.title;
1433
+ if (patch.metadata !== void 0) conversation.metadata = patch.metadata;
1434
+ conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1435
+ return conversation;
1436
+ }
1406
1437
  async delete(conversationId) {
1407
1438
  this.store.delete(conversationId);
1408
1439
  }
@@ -1530,6 +1561,30 @@ var _AIService = class _AIService {
1530
1561
  this.logger.info(`[AI] LLM adapter swapped: ${prev} \u2192 ${next.name}`);
1531
1562
  }
1532
1563
  }
1564
+ /**
1565
+ * Best-effort auto-creation of a conversation when the caller did not
1566
+ * supply one but did supply an actor we can attribute the chat to.
1567
+ * Returns the new id on success, or `undefined` if creation failed (in
1568
+ * which case we silently fall back to non-persisted chat).
1569
+ */
1570
+ async autoCreateConversation(ctx) {
1571
+ const actorId = ctx?.actor?.id;
1572
+ if (!actorId) return void 0;
1573
+ try {
1574
+ const conv = await this.conversationService.create({
1575
+ userId: actorId,
1576
+ metadata: ctx?.environmentId ? { environmentId: ctx.environmentId } : void 0
1577
+ });
1578
+ this.logger.debug("[AI] auto-created conversation", { conversationId: conv.id, actorId });
1579
+ return conv.id;
1580
+ } catch (err) {
1581
+ this.logger.warn("[AI] auto-create conversation failed", {
1582
+ actorId,
1583
+ error: err instanceof Error ? err.message : String(err)
1584
+ });
1585
+ return void 0;
1586
+ }
1587
+ }
1533
1588
  /**
1534
1589
  * Best-effort persistence of a single chat message to the conversation
1535
1590
  * store. Failures are logged at warn level and swallowed — chat requests
@@ -1668,7 +1723,12 @@ var _AIService = class _AIService {
1668
1723
  } = options ?? {};
1669
1724
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1670
1725
  const registeredTools = this.toolRegistry.getAll();
1671
- const conversationId = toolExecutionContext?.conversationId;
1726
+ let conversationId = toolExecutionContext?.conversationId;
1727
+ let autoCreatedConversationId;
1728
+ if (!conversationId) {
1729
+ autoCreatedConversationId = await this.autoCreateConversation(toolExecutionContext);
1730
+ conversationId = autoCreatedConversationId;
1731
+ }
1672
1732
  const mergedTools = [
1673
1733
  ...registeredTools,
1674
1734
  ...restOptions.tools ?? []
@@ -1702,7 +1762,7 @@ var _AIService = class _AIService {
1702
1762
  content: result.content
1703
1763
  });
1704
1764
  }
1705
- return result;
1765
+ return autoCreatedConversationId ? { ...result, conversationId: autoCreatedConversationId } : result;
1706
1766
  }
1707
1767
  this.logger.debug("[AI] chatWithTools tool calls", {
1708
1768
  iteration,
@@ -1769,7 +1829,7 @@ var _AIService = class _AIService {
1769
1829
  content: finalResult.content
1770
1830
  });
1771
1831
  }
1772
- return finalResult;
1832
+ return autoCreatedConversationId ? { ...finalResult, conversationId: autoCreatedConversationId } : finalResult;
1773
1833
  }
1774
1834
  /**
1775
1835
  * Stream chat with automatic tool call resolution.
@@ -1787,7 +1847,12 @@ var _AIService = class _AIService {
1787
1847
  } = options ?? {};
1788
1848
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1789
1849
  const registeredTools = this.toolRegistry.getAll();
1790
- const conversationId = toolExecutionContext?.conversationId;
1850
+ let conversationId = toolExecutionContext?.conversationId;
1851
+ let autoCreatedConversationId;
1852
+ if (!conversationId) {
1853
+ autoCreatedConversationId = await this.autoCreateConversation(toolExecutionContext);
1854
+ conversationId = autoCreatedConversationId;
1855
+ }
1791
1856
  const mergedTools = [
1792
1857
  ...registeredTools,
1793
1858
  ...restOptions.tools ?? []
@@ -1799,6 +1864,14 @@ var _AIService = class _AIService {
1799
1864
  };
1800
1865
  const conversation = [...messages];
1801
1866
  let abortedByCallback = false;
1867
+ if (autoCreatedConversationId) {
1868
+ yield {
1869
+ type: "tool-call",
1870
+ toolCallId: "__conversation_meta__",
1871
+ toolName: "__conversation_meta__",
1872
+ input: { conversationId: autoCreatedConversationId }
1873
+ };
1874
+ }
1802
1875
  if (conversationId && messages.length > 0) {
1803
1876
  const last = messages[messages.length - 1];
1804
1877
  if (last && last.role === "user") {
@@ -2450,6 +2523,56 @@ function buildAIRoutes(aiService, conversationService, logger) {
2450
2523
  }
2451
2524
  }
2452
2525
  },
2526
+ {
2527
+ method: "PATCH",
2528
+ path: "/api/v1/ai/conversations/:id",
2529
+ description: "Update mutable conversation fields (title, metadata)",
2530
+ auth: true,
2531
+ permissions: ["ai:conversations"],
2532
+ handler: async (req) => {
2533
+ const id = req.params?.id;
2534
+ if (!id) {
2535
+ return { status: 400, body: { error: "conversation id is required" } };
2536
+ }
2537
+ const body = req.body ?? {};
2538
+ const patch = {};
2539
+ if (body.title !== void 0) {
2540
+ if (typeof body.title !== "string") {
2541
+ return { status: 400, body: { error: "title must be a string" } };
2542
+ }
2543
+ patch.title = body.title;
2544
+ }
2545
+ if (body.metadata !== void 0) {
2546
+ if (typeof body.metadata !== "object" || body.metadata === null || Array.isArray(body.metadata)) {
2547
+ return { status: 400, body: { error: "metadata must be an object" } };
2548
+ }
2549
+ patch.metadata = body.metadata;
2550
+ }
2551
+ if (patch.title === void 0 && patch.metadata === void 0) {
2552
+ return { status: 400, body: { error: "at least one of title or metadata is required" } };
2553
+ }
2554
+ try {
2555
+ if (req.user?.userId) {
2556
+ const existing = await conversationService.get(id);
2557
+ if (!existing) {
2558
+ return { status: 404, body: { error: `Conversation "${id}" not found` } };
2559
+ }
2560
+ if (existing.userId && existing.userId !== req.user.userId) {
2561
+ return { status: 403, body: { error: "You do not have access to this conversation" } };
2562
+ }
2563
+ }
2564
+ const conversation = await conversationService.update(id, patch);
2565
+ return { status: 200, body: conversation };
2566
+ } catch (err) {
2567
+ const msg = err instanceof Error ? err.message : String(err);
2568
+ if (msg.includes("not found")) {
2569
+ return { status: 404, body: { error: msg } };
2570
+ }
2571
+ logger.error("[AI Route] PATCH /conversations/:id error", err instanceof Error ? err : void 0);
2572
+ return { status: 500, body: { error: "Internal AI service error" } };
2573
+ }
2574
+ }
2575
+ },
2453
2576
  {
2454
2577
  method: "DELETE",
2455
2578
  path: "/api/v1/ai/conversations/:id",
@@ -3213,6 +3336,22 @@ var ObjectQLConversationService = class {
3213
3336
  });
3214
3337
  return await this.get(conversationId);
3215
3338
  }
3339
+ async update(conversationId, patch) {
3340
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
3341
+ where: { id: conversationId }
3342
+ });
3343
+ if (!row) {
3344
+ throw new Error(`Conversation "${conversationId}" not found`);
3345
+ }
3346
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3347
+ const updates = { id: conversationId, updated_at: now };
3348
+ if (patch.title !== void 0) updates.title = patch.title;
3349
+ if (patch.metadata !== void 0) updates.metadata = JSON.stringify(patch.metadata);
3350
+ await this.engine.update(CONVERSATIONS_OBJECT, updates, {
3351
+ where: { id: conversationId }
3352
+ });
3353
+ return await this.get(conversationId);
3354
+ }
3216
3355
  async delete(conversationId) {
3217
3356
  await this.engine.delete(MESSAGES_OBJECT, {
3218
3357
  where: { conversation_id: conversationId },
@@ -3996,8 +4135,9 @@ var import_zod = require("zod");
3996
4135
 
3997
4136
  // src/schema-retriever.ts
3998
4137
  var SchemaRetriever = class {
3999
- constructor(metadata, options = {}) {
4138
+ constructor(metadata, options = {}, protocol) {
4000
4139
  this.metadata = metadata;
4140
+ this.protocol = protocol;
4001
4141
  this.options = {
4002
4142
  limit: options.limit ?? 3,
4003
4143
  minScore: options.minScore ?? 1,
@@ -4014,7 +4154,19 @@ var SchemaRetriever = class {
4014
4154
  async retrieve(query) {
4015
4155
  const terms = tokenise(query);
4016
4156
  if (terms.length === 0) return [];
4017
- const objects = await this.metadata.listObjects();
4157
+ let objects = [];
4158
+ if (this.protocol?.getMetaItems) {
4159
+ try {
4160
+ const fromProtocol = await this.protocol.getMetaItems({ type: "object" });
4161
+ const arr = Array.isArray(fromProtocol) ? fromProtocol : fromProtocol && typeof fromProtocol === "object" && Array.isArray(fromProtocol.items) ? fromProtocol.items : null;
4162
+ objects = arr ?? await this.metadata.listObjects();
4163
+ } catch {
4164
+ objects = await this.metadata.listObjects();
4165
+ }
4166
+ } else {
4167
+ objects = await this.metadata.listObjects();
4168
+ }
4169
+ if (!Array.isArray(objects)) objects = [];
4018
4170
  const hits = [];
4019
4171
  for (const raw of objects) {
4020
4172
  const obj = raw;
@@ -4184,7 +4336,7 @@ var QUERY_DATA_TOOL = {
4184
4336
  }
4185
4337
  };
4186
4338
  function createQueryDataHandler(ctx) {
4187
- const retriever = new SchemaRetriever(ctx.metadata);
4339
+ const retriever = new SchemaRetriever(ctx.metadata, {}, ctx.protocol);
4188
4340
  const maxLimit = ctx.maxLimit ?? 100;
4189
4341
  return async (args, execCtx) => {
4190
4342
  const { request } = args;
@@ -5451,16 +5603,18 @@ var AIServicePlugin = class {
5451
5603
  return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode)" };
5452
5604
  }
5453
5605
  if (provider === "gateway") {
5454
- const gatewayModel = String(values.gateway_model ?? "").trim();
5606
+ const gatewayModel = String(values.gateway_model ?? "").trim() || String(process.env.AI_GATEWAY_MODEL ?? "").trim();
5455
5607
  if (!gatewayModel) return null;
5608
+ const gatewayApiKey = String(values.gateway_api_key ?? "").trim() || String(process.env.AI_GATEWAY_API_KEY ?? "").trim();
5456
5609
  try {
5457
5610
  const gatewayPkg = "@ai-sdk/gateway";
5458
- const { gateway } = await import(
5611
+ const mod = await import(
5459
5612
  /* webpackIgnore: true */
5460
5613
  gatewayPkg
5461
5614
  );
5615
+ const gw = gatewayApiKey ? mod.createGateway({ apiKey: gatewayApiKey }) : mod.gateway;
5462
5616
  return {
5463
- adapter: new VercelLLMAdapter({ model: gateway(gatewayModel) }),
5617
+ adapter: new VercelLLMAdapter({ model: gw(gatewayModel) }),
5464
5618
  description: `Vercel AI Gateway (model: ${gatewayModel})`
5465
5619
  };
5466
5620
  } catch (err) {
@@ -5478,7 +5632,12 @@ var AIServicePlugin = class {
5478
5632
  };
5479
5633
  const spec = providerSpecs[provider];
5480
5634
  if (!spec) return null;
5481
- const apiKey = String(values[`${provider}_api_key`] ?? "").trim();
5635
+ const apiKey = String(values[`${provider}_api_key`] ?? "").trim() || // Fall back to the corresponding env var so operators who only
5636
+ // configured env credentials (and didn't paste the key into the
5637
+ // settings form) can still validate the connection.
5638
+ String(
5639
+ process.env[provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY"] ?? ""
5640
+ ).trim();
5482
5641
  if (!apiKey) return null;
5483
5642
  const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY";
5484
5643
  process.env[envKey] = apiKey;
@@ -5754,6 +5913,13 @@ var AIServicePlugin = class {
5754
5913
  metadataService = void 0;
5755
5914
  }
5756
5915
  }
5916
+ let protocolService;
5917
+ try {
5918
+ const p = ctx.getService("protocol");
5919
+ if (p && typeof p.getMetaItems === "function") protocolService = p;
5920
+ } catch {
5921
+ protocolService = void 0;
5922
+ }
5757
5923
  try {
5758
5924
  const dataEngine = ctx.getService("data");
5759
5925
  if (dataEngine) {
@@ -5763,7 +5929,8 @@ var AIServicePlugin = class {
5763
5929
  registerQueryDataTool(this.service.toolRegistry, {
5764
5930
  ai: this.service,
5765
5931
  metadata: metadataService,
5766
- dataEngine
5932
+ dataEngine,
5933
+ protocol: protocolService
5767
5934
  });
5768
5935
  ctx.logger.info("[AI] query_data tool registered");
5769
5936
  try {
@@ -5875,7 +6042,7 @@ var AIServicePlugin = class {
5875
6042
  }
5876
6043
  if (metadataService) {
5877
6044
  try {
5878
- registerMetadataTools(this.service.toolRegistry, { metadataService });
6045
+ registerMetadataTools(this.service.toolRegistry, { metadataService, protocol: protocolService });
5879
6046
  ctx.logger.info("[AI] Built-in metadata tools registered");
5880
6047
  const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
5881
6048
  for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
@@ -6070,7 +6237,7 @@ var AIServicePlugin = class {
6070
6237
  }
6071
6238
  if (typeof settings.registerAction === "function") {
6072
6239
  settings.registerAction("ai", "test_embedder", async ({ values, payload }) => {
6073
- const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6240
+ const overrides = extractOverrides(payload);
6074
6241
  const merged = { ...values ?? {}, ...overrides };
6075
6242
  const provider = String(merged.embedder_provider ?? "none");
6076
6243
  if (provider === "none") {
@@ -6115,10 +6282,33 @@ var AIServicePlugin = class {
6115
6282
  }
6116
6283
  if (typeof settings.registerAction === "function") {
6117
6284
  settings.registerAction("ai", "test", async ({ values, payload }) => {
6118
- const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6285
+ const overrides = extractOverrides(payload);
6119
6286
  const merged = { ...values ?? {}, ...overrides };
6120
6287
  const provider = String(merged.provider ?? "memory");
6121
6288
  if (provider === "memory") {
6289
+ const liveName = this.service?.adapterName ?? "";
6290
+ if (this.service && liveName && liveName !== "memory") {
6291
+ const started2 = Date.now();
6292
+ try {
6293
+ const result = await this.service.chat(
6294
+ [{ role: "user", content: "ping" }],
6295
+ { maxTokens: 8 }
6296
+ );
6297
+ const latency = Date.now() - started2;
6298
+ const preview = String(result?.text ?? "").slice(0, 60);
6299
+ return {
6300
+ ok: true,
6301
+ severity: "info",
6302
+ message: `Env-configured adapter "${liveName}" responded in ${latency}ms${preview ? ` \u2014 "${preview}"` : ""}.`
6303
+ };
6304
+ } catch (err) {
6305
+ return {
6306
+ ok: false,
6307
+ severity: "error",
6308
+ message: `Env-configured adapter "${liveName}" request failed: ${err?.message ?? String(err)}`
6309
+ };
6310
+ }
6311
+ }
6122
6312
  return {
6123
6313
  ok: true,
6124
6314
  severity: "warning",
@@ -6135,7 +6325,7 @@ var AIServicePlugin = class {
6135
6325
  return {
6136
6326
  ok: false,
6137
6327
  severity: "error",
6138
- message: `Could not build adapter for provider=${provider}. Check API key and that the provider SDK package is installed.`
6328
+ message: `Could not build adapter for provider=${provider}. Check API key (or the corresponding env var) and that the provider SDK package is installed.`
6139
6329
  };
6140
6330
  }
6141
6331
  const started = Date.now();
@@ -6152,6 +6342,15 @@ var AIServicePlugin = class {
6152
6342
  message: `${built.description} responded in ${latency}ms${preview ? ` \u2014 "${preview}"` : ""}.`
6153
6343
  };
6154
6344
  } catch (err) {
6345
+ const isGwAuth = err?.name === "GatewayAuthenticationError";
6346
+ const keyWasProvided = provider === "gateway" && String(merged.gateway_api_key ?? process.env.AI_GATEWAY_API_KEY ?? "").trim().length > 0;
6347
+ if (isGwAuth && keyWasProvided) {
6348
+ return {
6349
+ ok: false,
6350
+ severity: "error",
6351
+ message: `${built.description}: API key was rejected by the AI Gateway (invalid, expired, or lacking access to model "${String(merged.gateway_model)}"). Create a new key at https://vercel.com/dashboard/ai-gateway/api-keys and re-save the settings.`
6352
+ };
6353
+ }
6155
6354
  return {
6156
6355
  ok: false,
6157
6356
  severity: "error",
@@ -6166,6 +6365,14 @@ var AIServicePlugin = class {
6166
6365
  this.service = void 0;
6167
6366
  }
6168
6367
  };
6368
+ function extractOverrides(payload) {
6369
+ if (!payload || typeof payload !== "object") return {};
6370
+ const p = payload;
6371
+ if (p.values && typeof p.values === "object" && p.values !== null) {
6372
+ return p.values;
6373
+ }
6374
+ return p;
6375
+ }
6169
6376
 
6170
6377
  // src/index.ts
6171
6378
  init_data_tools();