improve api docs (#172)

This commit is contained in:
Logan
2025-09-09 22:12:25 -06:00
committed by GitHub
parent 050f1bba25
commit bcb04b005c
7 changed files with 372 additions and 24 deletions
+46
View File
@@ -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;
+6 -1
View File
@@ -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",
+45
View File
@@ -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
View File
@@ -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
View File
@@ -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}`);