mirror of
https://github.com/run-llama/workflows-ts.git
synced 2026-06-30 21:57:58 -04:00
improve api docs (#172)
This commit is contained in:
@@ -90,6 +90,52 @@ type CreateState<State, Input, Context extends WorkflowContext> = {
|
||||
|
||||
type InitFunc<Input, State> = (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<MyState>();
|
||||
* 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,
|
||||
|
||||
@@ -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<string>();
|
||||
*
|
||||
* // 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<
|
||||
|
||||
@@ -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<TimingMetadata>({
|
||||
* 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<Metadata>(config: {
|
||||
debugLabel?: string;
|
||||
getInitialValue: () => Metadata;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string>({ debugLabel: "start" });
|
||||
* const processEvent = workflowEvent<string>({ debugLabel: "process" });
|
||||
* const endEvent = workflowEvent<string>({ 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<WorkflowLike extends Workflow>(
|
||||
workflow: WorkflowLike,
|
||||
): WorkflowLike & WithDrawingWorkflow {
|
||||
|
||||
+14
-21
@@ -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
|
||||
|
||||
+167
-2
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user