diff --git a/ts/packages/agents/excalidraw/package.json b/ts/packages/agents/excalidraw/package.json new file mode 100644 index 000000000..99d6cee4f --- /dev/null +++ b/ts/packages/agents/excalidraw/package.json @@ -0,0 +1,57 @@ +{ + "name": "excalidraw-agent", + "version": "0.0.1", + "private": true, + "description": "Excalidraw diagram generation agent - converts documents and descriptions into Excalidraw diagrams", + "homepage": "https://github.com/microsoft/TypeAgent#readme", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeAgent.git", + "directory": "ts/packages/agents/excalidraw" + }, + "license": "MIT", + "author": "Microsoft", + "type": "module", + "exports": { + "./agent/manifest": "./src/excalidrawManifest.json", + "./agent/handlers": "./dist/excalidrawActionHandler.js" + }, + "scripts": { + "asc": "asc -i ./src/excalidrawActionSchema.ts -o ./dist/excalidrawSchema.pas.json -t ExcalidrawAction", + "build": "concurrently npm:tsc npm:asc", + "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", + "prettier": "prettier --check . --ignore-path ../../../.prettierignore", + "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", + "tsc": "tsc -b" + }, + "dependencies": { + "@typeagent/agent-sdk": "workspace:*", + "aiclient": "workspace:*", + "telemetry": "workspace:*", + "typechat-utils": "workspace:*" + }, + "devDependencies": { + "@typeagent/action-schema-compiler": "workspace:*", + "concurrently": "^9.1.2", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "typescript": "~5.4.5" + }, + "fluidBuild": { + "tasks": { + "asc": { + "dependsOn": [ + "@typeagent/action-schema-compiler#tsc" + ], + "files": { + "inputGlobs": [ + "src/excalidrawActionSchema.ts" + ], + "outputGlobs": [ + "dist/excalidrawSchema.pas.json" + ] + } + } + } + } +} diff --git a/ts/packages/agents/excalidraw/src/excalidrawActionHandler.ts b/ts/packages/agents/excalidraw/src/excalidrawActionHandler.ts new file mode 100644 index 000000000..1a1aaa99d --- /dev/null +++ b/ts/packages/agents/excalidraw/src/excalidrawActionHandler.ts @@ -0,0 +1,655 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ActionContext, + AppAction, + AppAgent, + ActionResult, +} from "@typeagent/agent-sdk"; +import { + createActionResult, + createActionResultFromTextDisplay, + createActionResultFromError, +} from "@typeagent/agent-sdk/helpers/action"; +import { openai } from "aiclient"; +import { + CreateDiagramAction, + ExcalidrawAction, + ExportDiagramAction, +} from "./excalidrawActionSchema.js"; + +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; + +export function instantiate(): AppAgent { + return { + executeAction: executeExcalidrawAction, + }; +} + +type ExcalidrawActionContext = { + store: undefined; +}; + +async function executeExcalidrawAction( + action: AppAction, + context: ActionContext, +): Promise { + return handleExcalidrawAction(action as ExcalidrawAction, context); +} + +async function handleExcalidrawAction( + action: ExcalidrawAction, + context: ActionContext, +): Promise { + switch (action.actionName) { + case "createDiagram": + return handleCreateDiagram(action as CreateDiagramAction, context); + case "exportDiagram": + return handleExportDiagram(action as ExportDiagramAction); + default: + throw new Error(`Unknown action: ${(action as any).actionName}`); + } +} + +function getDefaultOutputDir(): string { + // Use Documents folder, works on Windows (%USERPROFILE%\Documents) and Unix (~Documents) + const documentsDir = path.join(os.homedir(), "Documents"); + if (!fs.existsSync(documentsDir)) { + fs.mkdirSync(documentsDir, { recursive: true }); + } + return documentsDir; +} + +function sanitizeFilename(name: string): string { + return name + .replace(/[<>:"/\\|?*]/g, "_") + .replace(/\s+/g, "_") + .substring(0, 100); +} + +function resolveOutputPath( + outputPath: string | undefined, + diagramTitle: string | undefined, +): string { + if (outputPath) { + // Ensure it has .excalidraw extension + if (!outputPath.endsWith(".excalidraw")) { + outputPath += ".excalidraw"; + } + return path.resolve(outputPath); + } + + const title = diagramTitle ?? "diagram"; + const filename = `${sanitizeFilename(title)}_${Date.now()}.excalidraw`; + return path.join(getDefaultOutputDir(), filename); +} + +function buildMermaidSystemPrompt(sourceType: string): string { + return `You are an expert at reading documents and extracting their structure as a complete Mermaid flowchart. + +Your ONLY output is a valid Mermaid flowchart — no explanations, no markdown fences, just the raw Mermaid syntax starting with "flowchart TD" or "flowchart LR". + +RULES: +- Capture EVERY node, relationship, and label present in the source — do not simplify or omit anything +- Use quoted labels on nodes and edges so spaces and special characters are safe: A["My Label"] +- Use --> for directed edges, -- label --> for labelled edges +- Use subgraph ... end to represent groups or layers +- Prefer top-down (TD) layout unless the content is clearly horizontal + +SOURCE CONTENT TYPE: ${sourceType} +- If "markdown": headings become top-level nodes, bullet points become child nodes, nested bullets become sub-children +- If "text": identify all named concepts and their relationships; model causality/dependency as directed edges +- If "visio-xml": faithfully reproduce the shapes and connectors from the XML +- If "mermaid": output it unchanged (it is already Mermaid) +- If "architecture": represent every component, layer, and data-flow arrow`; +} + +function buildExcalidrawSystemPrompt(): string { + return `You are a mechanical converter from Mermaid flowchart syntax to Excalidraw JSON. +You receive a complete Mermaid flowchart and must produce a valid Excalidraw JSON file that faithfully represents every node and edge. Do not omit anything. + +OUTPUT FORMAT: +Output ONLY valid JSON — no markdown fences, no explanation. + +Top-level structure: +{ + "type": "excalidraw", + "version": 2, + "source": "typeagent-excalidraw", + "elements": [...], + "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" }, + "files": {} +} + +ELEMENT TYPES: +- "rectangle" — regular nodes +- "diamond" — decision/condition nodes (Mermaid {braces}) +- "ellipse" — terminal/start/end nodes (Mermaid stadium or circle) +- "text" — bound label for a shape (containerId set) or standalone text +- "arrow" — directed edge + +ELEMENT STRUCTURE (every field required): +{ + "id": "", + "type": "", + "x": , "y": , "width": , "height": , + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, + "seed": , "version": 1, "versionNonce": , + "isDeleted": false, "boundElements": [], "updated": 1, + "link": null, "locked": false, "groupIds": [], "frameId": null, + "roundness": { "type": 3 } +} + +TEXT elements also need: + "text": "