Wesichain

Simple Graph Tutorial

Build a basic RAG graph with separate retrieval and generation nodes.

What You’ll Build

A two-node graph that:

  1. Retrieves relevant documents based on query
  2. Generates an answer using the retrieved context

The Code

// Run: cargo run -p wesichain-graph --example simple_retrieval_graph

use async_trait::async_trait;
use futures::stream::{self, StreamExt};
use serde::{Deserialize, Serialize};
use wesichain_core::{Runnable, StreamEvent, WesichainError};
use wesichain_graph::{GraphBuilder, GraphError, GraphState, StateSchema, StateUpdate};

#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
struct RagState {
    query: String,
    docs: Vec<String>,
    answer: Option<String>,
}

impl StateSchema for RagState {
    type Update = Self;

    fn apply(current: &Self, update: Self::Update) -> Self {
        let query = if update.query.is_empty() {
            current.query.clone()
        } else {
            update.query
        };

        let mut docs = current.docs.clone();
        docs.extend(update.docs);

        let answer = if update.answer.is_some() {
            update.answer
        } else {
            current.answer.clone()
        };

        Self { query, docs, answer }
    }
}

struct Retriever;

#[async_trait]
impl Runnable<GraphState<RagState>, StateUpdate<RagState>> for Retriever {
    async fn invoke(
        &self,
        input: GraphState<RagState>,
    ) -> Result<StateUpdate<RagState>, WesichainError> {
        let docs = vec![
            format!("doc for '{}'", input.data.query),
            "doc: wesichain is rust-native".to_string(),
        ];
        Ok(StateUpdate::new(RagState {
            query: input.data.query,
            docs,
            answer: None,
        }))
    }

    fn stream(
        &self,
        _input: GraphState<RagState>,
    ) -> futures::stream::BoxStream<'_, Result<StreamEvent, WesichainError>> {
        stream::empty().boxed()
    }
}

struct Generator;

#[async_trait]
impl Runnable<GraphState<RagState>, StateUpdate<RagState>> for Generator {
    async fn invoke(
        &self,
        input: GraphState<RagState>,
    ) -> Result<StateUpdate<RagState>, WesichainError> {
        let answer = format!(
            "Answer based on {} docs: {}",
            input.data.docs.len(),
            input.data.docs.join(" | ")
        );
        Ok(StateUpdate::new(RagState {
            query: String::new(),
            docs: Vec::new(),
            answer: Some(answer),
        }))
    }

    fn stream(
        &self,
        _input: GraphState<RagState>,
    ) -> futures::stream::BoxStream<'_, Result<StreamEvent, WesichainError>> {
        stream::empty().boxed()
    }
}

#[tokio::main]
async fn main() -> Result<(), GraphError> {
    let graph = GraphBuilder::new()
        .add_node("retriever", Retriever)
        .add_node("llm", Generator)
        .add_edge("retriever", "llm")
        .set_entry("retriever")
        .build();

    let state = GraphState::new(RagState {
        query: "wesichain".to_string(),
        ..RagState::default()
    });
    let out = graph.invoke_graph(state).await?;
    println!("{}", out.data.answer.unwrap_or_default());
    Ok(())
}

Key Concepts

State Schema

Define your application state with StateSchema:

#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
struct RagState {
    query: String,
    docs: Vec<String>,
    answer: Option<String>,
}

impl StateSchema for RagState {
    type Update = Self;

    fn apply(current: &Self, update: Self::Update) -> Self {
        // Define how state updates combine
        Self {
            query: if update.query.is_empty() { ... } else { ... },
            docs: { ... },
            answer: update.answer.or_else(|| current.answer.clone()),
        }
    }
}

Graph Builder

Chain methods to construct your graph:

let graph = GraphBuilder::new()
    .add_node("retriever", Retriever)
    .add_node("llm", Generator)
    .add_edge("retriever", "llm")     // Sequential flow
    .set_entry("retriever")            // Starting node
    .build();

Running the Graph

let state = GraphState::new(RagState {
    query: "wesichain".to_string(),
    ..RagState::default()
});

let result = graph.invoke_graph(state).await?;
println!("Answer: {:?}", result.data.answer);

Run It

cargo run -p wesichain-graph --example simple_retrieval_graph

Next Steps

Updated Edit on GitHub