返回 presentation-ai
tools.ts
根目录 / src / ai / tools / presentation / tools.ts
1 import { tool } from "@langchain/core/tools";
2 import * as z from "zod";
3
4 import { LAYOUT_REFERENCE } from "@/lib/presentation/layout-catalog";
5 import { presentationAiThemePropertiesSchema } from "@/lib/presentation/theme-schema";
6 import { themes } from "@/lib/presentation/themes";
7 import { search_tool } from "../search";
8
9 const COMPONENT_INSTRUCTIONS = `Component instructions:
10 - Match component geometry to SECTION layout: vertical root images need horizontal/wide components, and left/right root images need vertical or compact components.
11 - Do not pair CYCLE with layout="vertical".
12 - Use compact text in dense visual components. SNAKE, CIRCULAR-GRID, CONNECTED-CIRCLES, and SLOPE items need very short labels.
13 - SLOPE items must use <H4> only and must not include <P>.
14 - Use <TITLE> only for the first slide, a newly created title slide, or an introduction slide.
15 - For most first/title slides, include <TITLE>, <CONTRIBUTOR />, and a supporting visual image. The contributor block self-populates with the creator name; omit it only when a stronger creative concept needs the space.
16 - Treat <LABEL>, <BLOCKQUOTE>, <QUOTE>, <CALLOUT>, and <CODE> as normal content blocks that can be used anywhere headings and paragraphs can be used, including inside COLUMNS. But don't overuse them.
17 - Use COLUMNS only for balanced lanes. Every column item must have parallel content, similar text length, and the same heading level; do not mix an H1-style item with H3/H4-style items in sibling columns.
18 - Keep columns visually balanced even when they include images, charts, infographics, or nested supported content.`;
19
20 // Schema for scope specification
21 const ScopeSchema = z
22 .enum(["all"])
23 .optional()
24 .describe(
25 "Scope of the action: 'all' for all slides. Defaults to 'all' if not specified. This property and slideIds property are mutually exclusive. If you provide both, the slideIds property will be ignored.",
26 );
27
28 const slideIdsSchema = z
29 .array(z.string())
30 .optional()
31 .describe(
32 "Specific slide ids to apply the action to. If provided, overrides scope. This property and scope property are mutually exclusive. If you provide both, this property will be ignored. So be very careful.",
33 );
34
35 const builtInThemeSchema = z.enum(
36 Object.keys(themes) as [keyof typeof themes, ...(keyof typeof themes)[]],
37 );
38
39 const edit_slide_properties = tool(
40 async (props) => {
41 const { slideIds: _slideIds, scope: _scope, ...rest } = props;
42 return `Updated ${Object.keys(rest).join(", ")} successfully to ${Object.values(rest).join(", ")}`;
43 },
44 {
45 name: "edit_slide_properties",
46 description: "You can use this tool to edit the properties of a slide",
47 schema: z.object({
48 scope: ScopeSchema,
49 slideIds: slideIdsSchema,
50 bgColor: z
51 .string()
52 .describe(
53 "The background color of the slide, use 'reset' to reset the background color",
54 )
55 .optional(),
56 alignment: z
57 .enum(["start", "center", "end", "reset"])
58 .describe("The content alignment of the slide")
59 .optional(),
60 layoutType: z
61 .enum(["left", "right", "vertical", "background", "reset"])
62 .describe(
63 "Determines where the accent / root image appears in the slide, left means the image is on the left, right means the image is on the right, vertical means the image is on the top, background means the image is the background of the slide",
64 )
65 .optional(),
66 width: z
67 .enum(["S", "M", "L", "reset"])
68 .describe("The width of the slide")
69 .optional(),
70 }),
71 },
72 );
73
74 const replace_image = tool(
75 async (props) => {
76 const { slideIds: _slideIds, scope: _scope, ...rest } = props;
77 if (rest.imageUrl) {
78 return `Image url replaced successfully`;
79 } else if (rest.imagePrompt) {
80 return `Image successfully generated from the given prompt`;
81 }
82 return `No image url or image prompt provided`;
83 },
84 {
85 name: "replace_image",
86 description:
87 "You can use this tool to replace the root image of a slide. If the user also asked for slide text, layout, or content changes, call create_slide or regenerate_slide first, wait for that content tool to complete, and only then call replace_image.",
88 schema: z.object({
89 slideIds: slideIdsSchema,
90 scope: ScopeSchema,
91 imageUrl: z
92 .string()
93 .describe("The URL of the image to replace")
94 .optional(),
95 imagePrompt: z
96 .string()
97 .describe(
98 "Image request for the replacement. Use a detailed descriptive prompt only when the selected image source is AI generation. For Unsplash, Pixabay, Google, web, or stock image search, use a short English keyword query with 2-5 concrete words, such as 'team collaboration' or 'solar panels roof'.",
99 )
100 .optional(),
101 imageSource: z
102 .enum(["ai", "stock", "gif"])
103 .describe(
104 "How the imagePrompt should be resolved. Use 'stock' for Unsplash/Pixabay/Google/web image search, 'gif' for animated GIFs, and 'ai' for detailed generated-image prompts.",
105 )
106 .optional(),
107 stockImageProvider: z
108 .enum(["unsplash", "pixabay", "google"])
109 .describe("Preferred stock provider when imageSource is 'stock'.")
110 .optional(),
111 }),
112 },
113 );
114
115 const change_theme = tool(
116 async (props) => {
117 const { theme } = props;
118 return `Theme changed successfully to ${theme}`;
119 },
120 {
121 name: "change_theme",
122 description:
123 "Apply an existing built-in presentation theme. Use create_custom_theme when the user asks for custom fonts, custom colors, brand styling, or a new theme.",
124 schema: z.object({
125 theme: builtInThemeSchema.describe("The built-in theme id to apply"),
126 }),
127 },
128 );
129
130 const create_custom_theme = tool(
131 async (props) => {
132 return `Custom theme "${props.themeData.name ?? "Custom theme"}" created and applied successfully`;
133 },
134 {
135 name: "create_custom_theme",
136 description:
137 "Create and apply a custom presentation theme. Use this for custom visual identity, brand styling, font changes, palettes, or backgrounds. Only provide colors, fonts, and background values that are relevant to the request; omitted values are kept from the current theme.",
138 schema: z.object({
139 isPublic: z
140 .boolean()
141 .optional()
142 .default(false)
143 .describe("Whether the new custom theme should be public"),
144 themeData: presentationAiThemePropertiesSchema.describe(
145 "Partial custom theme data. Only include colors, fonts, and background values. Use real, well-known font family names that fit the brand and requirement. Do not invent font names. smartLayout is the fill color for SVG layout elements such as pyramids, pie charts, staircases, cycles, timelines, and diagrams; choose it as a close companion or deliberate variant of primary, not as the cardBackground. cardBackground is the readable text container surface. Do not include animation, transitions, shadows, border radius, or masks.",
146 ),
147 }),
148 },
149 );
150
151 const update_custom_theme = tool(
152 async (props) => {
153 return `Custom theme "${props.themeData.name ?? "Custom theme"}" updated and applied successfully`;
154 },
155 {
156 name: "update_custom_theme",
157 description:
158 "Update the currently selected custom presentation theme and apply it. If the current theme is built-in, the app will create a new custom theme from this data instead. Only provide colors, fonts, and background values that should change; omitted values are kept from the current theme.",
159 schema: z.object({
160 isPublic: z
161 .boolean()
162 .optional()
163 .default(false)
164 .describe("Whether the custom theme should be public"),
165 themeData: presentationAiThemePropertiesSchema.describe(
166 "Partial replacement theme data. Only include colors, fonts, and background values. Use real, well-known font family names that fit the brand and requirement. Do not invent font names. smartLayout is the fill color for SVG layout elements such as pyramids, pie charts, staircases, cycles, timelines, and diagrams; choose it as a close companion or deliberate variant of primary, not as the cardBackground. cardBackground is the readable text container surface. Do not include animation, transitions, shadows, border radius, or masks.",
167 ),
168 }),
169 },
170 );
171
172 const REGENERATE_SLIDE_DESCRIPTION = `You are a presentation XML expert. Regenerate one or more existing slides from the user's request.
173
174 Your task is to return exactly two arrays: \`slideIds\` and \`slides\`, with the same length and same order so that \`slides[i]\` replaces \`slideIds[i]\`.
175
176 Use this tool for slide content, layout, text, chart, infographic, and structure changes. If the user also wants a new root image, regenerate the XML content here first; replace/generate the root image afterward with replace_image.
177
178 Return only valid XML strings in \`slides\`. Each item must be one <SECTION>...</SECTION> block, not a full <PRESENTATION>. Use only supported tags and attributes. Put headings, body, and layout content before any direct child root <IMG ... />. If a root image is included, it must be the final direct child of <SECTION>. Keep <IMG /> tags self-closing.
179
180 Preserve structure when the request is text-only. Keep the same SECTION layout, component type, item count when reasonable, root image placement, and existing image URLs unless the user asks to change them. When the user asks for a structural change, choose the component that fits the content shape: list for grouped points, sequence for process or progression, comparison for trade-offs or states, relationship for connected concepts, data for evidence, infographic for custom diagrams, and columns for balanced mixed-content lanes.
181
182 ${LAYOUT_REFERENCE}
183
184 ${COMPONENT_INSTRUCTIONS}
185
186 Use images deliberately. A direct child <IMG /> is the root slide image and must stay last. Use short English keyword queries for stock, web, Unsplash, Pixabay, Google, or GIF search. Use detailed visual prompts for AI image generation. Keep existing image urls exactly when the user did not request image regeneration.
187
188 Use icon attributes as search hints only. Each icon value must be exactly one broad lowercase English keyword with no spaces, punctuation, hyphens, underscores, or react-icons component names. Good examples: security, analytics, team, growth, upload, idea, automation, calendar, money, network, settings, document, message. Do not default to home unless the content is actually about home. For icon lists, use <ICONS variant="icon"> with DIV icon attributes, or <ICONS variant="image"> with DIV prompt attributes for generated item images. Use orientation="side" for visual beside text and orientation="top" for visual above text.
189
190 Do not choose background layouts or full-slide image backgrounds by default. Only use <SECTION layout="background"> when the user explicitly asks to make an image the slide background, or when preserving an existing/template structure that already uses it.
191
192 Use infographics when a process map, lifecycle, hierarchy, relationship diagram, matrix, framework, funnel, or cause-and-effect flow communicates better than a list, chart, or image. If the user explicitly asks for an infographic, diagram, process map, framework, or similar visual on a slide, include exactly one <INFOGRAPHIC> element on that slide. The infographic text must include only the information needed to generate the diagram: exact labels, entities, values, steps, sequence, relationships, the required visual orientation, and the takeaway. Do not include unrelated slide state. For <SECTION layout="vertical">, request a horizontal/landscape infographic because the infographic will sit in the wide content area. For <SECTION layout="left"> or <SECTION layout="right">, request a vertical/stacked infographic because it must fit beside the side root image. Limit layout-based infographic prompts to 5 or fewer visible items by merging lower-priority details. If INFOGRAPHIC is the main/root component, do not add another layout component; only simple headings or paragraphs may accompany it.
193
194 Mandatory rules:
195 - Include boolean-style attributes only when enabled; omit false attributes.
196 - Write compact real slide copy, not placeholders.
197 - Convert markdown into XML text content; do not include markdown markers inside headings or paragraphs.`;
198
199 const regenerate_slide = tool(
200 async (props) => {
201 const { slideIds: _slideIds } = props;
202 return `Slides regenerated successfully`;
203 },
204 {
205 name: "regenerate_slide",
206 description: REGENERATE_SLIDE_DESCRIPTION,
207 schema: z
208 .object({
209 slideIds: z
210 .array(z.string())
211 .min(1)
212 .describe(
213 "Array of slide ids to regenerate. Order must match the `slides` array.",
214 ),
215 slides: z
216 .array(z.string())
217 .min(1)
218 .describe(
219 "Array of XML <SECTION> strings. Each item is a single slide's content.",
220 ),
221 })
222 .superRefine((data, ctx) => {
223 if (data.slideIds.length !== data.slides.length) {
224 ctx.addIssue({
225 code: z.ZodIssueCode.custom,
226 message:
227 "`slideIds` and `slides` must have the same length and matching order.",
228 path: ["slides"],
229 });
230 }
231 }),
232 },
233 );
234
235 // Create new slides and insert them after a given slide id (if provided), else append
236 const create_slide = tool(
237 async (props) => {
238 const { afterSlideId: _afterSlideId, slides: _slides } = props as {
239 afterSlideId?: string;
240 slides: string[];
241 };
242 return `Slides created successfully`;
243 },
244 {
245 name: "create_slide",
246 description:
247 "Create one or more slides. Return an array of XML <SECTION> strings and optionally the slide id to insert after. Generate headings/body/layout content first; when adding a direct child root <IMG ... />, place it as the final child of <SECTION> so the content appears before root image generation.",
248 schema: z.object({
249 slides: z
250 .array(z.string())
251 .min(1)
252 .describe(
253 'Array of XML <SECTION> strings. Each item is a single slide\'s content. Supported default layouts are left, right, and vertical. Use layout="background" only when the user explicitly asks for a full-slide image background or when preserving an existing/template structure that already uses it. For the root image only provide the query and not url. Include an <INFOGRAPHIC> XML element inside SECTION when a custom visual explanation improves clarity. The element text must include labels, values, entities, sequence, relationships, takeaway, and required orientation, without unrelated slide state. For layout="vertical", request a horizontal/landscape infographic for the wide content area. For layout="left" or layout="right", request a vertical/stacked infographic for the narrow side-by-side content area. If INFOGRAPHIC is the main slide component, only simple headings or paragraphs may accompany it; do not add another layout component. For layout-based infographic prompts, cap visible items at 5 or fewer by combining lower-priority details.',
254 ),
255 afterSlideId: z
256 .string()
257 .optional()
258 .describe(
259 "Insert new slides immediately after this slide id. If omitted or not found, append to the end.",
260 ),
261 }),
262 },
263 );
264
265 // Delete slides by ids
266 const delete_slide = tool(
267 async (props) => {
268 const { slideIds: _slideIds } = props as { slideIds: string[] };
269 return `Slides deleted successfully`;
270 },
271 {
272 name: "delete_slide",
273 description: "Delete one or more slides by id.",
274 schema: z.object({
275 slideIds: z
276 .array(z.string())
277 .min(1)
278 .describe("Array of slide ids to delete."),
279 }),
280 },
281 );
282 // Export all tools as an array
283 export const presentationTools = [
284 edit_slide_properties,
285 replace_image,
286 change_theme,
287 create_custom_theme,
288 update_custom_theme,
289 regenerate_slide,
290 create_slide,
291 delete_slide,
292 search_tool,
293 ] as const;
294
295 export type PresentationTool =
296 | "edit_slide_properties"
297 | "replace_image"
298 | "change_theme"
299 | "create_custom_theme"
300 | "update_custom_theme"
301 | "regenerate_slide"
302 | "create_slide"
303 | "delete_slide"
304 | "exa_search_results_json";
305
305 lines TYPESCRIPT