Skip to main content

Add your description here

Project description

Orchid flow

A lightweight, async-first workflow orchestration (minimal lang-graph alternative) library for LLM applications. orchid_flow simplifies building multi-step agent workflows with type-safe state management, flexible routing, and built-in observability.

Features

  • Async-First: Built on asyncio for high-performance concurrent execution
  • Type-Safe: Leverages Pydantic for configuration and state validation
  • Flexible Routing: Static transitions, conditional routing, and parallel execution
  • Worker Support: Offload CPU-bound nodes to background processes
  • Pluggable Storage: In-memory, Redis, or custom context stores
  • FastAPI Integration: Quick REST API generation for workflows
  • Observability: Built-in callback system for logging and monitoring

Installation

pip install orchid_flow

Quick Start

import asyncio
from pydantic import BaseModel
from typing import Optional

from  orchid_flow import Workflow, Node, NodeContext, AgentRequest, AgentResp, UIOutput, UserInput


class CounterState(BaseModel):
    count: int = 0


async def increment(ctx: NodeContext) -> AgentResp:
    state = ctx.state
    state.count += 1

    return AgentResp(
        ui_output=UIOutput(text=f"Count: {state.count}")
    )


workflow = Workflow(
    name="counter",
    initial_state=CounterState(),
    entry_node="start",
    nodes=[
        Node(name="start", func=increment, output_node=None)
    ]
)


async def main():
    result = await workflow.run(
        AgentRequest(
            conversation_id="session-123",
            user_input=UserInput(text="increment")
        )
    )
    print(result.ui_output.text)  # Count: 1


asyncio.run(main())

Core Concepts

Nodes

Nodes are async functions that receive a NodeContext. Return AgentResp to respond to the user, or None to proceed silently to the next node.

async def my_node(ctx: NodeContext) -> AgentResp | None:
    user_text = ctx.user_input.text
    user_text = ctx.user_input.text

    if user_text:
        return AgentResp(ui_output=UIOutput(text=f"You said: {user_text}"))
    return None
  • Each node can get a config with pydantic type
  • Nodes can also be sync functions that run in a separate worker process
from pydantic import BaseModel


class MyConfig(BaseModel):
    max_retries: int = 3
    timeout: float = 30.0


def sync_node(ctx: NodeContext, config: MyConfig) -> AgentResp | None:
    ctx.add_log("info", f"Max retries: {config.max_retries}")
    ...

Routing

The output_node parameter supports multiple routing strategies:

Type Behavior
str Direct transition to named node
list[str] Parallel execution of multiple nodes
Callable Dynamic routing based on context
def router(ctx: NodeContext) -> str:
    if ctx.state.intent == "refund":
        return ["refund_node","send_email"]
    return "general_node"

# all following nodes are valid:
Node(name="classify", func=classify, output_node="process")
Node(name="classify", func=classify, output_node=["send_email", "update_db"])
Node(name="classify", func=classify, output_node=router)

State

Define conversation state with Pydantic models. State persists across turns within a conversation.

from  orchid_flow.context import ConversationState


class SupportState(ConversationState):
    intent: Optional[str] = None
    attempt_count: int = 0
    customer_id: Optional[str] = None


support_workflow = Workflow(
    name="support",
    initial_state=SupportState(),
    ...
)


async def handle_request(ctx: NodeContext) -> AgentResp:
    state = ctx.state
    state.attempt_count += 1
    ...

Callbacks

Observe workflow lifecycle with callbacks for logging, metrics, or side effects:

from  orchid_flow import Callback, CallbackEvent


async def log_start(ctx: NodeContext, e: CallbackEvent):
    print(f"[START] {ctx.node_name}")

async def log_end(ctx: NodeContext, e: CallbackEvent):
    elapsed = e.data["elapsed_ms"]
    print(f"[END] {ctx.node_name} - {elapsed:.2f}ms")

async def log_error(ctx: NodeContext, e: CallbackEvent):
    print(f"[ERROR] {ctx.node_name}: {e.error}")

async def on_state_change(ctx: NodeContext, e: CallbackEvent):
    print(f"[STATE_CHANGE] {ctx.node_name}: {e.data}")

workflow = Workflow(
    ...,
    callbacks=[
        Callback(on="node_start", fn=log_start),
        Callback(on="node_end", fn=log_end),
        Callback(on="node_err", fn=log_error),
        Callback(on="set_state", fn=on_state_change),
    ]
)

Complete Example: Customer Support Bot

import asyncio
from pydantic import BaseModel
from typing import Optional

from  orchid_flow import (
    Workflow, Node, NodeContext, Callback, CallbackEvent,
    AgentRequest, AgentResp, UIOutput, UserInput
)
from  orchid_flow.context import ConversationState


class SupportState(ConversationState):
    intent: Optional[str] = None
    attempt_count: int = 0
    customer_id: Optional[str] = None


class RefundConfig(BaseModel):
    max_refund_amount: float = 50.0
    require_reason: bool = True


async def classify_intent(ctx: NodeContext) -> AgentResp:
    state = ctx.state
    user_text = ctx.user_input.text or ""

    if "refund" in user_text.lower():
        state.intent = "refund"
    elif "human" in user_text.lower():
        state.intent = "human_agent"
    else:
        state.intent = "general"

    return None


def intent_router(ctx: NodeContext) -> str:
    state = ctx.state
    if state.intent == "refund":
        return "refund_node"
    elif state.intent == "human_agent":
        return "transfer_node"
    return "general_node"


def handle_refund(ctx: NodeContext, config: RefundConfig) -> AgentResp:
    ctx.add_log("info", "Processing refund request")

    return AgentResp(
        ui_output=UIOutput(
            text=f"Processing refund (Limit: ${config.max_refund_amount}). Confirm?",
            answer_type="select",
            select_options=["Yes, Refund", "Cancel"],
        )
    )


async def transfer_to_human(ctx: NodeContext) -> AgentResp:
    return AgentResp(
        ui_output=UIOutput(text="Transferring to human agent...")
    )


async def handle_general(ctx: NodeContext) -> AgentResp:
    return AgentResp(
        ui_output=UIOutput(text="How can I help you today?")
    )


async def on_set_state(ctx: NodeContext, e: CallbackEvent):
    print(f"State updated: {e.data}")


support_workflow = Workflow(
    name="customer_support",
    initial_state=SupportState(),
    entry_node="entry_point",
    callbacks=[
        Callback(on="set_state", fn=on_set_state),
    ],
    nodes=[
        Node(
            name="entry_point",
            func=classify_intent,
            output_node=intent_router,
            run_in_worker=False,
        ),
        Node(
            name="refund_node",
            func=handle_refund,
            config=RefundConfig(max_refund_amount=100.0),
            output_node=None,
            run_in_worker=True,
        ),
        Node(
            name="transfer_node",
            func=transfer_to_human,
            output_node=None,
        ),
        Node(
            name="general_node",
            func=handle_general,
            output_node=None,
        ),
    ],
)


app: FastAPI = make_fastapi_app(
    workflows=[support_workflow],
)

Workflow

Parameter Type Description
name str Workflow identifier
initial_state BaseModel Initial state instance
entry_node str Starting node name
nodes list[Node] Node definitions
callbacks list[Callback] Event handlers (optional)
n_worker int Worker processes (default: 4)
node_timeout float Node timeout in seconds (optional)
context_store ContextStore Context persistence (optional)

Node

Parameter Type Description
name str Node identifier
func Callable Node function (async or sync)
config BaseModel Config injected as second argument
output_node str | list | Callable Next node(s) or router
run_in_worker bool Run in background process

NodeContext

Property/Method Description
conversation_id Unique conversation identifier
node_name Current node name
node_counter Execution counter
conversation_history List of conversation turns
user_input Current user input
state Conversation state (typed)
logs List of log entries
add_log(level, message) Add a log entry

Types

Type Description
AgentRequest Incoming request with conversation_id and user_input
AgentResp Response with ui_output
UserInput User input with text, select, number, date
UIOutput UI output with text, answer_type, select_options
Turn Conversation turn (user/bot)
Log Log entry with level and message

Callback Events

Event Description
node_start Fired when a node begins execution
node_end Fired when a node completes successfully
node_err Fired when a node raises an exception
set_state Fired when state is modified

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

orchid_flow-0.1.3.tar.gz (13.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

orchid_flow-0.1.3-py3-none-any.whl (13.6 kB view details)

Uploaded Python 3

File details

Details for the file orchid_flow-0.1.3.tar.gz.

File metadata

  • Download URL: orchid_flow-0.1.3.tar.gz
  • Upload date:
  • Size: 13.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.8

File hashes

Hashes for orchid_flow-0.1.3.tar.gz
Algorithm Hash digest
SHA256 2785fe7af4938cde78e8a32d7f5669736b2afa9bbac0818db4bd3c8a1b7942d9
MD5 a2397c32805dd21c79532351c1bff1c0
BLAKE2b-256 086309f2af68d79d11c7a441184ee1966260fd905d3118e7c13a324b0292cb4e

See more details on using hashes here.

File details

Details for the file orchid_flow-0.1.3-py3-none-any.whl.

File metadata

File hashes

Hashes for orchid_flow-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 a5bf9cb4b9c0aa50078d2e5b680bae49b06b60a8d68939d040a8c7b19867dd7a
MD5 262c294dfbf505f48a1544a07b587688
BLAKE2b-256 72b2f6486a034f662aa99af6d6b7cdf93f8a4ed9f6b949a8fc9a845e63709949

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page