import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage, } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { z } from "zod"; import { reactEditorTools, getEditorContext, } from "@reacteditor/plugin-ai/server"; import { shopifyFetch } from "@/editor/services/shopify/client"; import { GET_PRODUCTS_QUERY, GET_PRODUCT_QUERY, } from "@/editor/graphql/products"; import { GET_COLLECTIONS_QUERY, GET_COLLECTION_PRODUCTS_QUERY, } from "@/editor/graphql/collections"; type Body = { messages: UIMessage[]; editorContext?: Parameters[0]; route?: string; }; export async function POST(req: Request) { const { messages, editorContext } = (await req.json()) as Body; const generateImage = tool({ description: "Generate an image from a prompt and return its URL.", inputSchema: z.object({ prompt: z.string(), width: z.number().int().positive().optional(), height: z.number().int().positive().optional(), }), execute: async ({ width = 768, height = 768 }) => ({ url: `https://picsum.photos/${width}/${height}?random=${Math.floor( Math.random() * 1_000_000, )}`, }), }); const credentials = { domain: process.env.VITE_SHOPIFY_DOMAIN ?? process.env.SHOPIFY_DOMAIN ?? "mock.shop", token: process.env.VITE_SHOPIFY_STOREFRONT_ACCESS_TOKEN ?? process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN ?? "", }; const searchProducts = tool({ description: "Search the Shopify store for products. Returns up to `limit` products matching `query`.", inputSchema: z.object({ query: z.string().optional(), limit: z.number().int().min(1).max(20).optional(), }), execute: async ({ query, limit = 8 }) => { try { const res = await shopifyFetch({ query: GET_PRODUCTS_QUERY, variables: { first: limit, query: query ?? null, sortKey: query ? "RELEVANCE" : "BEST_SELLING", reverse: false, }, credentials, }); const products = (res.data?.products?.edges ?? []).map((e: any) => { const n = e.node; return { id: n.id, handle: n.handle, title: n.title, description: n.description, featuredImage: n.images?.edges?.[0]?.node ?? null, priceRange: n.priceRange, }; }); return { ok: true, products }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "fetch failed", }; } }, }); const getProductByHandle = tool({ description: "Fetch a single product by its handle.", inputSchema: z.object({ handle: z.string() }), execute: async ({ handle }) => { try { const res = await shopifyFetch({ query: GET_PRODUCT_QUERY, variables: { handle }, credentials, }); const p = res.data?.product ?? null; if (!p) return { ok: false, error: "not_found" }; return { ok: true, product: { id: p.id, handle: p.handle, title: p.title, description: p.description, featuredImage: p.images?.edges?.[0]?.node ?? null, priceRange: p.priceRange, }, }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "fetch failed", }; } }, }); const searchCollections = tool({ description: "List up to `limit` collections from the Shopify store.", inputSchema: z.object({ limit: z.number().int().min(1).max(20).optional(), }), execute: async ({ limit = 8 }) => { try { const res = await shopifyFetch({ query: GET_COLLECTIONS_QUERY, variables: { first: limit }, credentials, }); const collections = (res.data?.collections?.edges ?? []).map( (e: any) => { const n = e.node; return { id: n.id, handle: n.handle, title: n.title, description: n.description, image: n.image ?? null, }; }, ); return { ok: true, collections }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "fetch failed", }; } }, }); const getCollectionByHandle = tool({ description: "Fetch a single collection by its handle, including a small page of its products.", inputSchema: z.object({ handle: z.string(), limit: z.number().int().min(1).max(20).optional(), }), execute: async ({ handle, limit = 8 }) => { try { const res = await shopifyFetch({ query: GET_COLLECTION_PRODUCTS_QUERY, variables: { handle, first: limit, sortKey: "BEST_SELLING", reverse: false, }, credentials, }); const c = res.data?.collection ?? null; if (!c) return { ok: false, error: "not_found" }; return { ok: true, collection: { id: c.id, handle: c.handle, title: c.title, description: c.description, image: c.image ?? null, products: (c.products?.edges ?? []).map((e: any) => ({ id: e.node.id, handle: e.node.handle, title: e.node.title, featuredImage: e.node.images?.edges?.[0]?.node ?? null, priceRange: e.node.priceRange, })), }, }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "fetch failed", }; } }, }); const result = streamText({ model: anthropic("claude-sonnet-4-5"), system: getEditorContext(editorContext), messages: await convertToModelMessages(messages), tools: { ...reactEditorTools, generateImage, searchProducts, getProductByHandle, searchCollections, getCollectionByHandle, }, stopWhen: stepCountIs(50), }); return result.toUIMessageStreamResponse(); }