@objectstack/service-ai 6.7.1 → 6.8.0

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.js CHANGED
@@ -754,7 +754,20 @@ function createDeleteFieldHandler(ctx) {
754
754
  function createListObjectsHandler(ctx) {
755
755
  return async (args) => {
756
756
  const { filter, includeFields } = args ?? {};
757
- const objects = await ctx.metadataService.listObjects();
757
+ let objects = [];
758
+ if (ctx.protocol?.getMetaItems) {
759
+ try {
760
+ const fromProtocol = await ctx.protocol.getMetaItems({ type: "object" });
761
+ const arr = Array.isArray(fromProtocol) ? fromProtocol : fromProtocol && typeof fromProtocol === "object" && Array.isArray(fromProtocol.items) ? fromProtocol.items : null;
762
+ objects = arr ?? await ctx.metadataService.listObjects();
763
+ } catch {
764
+ objects = await ctx.metadataService.listObjects();
765
+ }
766
+ } else {
767
+ objects = await ctx.metadataService.listObjects();
768
+ }
769
+ if (!Array.isArray(objects)) objects = [];
770
+ if (!Array.isArray(objects)) objects = [];
758
771
  let result = objects.map((o) => {
759
772
  const base = {
760
773
  name: o.name,
@@ -791,7 +804,15 @@ function createDescribeObjectHandler(ctx) {
791
804
  if (!isSnakeCase(objectName)) {
792
805
  return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
793
806
  }
794
- const objectDef = await ctx.metadataService.getObject(objectName);
807
+ let objectDef = await ctx.metadataService.getObject(objectName);
808
+ if (!objectDef && ctx.protocol?.getMetaItems) {
809
+ try {
810
+ const all = await ctx.protocol.getMetaItems({ type: "object" });
811
+ const arr = Array.isArray(all) ? all : all && typeof all === "object" && Array.isArray(all.items) ? all.items : [];
812
+ objectDef = arr.find((o) => o?.name === objectName);
813
+ } catch {
814
+ }
815
+ }
795
816
  if (!objectDef) {
796
817
  return JSON.stringify({ error: `Object "${objectName}" not found` });
797
818
  }
@@ -1329,6 +1350,16 @@ var InMemoryConversationService = class {
1329
1350
  conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1330
1351
  return conversation;
1331
1352
  }
1353
+ async update(conversationId, patch) {
1354
+ const conversation = this.store.get(conversationId);
1355
+ if (!conversation) {
1356
+ throw new Error(`Conversation "${conversationId}" not found`);
1357
+ }
1358
+ if (patch.title !== void 0) conversation.title = patch.title;
1359
+ if (patch.metadata !== void 0) conversation.metadata = patch.metadata;
1360
+ conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1361
+ return conversation;
1362
+ }
1332
1363
  async delete(conversationId) {
1333
1364
  this.store.delete(conversationId);
1334
1365
  }
@@ -1456,6 +1487,30 @@ var _AIService = class _AIService {
1456
1487
  this.logger.info(`[AI] LLM adapter swapped: ${prev} \u2192 ${next.name}`);
1457
1488
  }
1458
1489
  }
1490
+ /**
1491
+ * Best-effort auto-creation of a conversation when the caller did not
1492
+ * supply one but did supply an actor we can attribute the chat to.
1493
+ * Returns the new id on success, or `undefined` if creation failed (in
1494
+ * which case we silently fall back to non-persisted chat).
1495
+ */
1496
+ async autoCreateConversation(ctx) {
1497
+ const actorId = ctx?.actor?.id;
1498
+ if (!actorId) return void 0;
1499
+ try {
1500
+ const conv = await this.conversationService.create({
1501
+ userId: actorId,
1502
+ metadata: ctx?.environmentId ? { environmentId: ctx.environmentId } : void 0
1503
+ });
1504
+ this.logger.debug("[AI] auto-created conversation", { conversationId: conv.id, actorId });
1505
+ return conv.id;
1506
+ } catch (err) {
1507
+ this.logger.warn("[AI] auto-create conversation failed", {
1508
+ actorId,
1509
+ error: err instanceof Error ? err.message : String(err)
1510
+ });
1511
+ return void 0;
1512
+ }
1513
+ }
1459
1514
  /**
1460
1515
  * Best-effort persistence of a single chat message to the conversation
1461
1516
  * store. Failures are logged at warn level and swallowed — chat requests
@@ -1594,7 +1649,12 @@ var _AIService = class _AIService {
1594
1649
  } = options ?? {};
1595
1650
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1596
1651
  const registeredTools = this.toolRegistry.getAll();
1597
- const conversationId = toolExecutionContext?.conversationId;
1652
+ let conversationId = toolExecutionContext?.conversationId;
1653
+ let autoCreatedConversationId;
1654
+ if (!conversationId) {
1655
+ autoCreatedConversationId = await this.autoCreateConversation(toolExecutionContext);
1656
+ conversationId = autoCreatedConversationId;
1657
+ }
1598
1658
  const mergedTools = [
1599
1659
  ...registeredTools,
1600
1660
  ...restOptions.tools ?? []
@@ -1628,7 +1688,7 @@ var _AIService = class _AIService {
1628
1688
  content: result.content
1629
1689
  });
1630
1690
  }
1631
- return result;
1691
+ return autoCreatedConversationId ? { ...result, conversationId: autoCreatedConversationId } : result;
1632
1692
  }
1633
1693
  this.logger.debug("[AI] chatWithTools tool calls", {
1634
1694
  iteration,
@@ -1695,7 +1755,7 @@ var _AIService = class _AIService {
1695
1755
  content: finalResult.content
1696
1756
  });
1697
1757
  }
1698
- return finalResult;
1758
+ return autoCreatedConversationId ? { ...finalResult, conversationId: autoCreatedConversationId } : finalResult;
1699
1759
  }
1700
1760
  /**
1701
1761
  * Stream chat with automatic tool call resolution.
@@ -1713,7 +1773,12 @@ var _AIService = class _AIService {
1713
1773
  } = options ?? {};
1714
1774
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1715
1775
  const registeredTools = this.toolRegistry.getAll();
1716
- const conversationId = toolExecutionContext?.conversationId;
1776
+ let conversationId = toolExecutionContext?.conversationId;
1777
+ let autoCreatedConversationId;
1778
+ if (!conversationId) {
1779
+ autoCreatedConversationId = await this.autoCreateConversation(toolExecutionContext);
1780
+ conversationId = autoCreatedConversationId;
1781
+ }
1717
1782
  const mergedTools = [
1718
1783
  ...registeredTools,
1719
1784
  ...restOptions.tools ?? []
@@ -1725,6 +1790,14 @@ var _AIService = class _AIService {
1725
1790
  };
1726
1791
  const conversation = [...messages];
1727
1792
  let abortedByCallback = false;
1793
+ if (autoCreatedConversationId) {
1794
+ yield {
1795
+ type: "tool-call",
1796
+ toolCallId: "__conversation_meta__",
1797
+ toolName: "__conversation_meta__",
1798
+ input: { conversationId: autoCreatedConversationId }
1799
+ };
1800
+ }
1728
1801
  if (conversationId && messages.length > 0) {
1729
1802
  const last = messages[messages.length - 1];
1730
1803
  if (last && last.role === "user") {
@@ -2376,6 +2449,56 @@ function buildAIRoutes(aiService, conversationService, logger) {
2376
2449
  }
2377
2450
  }
2378
2451
  },
2452
+ {
2453
+ method: "PATCH",
2454
+ path: "/api/v1/ai/conversations/:id",
2455
+ description: "Update mutable conversation fields (title, metadata)",
2456
+ auth: true,
2457
+ permissions: ["ai:conversations"],
2458
+ handler: async (req) => {
2459
+ const id = req.params?.id;
2460
+ if (!id) {
2461
+ return { status: 400, body: { error: "conversation id is required" } };
2462
+ }
2463
+ const body = req.body ?? {};
2464
+ const patch = {};
2465
+ if (body.title !== void 0) {
2466
+ if (typeof body.title !== "string") {
2467
+ return { status: 400, body: { error: "title must be a string" } };
2468
+ }
2469
+ patch.title = body.title;
2470
+ }
2471
+ if (body.metadata !== void 0) {
2472
+ if (typeof body.metadata !== "object" || body.metadata === null || Array.isArray(body.metadata)) {
2473
+ return { status: 400, body: { error: "metadata must be an object" } };
2474
+ }
2475
+ patch.metadata = body.metadata;
2476
+ }
2477
+ if (patch.title === void 0 && patch.metadata === void 0) {
2478
+ return { status: 400, body: { error: "at least one of title or metadata is required" } };
2479
+ }
2480
+ try {
2481
+ if (req.user?.userId) {
2482
+ const existing = await conversationService.get(id);
2483
+ if (!existing) {
2484
+ return { status: 404, body: { error: `Conversation "${id}" not found` } };
2485
+ }
2486
+ if (existing.userId && existing.userId !== req.user.userId) {
2487
+ return { status: 403, body: { error: "You do not have access to this conversation" } };
2488
+ }
2489
+ }
2490
+ const conversation = await conversationService.update(id, patch);
2491
+ return { status: 200, body: conversation };
2492
+ } catch (err) {
2493
+ const msg = err instanceof Error ? err.message : String(err);
2494
+ if (msg.includes("not found")) {
2495
+ return { status: 404, body: { error: msg } };
2496
+ }
2497
+ logger.error("[AI Route] PATCH /conversations/:id error", err instanceof Error ? err : void 0);
2498
+ return { status: 500, body: { error: "Internal AI service error" } };
2499
+ }
2500
+ }
2501
+ },
2379
2502
  {
2380
2503
  method: "DELETE",
2381
2504
  path: "/api/v1/ai/conversations/:id",
@@ -3139,6 +3262,22 @@ var ObjectQLConversationService = class {
3139
3262
  });
3140
3263
  return await this.get(conversationId);
3141
3264
  }
3265
+ async update(conversationId, patch) {
3266
+ const row = await this.engine.findOne(CONVERSATIONS_OBJECT, {
3267
+ where: { id: conversationId }
3268
+ });
3269
+ if (!row) {
3270
+ throw new Error(`Conversation "${conversationId}" not found`);
3271
+ }
3272
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3273
+ const updates = { id: conversationId, updated_at: now };
3274
+ if (patch.title !== void 0) updates.title = patch.title;
3275
+ if (patch.metadata !== void 0) updates.metadata = JSON.stringify(patch.metadata);
3276
+ await this.engine.update(CONVERSATIONS_OBJECT, updates, {
3277
+ where: { id: conversationId }
3278
+ });
3279
+ return await this.get(conversationId);
3280
+ }
3142
3281
  async delete(conversationId) {
3143
3282
  await this.engine.delete(MESSAGES_OBJECT, {
3144
3283
  where: { conversation_id: conversationId },
@@ -3922,8 +4061,9 @@ import { z } from "zod";
3922
4061
 
3923
4062
  // src/schema-retriever.ts
3924
4063
  var SchemaRetriever = class {
3925
- constructor(metadata, options = {}) {
4064
+ constructor(metadata, options = {}, protocol) {
3926
4065
  this.metadata = metadata;
4066
+ this.protocol = protocol;
3927
4067
  this.options = {
3928
4068
  limit: options.limit ?? 3,
3929
4069
  minScore: options.minScore ?? 1,
@@ -3940,7 +4080,19 @@ var SchemaRetriever = class {
3940
4080
  async retrieve(query) {
3941
4081
  const terms = tokenise(query);
3942
4082
  if (terms.length === 0) return [];
3943
- const objects = await this.metadata.listObjects();
4083
+ let objects = [];
4084
+ if (this.protocol?.getMetaItems) {
4085
+ try {
4086
+ const fromProtocol = await this.protocol.getMetaItems({ type: "object" });
4087
+ const arr = Array.isArray(fromProtocol) ? fromProtocol : fromProtocol && typeof fromProtocol === "object" && Array.isArray(fromProtocol.items) ? fromProtocol.items : null;
4088
+ objects = arr ?? await this.metadata.listObjects();
4089
+ } catch {
4090
+ objects = await this.metadata.listObjects();
4091
+ }
4092
+ } else {
4093
+ objects = await this.metadata.listObjects();
4094
+ }
4095
+ if (!Array.isArray(objects)) objects = [];
3944
4096
  const hits = [];
3945
4097
  for (const raw of objects) {
3946
4098
  const obj = raw;
@@ -4110,7 +4262,7 @@ var QUERY_DATA_TOOL = {
4110
4262
  }
4111
4263
  };
4112
4264
  function createQueryDataHandler(ctx) {
4113
- const retriever = new SchemaRetriever(ctx.metadata);
4265
+ const retriever = new SchemaRetriever(ctx.metadata, {}, ctx.protocol);
4114
4266
  const maxLimit = ctx.maxLimit ?? 100;
4115
4267
  return async (args, execCtx) => {
4116
4268
  const { request } = args;
@@ -5377,16 +5529,18 @@ var AIServicePlugin = class {
5377
5529
  return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode)" };
5378
5530
  }
5379
5531
  if (provider === "gateway") {
5380
- const gatewayModel = String(values.gateway_model ?? "").trim();
5532
+ const gatewayModel = String(values.gateway_model ?? "").trim() || String(process.env.AI_GATEWAY_MODEL ?? "").trim();
5381
5533
  if (!gatewayModel) return null;
5534
+ const gatewayApiKey = String(values.gateway_api_key ?? "").trim() || String(process.env.AI_GATEWAY_API_KEY ?? "").trim();
5382
5535
  try {
5383
5536
  const gatewayPkg = "@ai-sdk/gateway";
5384
- const { gateway } = await import(
5537
+ const mod = await import(
5385
5538
  /* webpackIgnore: true */
5386
5539
  gatewayPkg
5387
5540
  );
5541
+ const gw = gatewayApiKey ? mod.createGateway({ apiKey: gatewayApiKey }) : mod.gateway;
5388
5542
  return {
5389
- adapter: new VercelLLMAdapter({ model: gateway(gatewayModel) }),
5543
+ adapter: new VercelLLMAdapter({ model: gw(gatewayModel) }),
5390
5544
  description: `Vercel AI Gateway (model: ${gatewayModel})`
5391
5545
  };
5392
5546
  } catch (err) {
@@ -5404,7 +5558,12 @@ var AIServicePlugin = class {
5404
5558
  };
5405
5559
  const spec = providerSpecs[provider];
5406
5560
  if (!spec) return null;
5407
- const apiKey = String(values[`${provider}_api_key`] ?? "").trim();
5561
+ const apiKey = String(values[`${provider}_api_key`] ?? "").trim() || // Fall back to the corresponding env var so operators who only
5562
+ // configured env credentials (and didn't paste the key into the
5563
+ // settings form) can still validate the connection.
5564
+ String(
5565
+ process.env[provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY"] ?? ""
5566
+ ).trim();
5408
5567
  if (!apiKey) return null;
5409
5568
  const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY";
5410
5569
  process.env[envKey] = apiKey;
@@ -5680,6 +5839,13 @@ var AIServicePlugin = class {
5680
5839
  metadataService = void 0;
5681
5840
  }
5682
5841
  }
5842
+ let protocolService;
5843
+ try {
5844
+ const p = ctx.getService("protocol");
5845
+ if (p && typeof p.getMetaItems === "function") protocolService = p;
5846
+ } catch {
5847
+ protocolService = void 0;
5848
+ }
5683
5849
  try {
5684
5850
  const dataEngine = ctx.getService("data");
5685
5851
  if (dataEngine) {
@@ -5689,7 +5855,8 @@ var AIServicePlugin = class {
5689
5855
  registerQueryDataTool(this.service.toolRegistry, {
5690
5856
  ai: this.service,
5691
5857
  metadata: metadataService,
5692
- dataEngine
5858
+ dataEngine,
5859
+ protocol: protocolService
5693
5860
  });
5694
5861
  ctx.logger.info("[AI] query_data tool registered");
5695
5862
  try {
@@ -5801,7 +5968,7 @@ var AIServicePlugin = class {
5801
5968
  }
5802
5969
  if (metadataService) {
5803
5970
  try {
5804
- registerMetadataTools(this.service.toolRegistry, { metadataService });
5971
+ registerMetadataTools(this.service.toolRegistry, { metadataService, protocol: protocolService });
5805
5972
  ctx.logger.info("[AI] Built-in metadata tools registered");
5806
5973
  const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
5807
5974
  for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
@@ -5996,7 +6163,7 @@ var AIServicePlugin = class {
5996
6163
  }
5997
6164
  if (typeof settings.registerAction === "function") {
5998
6165
  settings.registerAction("ai", "test_embedder", async ({ values, payload }) => {
5999
- const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6166
+ const overrides = extractOverrides(payload);
6000
6167
  const merged = { ...values ?? {}, ...overrides };
6001
6168
  const provider = String(merged.embedder_provider ?? "none");
6002
6169
  if (provider === "none") {
@@ -6041,10 +6208,33 @@ var AIServicePlugin = class {
6041
6208
  }
6042
6209
  if (typeof settings.registerAction === "function") {
6043
6210
  settings.registerAction("ai", "test", async ({ values, payload }) => {
6044
- const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6211
+ const overrides = extractOverrides(payload);
6045
6212
  const merged = { ...values ?? {}, ...overrides };
6046
6213
  const provider = String(merged.provider ?? "memory");
6047
6214
  if (provider === "memory") {
6215
+ const liveName = this.service?.adapterName ?? "";
6216
+ if (this.service && liveName && liveName !== "memory") {
6217
+ const started2 = Date.now();
6218
+ try {
6219
+ const result = await this.service.chat(
6220
+ [{ role: "user", content: "ping" }],
6221
+ { maxTokens: 8 }
6222
+ );
6223
+ const latency = Date.now() - started2;
6224
+ const preview = String(result?.text ?? "").slice(0, 60);
6225
+ return {
6226
+ ok: true,
6227
+ severity: "info",
6228
+ message: `Env-configured adapter "${liveName}" responded in ${latency}ms${preview ? ` \u2014 "${preview}"` : ""}.`
6229
+ };
6230
+ } catch (err) {
6231
+ return {
6232
+ ok: false,
6233
+ severity: "error",
6234
+ message: `Env-configured adapter "${liveName}" request failed: ${err?.message ?? String(err)}`
6235
+ };
6236
+ }
6237
+ }
6048
6238
  return {
6049
6239
  ok: true,
6050
6240
  severity: "warning",
@@ -6061,7 +6251,7 @@ var AIServicePlugin = class {
6061
6251
  return {
6062
6252
  ok: false,
6063
6253
  severity: "error",
6064
- message: `Could not build adapter for provider=${provider}. Check API key and that the provider SDK package is installed.`
6254
+ message: `Could not build adapter for provider=${provider}. Check API key (or the corresponding env var) and that the provider SDK package is installed.`
6065
6255
  };
6066
6256
  }
6067
6257
  const started = Date.now();
@@ -6078,6 +6268,15 @@ var AIServicePlugin = class {
6078
6268
  message: `${built.description} responded in ${latency}ms${preview ? ` \u2014 "${preview}"` : ""}.`
6079
6269
  };
6080
6270
  } catch (err) {
6271
+ const isGwAuth = err?.name === "GatewayAuthenticationError";
6272
+ const keyWasProvided = provider === "gateway" && String(merged.gateway_api_key ?? process.env.AI_GATEWAY_API_KEY ?? "").trim().length > 0;
6273
+ if (isGwAuth && keyWasProvided) {
6274
+ return {
6275
+ ok: false,
6276
+ severity: "error",
6277
+ 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.`
6278
+ };
6279
+ }
6081
6280
  return {
6082
6281
  ok: false,
6083
6282
  severity: "error",
@@ -6092,6 +6291,14 @@ var AIServicePlugin = class {
6092
6291
  this.service = void 0;
6093
6292
  }
6094
6293
  };
6294
+ function extractOverrides(payload) {
6295
+ if (!payload || typeof payload !== "object") return {};
6296
+ const p = payload;
6297
+ if (p.values && typeof p.values === "object" && p.values !== null) {
6298
+ return p.values;
6299
+ }
6300
+ return p;
6301
+ }
6095
6302
 
6096
6303
  // src/index.ts
6097
6304
  init_data_tools();