Files
workflows-py/examples/visualization/resource_nodes_example.py
Adrian Lyjak e53c654b42 Add resource nodes to workflow graph export (#269)
* feat: add resource nodes to workflow graph export

Add support for visualizing resource dependencies in workflow graphs:

- Enhanced _Resource class with source location metadata (file, line,
  docstring, unique hash for deduplication)
- Added ResourceDefinition.type_annotation to capture the type from
  Annotated[T, Resource(...)]
- Added WorkflowGraphResourceNode protocol model with full metadata
- Added DrawWorkflowResourceNode class for intermediate representation
- Updated DrawWorkflowEdge to support edge labels (variable names)
- Modified extract_workflow_structure to extract and deduplicate
  resource nodes, connecting them to steps with labeled edges
- Updated Pyvis and Mermaid renderers to display resource nodes
  (hexagon shape, plum color) with metadata tooltips

* test: add tests and example for resource nodes visualization

- Add tests for resource node extraction in test_representation_utils.py
- Add tests for resource rendering in test_drawing.py (Mermaid + Pyvis)
- Add workflow_with_resources fixture with multiple resources
- Add runnable example (examples/visualization/resource_nodes_example.py)
  that generates both Mermaid and Pyvis output with resource nodes

* chore: add changeset for resource nodes feature

* fix: reverse resource edge direction to step -> resource

* refactor: unify DrawWorkflowNode and DrawWorkflowResourceNode

- Merge resource-specific fields into DrawWorkflowNode
- Add to_resource_response_model() for resource serialization
- Keep DrawWorkflowResourceNode as backwards compat alias
- Simplify type annotations in rendering code (remove unions)
- Remove isinstance checks since all nodes are now DrawWorkflowNode

* refactor: simplify _get_clean_node_id to use node_type prefix

* refactor: merge resource_nodes into nodes list

Unify resource nodes with the main nodes list instead of a separate
resource_nodes field. This simplifies the API and rendering code while
maintaining backwards compatibility through a property accessor.

- Remove resource_nodes field from DrawWorkflowGraph
- Add resource_nodes property for backwards compatibility
- Merge WorkflowGraphResourceNode fields into WorkflowGraphNode
- Simplify rendering loops to single iteration over nodes

* refactor: remove unnecessary backwards compatibility aliases

This is all new code in this PR - no need for backwards compat aliases.
Removes WorkflowGraphResourceNode, DrawWorkflowResourceNode,
to_resource_response_model, and resource_nodes property.

* perf: make resource metadata extraction lazy

Move expensive inspect operations (getfile, getsourcelines, getdoc)
and hash computation from Resource creation time to graph extraction
time. This avoids the overhead when resources are used at runtime
but visualization is not needed.

* test: remove unnecessary async from mermaid drawing tests

* consolidate types

* Split types

* feat: add description and schema fields to workflow graph nodes

- Add description field to WorkflowGraph (workflow class docstring)
- Add description field to WorkflowStepNode (step function docstring)
- Add event_schema field to WorkflowEventNode (Pydantic JSON schema)
- Rename WorkflowResourceNode.docstring to description for consistency
- Rename WorkflowGraphNodeEdges to WorkflowGraph

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* filter by node type

* Update add_resource_nodes.md

* remove resource hash

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-10 13:01:12 -05:00

261 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
Example demonstrating resource nodes in workflow graph visualization.
This example shows how resources (dependencies injected via Annotated types)
are rendered in both Mermaid and Pyvis diagrams.
Run this script to generate:
- workflow_with_resources.mermaid - A Mermaid diagram file
- workflow_with_resources.html - An interactive Pyvis HTML visualization
You can view the Mermaid diagram at https://mermaid.live/ by pasting the contents.
The HTML file can be opened directly in any web browser.
"""
import argparse
from typing import Annotated
from llama_index.utils.workflow import (
draw_all_possible_flows,
draw_all_possible_flows_mermaid,
)
from workflows import Workflow, step
from workflows.events import Event, StartEvent, StopEvent
from workflows.resource import Resource
# --- Mock resource types ---
class DatabaseClient:
"""A database client for persistent storage."""
def __init__(self, connection_string: str = "postgres://localhost/db"):
self.connection_string = connection_string
def query(self, sql: str) -> list:
"""Execute a SQL query."""
return []
class CacheClient:
"""A cache client for fast data retrieval."""
def __init__(self, host: str = "localhost", port: int = 6379):
self.host = host
self.port = port
def get(self, key: str) -> str | None:
"""Get a value from cache."""
return None
def set(self, key: str, value: str) -> None:
"""Set a value in cache."""
pass
class LLMClient:
"""A client for interacting with a large language model."""
def __init__(self, api_key: str = "sk-..."):
self.api_key = api_key
async def complete(self, prompt: str) -> str:
"""Generate a completion for the given prompt."""
return f"Response to: {prompt}"
# --- Resource factory functions ---
def get_database_client() -> DatabaseClient:
"""Factory function to create a database client.
This function creates a PostgreSQL database client configured
for the application's data storage needs.
"""
return DatabaseClient(connection_string="postgres://localhost/myapp")
def get_cache_client() -> CacheClient:
"""Factory function to create a Redis cache client.
Provides fast caching for frequently accessed data.
"""
return CacheClient(host="localhost", port=6379)
def get_llm_client() -> LLMClient:
"""Factory function to create an LLM client.
Creates a client for the language model API.
"""
return LLMClient(api_key="sk-example-key")
# --- Event types ---
class QueryProcessedEvent(Event):
"""Event emitted after processing a user query."""
query: str
cached: bool = False
class ContextRetrievedEvent(Event):
"""Event emitted after retrieving context from the database."""
context: str
class ResponseGeneratedEvent(Event):
"""Event emitted after generating an LLM response."""
response: str
# --- Workflow with resources ---
class RAGWorkflow(Workflow):
"""A RAG (Retrieval-Augmented Generation) workflow demonstrating resource usage.
This workflow shows how different steps can depend on shared resources
like database clients, cache clients, and LLM clients.
"""
@step
async def process_query(
self,
ev: StartEvent,
cache: Annotated[CacheClient, Resource(get_cache_client)],
) -> QueryProcessedEvent:
"""Process the incoming query, checking cache first."""
query = getattr(ev, "query", "default query")
# Check if query result is cached
cached_result = cache.get(f"query:{query}")
if cached_result:
return QueryProcessedEvent(query=query, cached=True)
return QueryProcessedEvent(query=query, cached=False)
@step
async def retrieve_context(
self,
ev: QueryProcessedEvent,
db: Annotated[DatabaseClient, Resource(get_database_client)],
cache: Annotated[CacheClient, Resource(get_cache_client)],
) -> ContextRetrievedEvent:
"""Retrieve relevant context from the database."""
if ev.cached:
context = "Cached context"
else:
# Query the database for relevant documents
results = db.query(
f"SELECT content FROM documents WHERE query = '{ev.query}'"
)
context = " ".join(str(r) for r in results) or "No context found"
# Cache the result
cache.set(f"context:{ev.query}", context)
return ContextRetrievedEvent(context=context)
@step
async def generate_response(
self,
ev: ContextRetrievedEvent,
llm: Annotated[LLMClient, Resource(get_llm_client)],
) -> ResponseGeneratedEvent:
"""Generate a response using the LLM with the retrieved context."""
prompt = f"Context: {ev.context}\n\nGenerate a response."
response = await llm.complete(prompt)
return ResponseGeneratedEvent(response=response)
@step
async def finalize_response(
self,
ev: ResponseGeneratedEvent,
cache: Annotated[CacheClient, Resource(get_cache_client)],
) -> StopEvent:
"""Finalize and cache the response."""
# Cache the final response
cache.set("last_response", ev.response)
return StopEvent(result=ev.response)
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate workflow visualizations with resource nodes"
)
parser.add_argument(
"--output-dir",
default=".",
help="Directory to save output files (default: current directory)",
)
parser.add_argument(
"--mermaid-only",
action="store_true",
help="Only generate Mermaid output (print to stdout)",
)
args = parser.parse_args()
# Create the workflow
workflow = RAGWorkflow()
print("=" * 60)
print("Workflow Graph Visualization with Resource Nodes")
print("=" * 60)
# Generate Mermaid diagram
mermaid_file = f"{args.output_dir}/workflow_with_resources.mermaid"
mermaid_output = draw_all_possible_flows_mermaid(
workflow,
filename="" if args.mermaid_only else mermaid_file,
)
print("\n--- Mermaid Diagram ---")
print(mermaid_output)
print()
if not args.mermaid_only:
print(f"Mermaid diagram saved to: {mermaid_file}")
# Generate Pyvis HTML
html_file = f"{args.output_dir}/workflow_with_resources.html"
draw_all_possible_flows(workflow, filename=html_file)
print(f"Interactive Pyvis diagram saved to: {html_file}")
print("\n--- Resource Nodes Summary ---")
print(
"""
The diagram shows:
- HEXAGON nodes (plum color): Resource dependencies
- DatabaseClient: Database connection via get_database_client()
- CacheClient: Cache connection via get_cache_client()
- LLMClient: LLM API client via get_llm_client()
- Edge labels on resource connections show the variable name used in the step
e.g., "db", "cache", "llm"
- Resources are deduplicated: CacheClient appears once even though
it's used by multiple steps (process_query, retrieve_context, finalize_response)
To view the Mermaid diagram:
1. Go to https://mermaid.live/
2. Paste the diagram content above
3. See the interactive visualization
To view the Pyvis diagram:
1. Open workflow_with_resources.html in a web browser
2. Hover over nodes to see metadata (type, getter, source location, docstring)
3. Drag nodes to rearrange the layout
"""
)
if __name__ == "__main__":
main()