diff --git a/packages/core/src/middleware/state.ts b/packages/core/src/middleware/state.ts index 315c971..8f01132 100644 --- a/packages/core/src/middleware/state.ts +++ b/packages/core/src/middleware/state.ts @@ -90,6 +90,52 @@ type CreateState = { type InitFunc = (input: Input) => State; +/** + * Creates a stateful middleware that adds state management capabilities to workflows. + * + * The stateful middleware allows workflows to maintain persistent state across handler executions, + * with support for snapshots and resuming workflow execution from saved states. + * + * @typeParam State - The type of state object to maintain + * @typeParam Input - The type of input used to initialize the state (defaults to void) + * @typeParam Context - The workflow context type (defaults to WorkflowContext) + * + * @param init - Optional initialization function that creates the initial state from input + * @returns A middleware object with state management capabilities + * + * @example + * ```typescript + * import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; + * import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; + * + * // Define your state type + * type MyState = { + * counter: number; + * messages: string[]; + * }; + * + * // Create the stateful middleware + * const stateful = createStatefulMiddleware(); + * const workflow = stateful.withState(createWorkflow()); + * + * // Use state in handlers + * workflow.handle([inputEvent], async (context, event) => { + * const { state, sendEvent } = context; + * state.counter += 1; + * state.messages.push(`Processed: ${event.data}`); + * sendEvent(outputEvent.with({ count: state.counter })); + * }); + * + * // Initialize with state + * const { sendEvent, snapshot } = workflow.createContext({ + * counter: 0, + * messages: [] + * }); + * ``` + * + * @category Middleware + * @public + */ export function createStatefulMiddleware< State, Input = void, diff --git a/packages/core/src/middleware/trace-events.ts b/packages/core/src/middleware/trace-events.ts index d13ee0d..07eb139 100644 --- a/packages/core/src/middleware/trace-events.ts +++ b/packages/core/src/middleware/trace-events.ts @@ -81,6 +81,50 @@ export type WithTraceEventsOptions = { plugins?: TracePlugin[]; }; +/** + * Adds tracing capabilities to a workflow by wrapping handlers with trace plugins. + * + * This middleware enables comprehensive tracing and monitoring of workflow execution, + * allowing you to attach plugins that can observe, measure, and instrument handler execution. + * + * @typeParam WorkflowLike - The workflow type to enhance with tracing + * + * @param workflow - The workflow instance to add tracing to + * @param options - Configuration object containing trace plugins + * @returns The workflow enhanced with tracing capabilities + * + * @example + * ```typescript + * import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; + * import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events"; + * + * // Define events + * const startEvent = workflowEvent(); + * const processEvent = workflowEvent(); + * + * // Create a simple timing plugin + * const timingPlugin = (handler) => async (...args) => { + * const start = Date.now(); + * try { + * return await handler(...args); + * } finally { + * console.log(`Handler took ${Date.now() - start}ms`); + * } + * }; + * + * // Apply tracing to workflow + * const workflow = withTraceEvents(createWorkflow(), { + * plugins: [timingPlugin] + * }); + * + * workflow.handle([startEvent], (context) => { + * context.sendEvent(processEvent.with("data")); + * }); + * ``` + * + * @category Middleware + * @public + */ export function withTraceEvents< WorkflowLike extends { handle< diff --git a/packages/core/src/middleware/trace-events/create-handler-decorator.ts b/packages/core/src/middleware/trace-events/create-handler-decorator.ts index 282c9db..040d737 100644 --- a/packages/core/src/middleware/trace-events/create-handler-decorator.ts +++ b/packages/core/src/middleware/trace-events/create-handler-decorator.ts @@ -26,6 +26,56 @@ export const decoratorRegistry = new Map< } >(); +/** + * Creates a handler decorator that can instrument workflow handlers with custom behavior. + * + * Handler decorators allow you to wrap workflow handlers with additional functionality + * such as logging, timing, error handling, or state management. They provide hooks + * that run before and after handler execution. + * + * @typeParam Metadata - The type of metadata to track for each handler + * + * @param config - Configuration object for the decorator + * @param config.debugLabel - Optional debug label for identifying the decorator + * @param config.getInitialValue - Function that returns initial metadata value + * @param config.onBeforeHandler - Hook that runs before handler execution + * @param config.onAfterHandler - Hook that runs after handler execution + * @returns A decorator function that can be used as a trace plugin + * + * @example + * ```typescript + * import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; + * import { + * withTraceEvents, + * createHandlerDecorator + * } from "@llamaindex/workflow-core/middleware/trace-events"; + * + * // Create a timing decorator + * type TimingMetadata = { startTime: number | null }; + * const timingDecorator = createHandlerDecorator({ + * debugLabel: "timing", + * getInitialValue: () => ({ startTime: null }), + * onBeforeHandler: (handler, context, metadata) => async (...args) => { + * metadata.startTime = Date.now(); + * try { + * return await handler(...args); + * } finally { + * const duration = Date.now() - (metadata.startTime ?? 0); + * console.log(`Handler executed in ${duration}ms`); + * } + * }, + * onAfterHandler: () => ({ startTime: null }) + * }); + * + * // Use the decorator + * const workflow = withTraceEvents(createWorkflow(), { + * plugins: [timingDecorator] + * }); + * ``` + * + * @category Middleware + * @public + */ export function createHandlerDecorator(config: { debugLabel?: string; getInitialValue: () => Metadata; diff --git a/packages/core/typedoc.json b/packages/core/typedoc.json index ff1e76c..f3f776f 100644 --- a/packages/core/typedoc.json +++ b/packages/core/typedoc.json @@ -1,6 +1,11 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["./src/core/index.ts"], + "entryPoints": [ + "./src/core/index.ts", + "./src/middleware/state.ts", + "./src/middleware/trace-events.ts", + "./src/middleware/trace-events/create-handler-decorator.ts" + ], "out": "../../docs/workflows/api-reference", "plugin": ["typedoc-plugin-markdown"], "outputFileStrategy": "members", diff --git a/packages/viz/src/drawing.ts b/packages/viz/src/drawing.ts index f52608a..ffc55bd 100644 --- a/packages/viz/src/drawing.ts +++ b/packages/viz/src/drawing.ts @@ -13,6 +13,51 @@ export type WithDrawingWorkflow = { draw(container: HTMLElement, options?: DrawingOptions): void; }; +/** + * Adds visualization capabilities to a workflow, enabling it to be rendered as an interactive graph. + * + * This function enhances a workflow with drawing functionality, allowing you to visualize + * the flow of events and handlers as an interactive graph in the browser using Sigma.js. + * + * @typeParam WorkflowLike - The workflow type to enhance with drawing capabilities + * + * @param workflow - The workflow instance to add visualization to + * @returns The workflow enhanced with a `draw` method for rendering graphs + * + * @example + * ```typescript + * import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; + * import { withDrawing } from "@llamaindex/workflow-viz"; + * + * // Define events with debug labels for better visualization + * const startEvent = workflowEvent({ debugLabel: "start" }); + * const processEvent = workflowEvent({ debugLabel: "process" }); + * const endEvent = workflowEvent({ debugLabel: "end" }); + * + * // Create workflow with drawing capabilities + * const workflow = withDrawing(createWorkflow()); + * + * // Add handlers + * workflow.handle([startEvent], (context, event) => { + * return processEvent.with(`Processing: ${event.data}`); + * }); + * + * workflow.handle([processEvent], (context, event) => { + * return endEvent.with(`Completed: ${event.data}`); + * }); + * + * // Render the workflow graph + * const container = document.getElementById("workflow-container"); + * workflow.draw(container, { + * layout: "force", + * defaultEdgeColor: "#999", + * defaultNodeColor: "#333" + * }); + * ``` + * + * @category Visualization + * @public + */ export function withDrawing( workflow: WorkflowLike, ): WorkflowLike & WithDrawingWorkflow { diff --git a/scripts/add-frontmatter.js b/scripts/add-frontmatter.js index 095bcd0..499e7f3 100644 --- a/scripts/add-frontmatter.js +++ b/scripts/add-frontmatter.js @@ -13,13 +13,19 @@ function extractTitle(content) { if (!match) return "API Reference"; // Stop matching at <> or () characters and clean up - const title = match[1] + let title = match[1] .replace(/`/g, "") // Remove backticks .replace(/^([^<(]+)[<(].*$/, "$1") // Stop at < or ( characters .replace(/\\+$/, "") // Remove trailing backslashes .replace(/\\(.)/g, "$1") // Remove escape characters .trim(); // Remove trailing whitespace + // Remove TypeDoc prefixes (Class:, Type Alias:, Function:, etc.) + title = title.replace( + /^(Class|Type Alias|Function|Interface|Enum|Variable|Namespace):\s*/, + "", + ); + return title || "API Reference"; } @@ -118,26 +124,13 @@ function generateFrontmatter(filePath, content) { frontmatter.description = description; } - // Add specific metadata based on file type - switch (dirName) { - case "functions": - frontmatter.category = "Functions"; - frontmatter.sidebar_label = fileName; - break; - case "classes": - frontmatter.category = "Classes"; - frontmatter.sidebar_label = fileName; - break; - case "type-aliases": - frontmatter.category = "Types"; - frontmatter.sidebar_label = fileName; - break; - default: - if (fileName === "README") { - frontmatter.slug = "/api-reference"; - frontmatter.sidebar_position = 1; - } - break; + // Add specific metadata for special files + if (fileName === "README") { + frontmatter.slug = "/api-reference"; + frontmatter.sidebar_position = 1; + } else { + // For all other files, just use the filename as sidebar label + frontmatter.sidebar_label = fileName; } // Generate YAML frontmatter diff --git a/scripts/build-docs.js b/scripts/build-docs.js index 7fc2e25..2a3912f 100644 --- a/scripts/build-docs.js +++ b/scripts/build-docs.js @@ -7,6 +7,163 @@ import { addFrontmatter } from "./add-frontmatter.js"; const DOCS_DIR = "./docs/workflows/api-reference"; +/** + * Recursively finds all .mdx files in a directory + */ +async function findAllMdxFiles(dir, basePath = "") { + const files = []; + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = path.join(basePath, entry.name); + + if (entry.isDirectory()) { + const subFiles = await findAllMdxFiles(fullPath, relativePath); + files.push(...subFiles); + } else if (entry.isFile() && entry.name.endsWith(".mdx")) { + // Skip README files as they're just aggregation pages + if (entry.name === "README.mdx") { + continue; + } + + // Skip deprecated functions + if (entry.name === "getContext.mdx") { + continue; + } + + files.push({ + sourcePath: fullPath, + originalPath: relativePath, + fileName: entry.name, + }); + } + } + } catch (error) { + console.warn( + `⚠️ Warning: Could not read directory ${dir}: ${error.message}`, + ); + } + + return files; +} + +/** + * Flattens the nested folder structure created by TypeDoc + */ +async function flattenApiStructure() { + // Find all .mdx files recursively (except README files which we'll handle separately) + const allFiles = await findAllMdxFiles(DOCS_DIR); + + // Track used filenames to handle conflicts + const usedNames = new Set(); + + for (const fileInfo of allFiles) { + let targetFileName = fileInfo.fileName; + + // Handle name conflicts by prefixing with directory info if needed + if (usedNames.has(targetFileName)) { + // Extract meaningful prefix from the path + const pathParts = fileInfo.originalPath.split(path.sep); + const meaningfulParts = pathParts.filter( + (part) => + part !== "classes" && + part !== "functions" && + part !== "type-aliases" && + part !== "README.mdx", + ); + + if (meaningfulParts.length > 1) { + const prefix = meaningfulParts[meaningfulParts.length - 2]; // Use parent directory + targetFileName = `${prefix}-${fileInfo.fileName}`; + } + } + + usedNames.add(targetFileName); + + const targetPath = path.join(DOCS_DIR, targetFileName); + + try { + await fs.rename(fileInfo.sourcePath, targetPath); + console.log(`📋 Moved ${fileInfo.originalPath} → ${targetFileName}`); + } catch (error) { + console.warn( + `⚠️ Warning: Could not move ${fileInfo.originalPath}: ${error.message}`, + ); + } + } + + // Clean up unwanted files (README files and deprecated functions) + await cleanupUnwantedFiles(DOCS_DIR); + + // Remove all empty directories + await removeEmptyDirectories(DOCS_DIR); +} + +/** + * Recursively removes unwanted files (README files and deprecated functions) + */ +async function cleanupUnwantedFiles(dir) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively clean subdirectories + await cleanupUnwantedFiles(fullPath); + } else if (entry.isFile() && entry.name.endsWith(".mdx")) { + // Remove README files and deprecated functions + if (entry.name === "README.mdx" || entry.name === "getContext.mdx") { + await fs.rm(fullPath); + console.log( + `🗑️ Removed unwanted file: ${path.relative(DOCS_DIR, fullPath)}`, + ); + } + } + } + } catch (error) { + console.warn( + `⚠️ Warning: Could not clean directory ${dir}: ${error.message}`, + ); + } +} + +/** + * Recursively removes empty directories + */ +async function removeEmptyDirectories(dir) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + // First, recursively clean subdirectories + for (const entry of entries) { + if (entry.isDirectory()) { + const subDir = path.join(dir, entry.name); + await removeEmptyDirectories(subDir); + } + } + + // Then check if this directory is now empty (only contains meta.json or is completely empty) + const remainingEntries = await fs.readdir(dir); + const nonMetaFiles = remainingEntries.filter( + (name) => name !== "meta.json", + ); + + if (nonMetaFiles.length === 0 && dir !== DOCS_DIR) { + await fs.rmdir(dir); + console.log( + `🗑️ Removed empty directory: ${path.relative(DOCS_DIR, dir)}`, + ); + } + } catch (error) { + // Directory might not exist or might not be empty, that's ok + } +} + async function buildApiDocs() { console.log("🚀 Building API documentation..."); @@ -25,6 +182,10 @@ async function buildApiDocs() { stdio: "inherit", }); + // Flatten the folder structure + console.log("📁 Flattening folder structure..."); + await flattenApiStructure(); + // Add frontmatter to all generated files console.log("🎨 Adding frontmatter headers..."); await addFrontmatter(); @@ -45,8 +206,12 @@ async function buildApiDocs() { await fs.writeFile(metaPath, JSON.stringify(metaContent, null, 2)); } - // Remove generated README.mdx - await fs.rm(path.join(DOCS_DIR, "README.mdx")); + // Remove generated README.mdx if it exists + try { + await fs.rm(path.join(DOCS_DIR, "README.mdx")); + } catch (error) { + // File might not exist, that's ok + } console.log("✅ API documentation built successfully!"); console.log(`📁 Output location: ${DOCS_DIR}`);