immersive2/server/services/langchainTools.js
Michael Mainguy 4ca98cf980 Add LangChain model wrappers and enhance diagram AI tools
- Migrate to LangChain for model abstraction (@langchain/anthropic, @langchain/ollama)
- Add custom ChatCloudflare class for Cloudflare Workers AI
- Simplify API routes using unified LangChain interface
- Add session preferences API for storing user settings
- Add connection label preference (ask user once, remember for session)
- Add shape modification support (change entity shapes via AI)
- Add template setter to DiagramObject for shape changes
- Improve entity inference with fuzzy matching
- Map colors to 16 toolbox palette colors
- Limit conversation history to last 6 messages
- Fix model switching to accept display names

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:17:15 -06:00

224 lines
9.4 KiB
JavaScript

/**
* LangChain Tool Definitions with Zod Schemas
*
* Single source of truth for all diagram AI tools.
* Uses Zod for type-safe schema definitions.
* Can be exported to Claude or OpenAI format.
*/
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
// Position schema (reusable) - from camera/user perspective
const positionSchema = z.object({
x: z.number().describe("Left (-) / Right (+) position from camera view"),
y: z.number().describe("Down (-) / Up (+) position (0 = floor, 1.5 = eye level)"),
z: z.number().describe("Backward (-) / Forward (+) position from camera view")
}).optional().describe("3D position from camera perspective. Example: (0, 1.5, 2) = directly in front at eye level");
// Scale schema - can be number or object, values are in METERS
const scaleSchema = z.union([
z.number().describe("Uniform size in meters (e.g., 1 = 1 meter cube, 0.5 = 50cm cube)"),
z.object({
x: z.number().describe("Width in meters"),
y: z.number().describe("Height in meters"),
z: z.number().describe("Depth in meters")
}).describe("Size as {x: width, y: height, z: depth} in meters. Example: {x: 1, y: 0.1, z: 1} = 1m wide, 10cm tall, 1m deep")
]).optional().describe("Size in METERS. Use {x, y, z} for different width/height/depth.");
// Rotation schema - can be number or object
const rotationSchema = z.union([
z.number().describe("Y-axis rotation in degrees (e.g., 90 = turn right 90°, -90 = turn left 90°)"),
z.object({
x: z.number().describe("Pitch in degrees"),
y: z.number().describe("Yaw in degrees"),
z: z.number().describe("Roll in degrees")
}).describe("Full 3D rotation in degrees as {x, y, z}")
]).optional().describe("Rotation in degrees. Use a number for Y-axis rotation or {x, y, z} for full 3D rotation.");
// Tool definitions with Zod schemas
export const toolSchemas = {
create_entity: {
name: "create_entity",
description: "Create a 3D shape in the diagram. Use this to add new elements like boxes, spheres, cylinders, etc.",
schema: z.object({
shape: z.enum(["box", "sphere", "cylinder", "cone", "plane", "person"])
.describe("The type of 3D shape to create"),
color: z.string().optional()
.describe("Color name (red, blue, green, etc.) or hex code (#ff0000)"),
text: z.string().optional()
.describe("Text label to display on or near the entity"),
position: positionSchema
})
},
connect_entities: {
name: "connect_entities",
description: "Draw a connection line between two entities. Check useDefaultLabels preference first - if not set, ask user if they want default labels on connections.",
schema: z.object({
from: z.string().describe("ID or label of the source entity"),
to: z.string().describe("ID or label of the target entity"),
label: z.string().optional().describe("Optional label for the connection. If omitted, default label 'X to Y' is used based on useDefaultLabels preference."),
color: z.string().optional().describe("Color of the connection line")
})
},
list_entities: {
name: "list_entities",
description: "List all entities currently in the diagram. Use this to see what exists before connecting or modifying.",
schema: z.object({})
},
remove_entity: {
name: "remove_entity",
description: "Remove an entity from the diagram by its ID or label.",
schema: z.object({
target: z.string().describe("ID or label of the entity to remove")
})
},
modify_entity: {
name: "modify_entity",
description: "CALL THIS TOOL to modify an entity. You MUST call this - describing changes does nothing. Use for: resize, move, rename, recolor, rotate, change shape.",
schema: z.object({
target: z.string().describe("Label or ID of entity to modify (e.g., 'CDN', 'Server')"),
color: z.string().optional().describe("New color hex code from toolbox palette"),
text: z.string().optional()
.describe("New label text. Use empty string \"\" to remove the label."),
shape: z.enum(["box", "sphere", "cylinder", "cone", "plane", "person"]).optional()
.describe("New shape for the entity"),
position: positionSchema,
scale: scaleSchema,
rotation: rotationSchema
})
},
modify_connection: {
name: "modify_connection",
description: "Modify a connection's label or color. Connections can be identified by their label or by specifying the from/to entities.",
schema: z.object({
target: z.string().optional()
.describe("Label of the connection to modify, or use from/to to identify it"),
from: z.string().optional()
.describe("ID or label of the source entity (alternative to target)"),
to: z.string().optional()
.describe("ID or label of the destination entity (alternative to target)"),
text: z.string().optional()
.describe("New label text for the connection. Use empty string \"\" to remove the label."),
color: z.string().optional()
.describe("New color for the connection")
})
},
clear_diagram: {
name: "clear_diagram",
description: "DESTRUCTIVE: Permanently delete ALL entities from the diagram and clear the session. This cannot be undone. IMPORTANT: Before calling this tool, you MUST first ask the user to confirm. Only call this tool with confirmed=true AFTER the user explicitly confirms.",
schema: z.object({
confirmed: z.boolean()
.describe("Must be true to execute. Only set to true after user has explicitly confirmed the deletion.")
})
},
get_camera_position: {
name: "get_camera_position",
description: "Get the current camera/viewer position and orientation in the 3D scene. Use this to understand where the user is looking and to position new entities relative to their view.",
schema: z.object({})
},
list_models: {
name: "list_models",
description: "List all available AI models that can be used for this conversation.",
schema: z.object({})
},
get_current_model: {
name: "get_current_model",
description: "Get information about the currently active AI model.",
schema: z.object({})
},
set_model: {
name: "set_model",
description: "Change the AI model. Use the model name from list_models.",
schema: z.object({
model_id: z.string()
.describe("Model name like 'Claude Opus 4', 'Hermes 2 Pro (CF)', 'Mistral Small 3.1 (CF)', etc.")
})
},
clear_conversation: {
name: "clear_conversation",
description: "Clear the conversation history to start fresh. This preserves the diagram entities but clears chat history.",
schema: z.object({})
},
search_wikipedia: {
name: "search_wikipedia",
description: "Search Wikipedia for information about a topic. Use this to research concepts, architectures, technologies, or anything else that would help create more accurate and detailed diagrams.",
schema: z.object({
query: z.string()
.describe("The topic or concept to search for (e.g., 'microservices architecture', 'neural network', 'kubernetes')")
})
},
set_connection_label_preference: {
name: "set_connection_label_preference",
description: "Set whether connections should have default labels. Call this after asking the user their preference.",
schema: z.object({
use_default_labels: z.boolean()
.describe("true = create labels like 'Server to Database', false = no labels on connections")
})
},
get_connection_label_preference: {
name: "get_connection_label_preference",
description: "Check if the user has set a preference for connection labels. Returns the preference or null if not set.",
schema: z.object({})
}
};
/**
* Convert tool schema to Claude/Anthropic format
* @param {object} toolDef - Tool definition with name, description, and Zod schema
* @returns {object} Tool in Claude format
*/
function toClaudeFormat(toolDef) {
return {
name: toolDef.name,
description: toolDef.description,
input_schema: zodToJsonSchema(toolDef.schema, { target: "openApi3" })
};
}
/**
* Convert tool schema to OpenAI/Ollama/Cloudflare format
* @param {object} toolDef - Tool definition with name, description, and Zod schema
* @returns {object} Tool in OpenAI function format
*/
function toOpenAIFormat(toolDef) {
return {
type: "function",
function: {
name: toolDef.name,
description: toolDef.description,
parameters: zodToJsonSchema(toolDef.schema, { target: "openApi3" })
}
};
}
// Export tools in different formats
export const claudeTools = Object.values(toolSchemas).map(toClaudeFormat);
export const openAITools = Object.values(toolSchemas).map(toOpenAIFormat);
// For backwards compatibility - alias
export const ollamaTools = openAITools;
export const cloudflareTools = openAITools;
export default {
toolSchemas,
claudeTools,
openAITools,
ollamaTools,
cloudflareTools
};