Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
[workspace]
resolver = "2"
members = [
"crates/libsy",
"crates/libsy-examples",
"crates/switchyard-components",
"crates/switchyard-components-v2",
"crates/switchyard-components-v2-macros",
Expand Down
21 changes: 21 additions & 0 deletions crates/libsy-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "libsy-examples"
version = "0.1.0"
description = "Reference algorithms and runnable agents built on libsy"
publish = false
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
libsy = { path = "../libsy" }
async-trait = "0.1"
rand = "0.8"
futures = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
106 changes: 106 additions & 0 deletions crates/libsy-examples/examples/research_agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Minimal research agent using the [`Algorithm::run`] convenience.
//!
//! Every target owns an `LlmClient`, so the agent runs each request to completion with
//! [`Algorithm::run`]: it serves each offloaded call with the routed
//! target's `default_client` and returns the final response — no stream to drive. The
//! multi-step routing (classify -> route) happens inside the classifier algorithm; the
//! agent never sees it. To drive the step stream yourself instead, use
//! `Algorithm::run_stream`. Run with:
//! cargo run -p libsy --example research_agent

use std::error::Error;
use std::sync::Arc;

use async_trait::async_trait;
use libsy::{
Algorithm, Context, LlmClient, LlmRequest, LlmResponse, LlmTarget, LlmTargetSet, Request,
Response, RoutedRequest,
};
use libsy_examples::llm_class::LlmClassifierOrchAlgo;

const CLASSIFIER: &str = "classifier/model";
const STRONG: &str = "strong/model";
const WEAK: &str = "weak/model";

/// Stub transport. Real integrators implement `LlmClient` over their own HTTP.
struct StubClient;

#[async_trait]
impl LlmClient for StubClient {
async fn call(&self, routed: RoutedRequest) -> Result<Response, Box<dyn Error + Send + Sync>> {
// The model to call is the routed decision's selection, not the inbound name.
let model = routed.decision.selected_model().to_string();
println!(" -> model call: {model}");
// The classifier returns a score; other models return an answer.
let completion = if model == CLASSIFIER {
"0.9".to_string()
} else {
format!("answer from {model}")
};
Ok(Response {
llm_response: LlmResponse {
completion,
raw_response: None,
},
metadata: None,
})
}
}

fn targets() -> LlmTargetSet {
let client = Arc::new(StubClient) as Arc<dyn LlmClient>;
let target = |name: &str| LlmTarget {
semantic_name: name.to_string(),
llm_client: Some(client.clone()),
};
LlmTargetSet::new(vec![target(CLASSIFIER), target(STRONG), target(WEAK)])
}

struct ResearchAgent {
algo: Arc<dyn Algorithm>,
}

impl ResearchAgent {
/// Trivial plan: one lookup per question (stub).
fn plan(&self, question: &str) -> Vec<String> {
vec![format!("look up: {question}")]
}

async fn run(&self, question: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
let mut notes = Vec::new();
for step in self.plan(question) {
let request = Request {
llm_request: LlmRequest {
inbound_model_name: "auto".to_string(),
prompt: step,
},
raw_request: None,
metadata: None,
};

let (_trace, response) = self.algo.clone().run(Context::default(), request).await?;
notes.push(response.llm_response.completion);
}
Ok(notes.join("\n"))
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// Configure routing once: an LLM classifier over three named targets. Swapping
// in `RandomOrchAlgo` needs no change to the agent.
let algo: Arc<dyn Algorithm> = Arc::new(LlmClassifierOrchAlgo::new(
CLASSIFIER,
STRONG,
WEAK,
0.5,
targets(),
));

let agent = ResearchAgent { algo };
println!("{}", agent.run("what is switchyard?").await?);
Ok(())
}
118 changes: 118 additions & 0 deletions crates/libsy-examples/examples/research_agent_core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Research agent driving the raw `run` stream with **client-less** targets.
//!
//! With no client, every `driver.call_llm_target` is offloaded as a promise the orchestrator
//! surfaces as a `CallLlm` step. The agent makes the "real" model call itself and
//! fulfills the promise — this is the offload/streaming path ("ask, don't call").
//! The classifier's two steps show up as two `model call:` lines. Run with:
//! cargo run -p libsy --example research_agent_core

use std::error::Error;
use std::sync::Arc;

use libsy::{
Algorithm, Context, Decision, LlmRequest, LlmResponse, LlmTarget, LlmTargetSet, Request,
Response, Step,
};
use libsy_examples::llm_class::LlmClassifierOrchAlgo;
use tokio_stream::StreamExt;

const CLASSIFIER: &str = "classifier/model";
const STRONG: &str = "strong/model";
const WEAK: &str = "weak/model";

/// The "real" model call the agent makes to fulfill a promise. The core never
/// makes the call itself — it hands back a request and waits for the response.
/// The model to call is the routing decision's selection, read off the promise.
async fn call_model(model: &str) -> Response {
println!(" -> model call: {model}");
let completion = if model == CLASSIFIER {
"0.9".to_string()
} else {
format!("answer from {model}")
};
Response {
llm_response: LlmResponse {
completion,
raw_response: None,
},
metadata: None,
}
}

fn targets() -> LlmTargetSet {
// Client-less targets -> every call is offloaded via a promise.
let target = |name: &str| LlmTarget {
semantic_name: name.to_string(),
llm_client: None,
};
LlmTargetSet::new(vec![target(CLASSIFIER), target(STRONG), target(WEAK)])
}

struct ResearchAgent {
algo: Arc<dyn Algorithm>,
}

impl ResearchAgent {
/// Trivial plan: one lookup per question (stub).
fn plan(&self, question: &str) -> Vec<String> {
vec![format!("look up: {question}")]
}

async fn run(&mut self, question: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
let mut notes = Vec::new();
for step in self.plan(question) {
let request = Request {
llm_request: LlmRequest {
inbound_model_name: "auto".to_string(),
prompt: step,
},
raw_request: None,
metadata: None,
};
let stream = self.algo.clone().run_stream(Context::default(), request);
tokio::pin!(stream);
while let Some(update) = stream.next().await {
match update? {
Step::CallLlm(call) => {
// Perform the model call the algorithm asked for, then fulfill.
let response = call_model(call.get_decision()?.selected_model()).await;
call.respond(Ok(response))?;
}
// Decisions stream in as the algorithm makes them.
Step::Decision(decision) => print_decision(decision.as_ref()),
Step::ReturnToAgent(response) => {
notes.push(response.llm_response.completion);
}
}
}
}
Ok(notes.join("\n"))
}
}

/// Print one decision the algorithm recorded — uniform access via the trait.
fn print_decision(decision: &dyn Decision) {
println!(
" decision: {} ({})",
decision.selected_model(),
decision.reasoning().unwrap_or_default()
);
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let algo: Arc<dyn Algorithm> = Arc::new(LlmClassifierOrchAlgo::new(
CLASSIFIER,
STRONG,
WEAK,
0.5,
targets(),
));

let mut agent = ResearchAgent { algo };
println!("{}", agent.run("what is switchyard?").await?);
Ok(())
}
Loading
Loading