From edfeedaaeec4ee134c7ff8a63802f3baf5e87abe Mon Sep 17 00:00:00 2001 From: Superlog app Date: Sat, 13 Jun 2026 09:19:18 +0000 Subject: [PATCH] [superlog] Resolve domain names to UUID before creating annotations --- packages/ai/src/ai/tools/annotations.ts | 16 ++++++-- .../ai/src/ai/tools/utils/context.test.ts | 39 +++++++++++++++++++ packages/ai/src/ai/tools/utils/context.ts | 26 ++++++++++--- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/ai/tools/annotations.ts b/packages/ai/src/ai/tools/annotations.ts index 096f40461..57ca48864 100644 --- a/packages/ai/src/ai/tools/annotations.ts +++ b/packages/ai/src/ai/tools/annotations.ts @@ -1,7 +1,12 @@ import { tool } from "ai"; import dayjs from "dayjs"; import { z } from "zod"; -import { callRPCProcedure, createToolLogger, getAppContext } from "./utils"; +import { + callRPCProcedure, + createToolLogger, + getAppContext, + resolveToolWebsite, +} from "./utils"; const logger = createToolLogger("Annotations Tools"); @@ -95,10 +100,11 @@ export function createAnnotationTools() { execute: async ({ websiteId, chartType, chartContext }, options) => { const context = getAppContext(options); try { + const resolved = resolveToolWebsite(context, websiteId); const result = await callRPCProcedure( "annotations", "list", - { websiteId, chartType, chartContext }, + { websiteId: resolved.websiteId, chartType, chartContext }, context ); return { @@ -141,6 +147,8 @@ export function createAnnotationTools() { ) => { const context = getAppContext(options); try { + const resolved = resolveToolWebsite(context, websiteId); + if (!confirmed) { const dateRangePreview = `${chartContext.dateRange.start_date} to ${chartContext.dateRange.end_date} (${chartContext.dateRange.granularity})`; @@ -149,7 +157,7 @@ export function createAnnotationTools() { message: "Please review the annotation details below and confirm if you want to create it:", annotation: { - websiteId, + websiteId: resolved.websiteId, chartType, dateRange: dateRangePreview, annotationType, @@ -170,7 +178,7 @@ export function createAnnotationTools() { "annotations", "create", { - websiteId, + websiteId: resolved.websiteId, chartType, chartContext, annotationType, diff --git a/packages/ai/src/ai/tools/utils/context.test.ts b/packages/ai/src/ai/tools/utils/context.test.ts index 92096cdea..6a8a34b65 100644 --- a/packages/ai/src/ai/tools/utils/context.test.ts +++ b/packages/ai/src/ai/tools/utils/context.test.ts @@ -86,4 +86,43 @@ describe("resolveToolWebsite", () => { expect(() => resolveToolWebsite(ctx)).toThrow(/multiple websites/); }); + + it("resolves a domain name to the matching website id in the accessible set", () => { + const ctx = makeCtx({ + accessibleWebsites: [ + { id: "web_a", domain: "app.example.com", name: null, isPublic: null, createdAt: null }, + ], + }); + + expect(resolveToolWebsite(ctx, "app.example.com")).toEqual({ + websiteId: "web_a", + domain: "app.example.com", + }); + }); + + it("resolves a domain name to the context websiteId when accessibleWebsites is absent", () => { + const ctx = makeCtx({ + websiteId: "web_ctx", + websiteDomain: "ctx.com", + }); + + expect(resolveToolWebsite(ctx, "ctx.com")).toEqual({ + websiteId: "web_ctx", + domain: "ctx.com", + }); + }); + + it("still rejects a domain name that does not match any accessible website or the context domain", () => { + const ctx = makeCtx({ + accessibleWebsites: [ + { id: "web_a", domain: "a.com", name: null, isPublic: null, createdAt: null }, + ], + websiteId: "web_ctx", + websiteDomain: "ctx.com", + }); + + expect(() => resolveToolWebsite(ctx, "unknown.com")).toThrow( + /not in this workspace/ + ); + }); }); diff --git a/packages/ai/src/ai/tools/utils/context.ts b/packages/ai/src/ai/tools/utils/context.ts index 48b155ecb..8901e75be 100644 --- a/packages/ai/src/ai/tools/utils/context.ts +++ b/packages/ai/src/ai/tools/utils/context.ts @@ -27,15 +27,29 @@ export function resolveToolWebsite( (id === ctx.websiteId ? ctx.websiteDomain : undefined); if (inputWebsiteId) { - const isAccessible = + // Fast path: exact UUID match in the accessible set or the context default + const isAccessibleById = accessible.some((w) => w.id === inputWebsiteId) || inputWebsiteId === ctx.websiteId; - if (!isAccessible) { - throw new Error( - `Website "${inputWebsiteId}" is not in this workspace. Call list_websites to see available websites.` - ); + if (isAccessibleById) { + return { websiteId: inputWebsiteId, domain: domainFor(inputWebsiteId) }; } - return { websiteId: inputWebsiteId, domain: domainFor(inputWebsiteId) }; + + // Slow path: the model may have passed a domain name instead of an id. + // Check the accessible list first, then the single-site context default. + const byDomain = accessible.find( + (w) => w.domain != null && w.domain === inputWebsiteId + ); + if (byDomain) { + return { websiteId: byDomain.id, domain: byDomain.domain ?? undefined }; + } + if (inputWebsiteId === ctx.websiteDomain && ctx.websiteId) { + return { websiteId: ctx.websiteId, domain: ctx.websiteDomain }; + } + + throw new Error( + `Website "${inputWebsiteId}" is not in this workspace. Call list_websites to see available websites.` + ); } const fallbackId = ctx.defaultWebsiteId ?? ctx.websiteId;