Skip to content

Commit 1df67dc

Browse files
committed
docs: cleanup readme
Signed-off-by: Greg Clark <grclark@nvidia.com>
1 parent d298420 commit 1df67dc

1 file changed

Lines changed: 99 additions & 106 deletions

File tree

‎crates/libsy/README.md‎

Lines changed: 99 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,63 @@
11
# libsy — Switchyard-Lib
22

3-
A lightweight library for multi-LLM agent optimization, with **routing** as the first case. An
4-
algorithm decides — statefully, using more than the request — which model(s) to call and how, and
5-
never performs the call itself ("ask, don't call"), so it owns no HTTP/provider SDK and embeds cleanly
6-
in a proxy, gateway, or agent runtime.
3+
A lightweight library for multi-LLM agent optimization, with **routing** as the first case.
4+
The library is provider agnostic and gives the user control of the llm api calls.
75

8-
Two audiences:
6+
## Example
97

10-
- **Integrators** — build a target set, pick an algorithm, run requests through a
11-
[`MultiLlmOrchestrator`](#the-orchestrator--multillmorchestrator).
12-
- **Algorithm developers** — add strategies by [implementing `OrchAlgo`](#the-orchalgo-trait).
8+
Build a target set, pick an algorithm, run a request:
139

14-
Narrative design: [`DESIGN.md`](DESIGN.md).
15-
16-
## Concepts
10+
```rust
11+
use libsy::llm_class::LlmClassifierOrchAlgo;
12+
use libsy::{LlmRequest, LlmTarget, LlmTargetSet, MultiLlmOrchestrator, OrchestratorRequest};
13+
use std::sync::Arc;
1714

18-
| Type | Role |
19-
|------|------|
20-
| `OrchAlgo` | The **algorithm**: runs once per request, makes model calls, returns a decision trace + response. |
21-
| `LlmTarget` | A **target**: a `name` the algorithm routes by, the provider `model` id, and an optional `LlmClient`. |
22-
| `LlmTargetSet` | The set of targets an algorithm routes among. |
23-
| `LlmClient` | The one piece of I/O libsy doesn't own — a target's real model call over *your* transport. |
24-
| `MultiLlmOrchestrator` | Drives one algorithm; the integrator's entry point. |
25-
| `DecisionTrace` | Why the algorithm chose as it did — read uniformly, downcast when the algo is known. |
15+
// Targets the algorithm routes among, each backed by your LlmClient (see below).
16+
let client = Arc::new(MyClient { /* .. */ }) as Arc<dyn LlmClient>;
17+
let t = |name: &str, model: &str| LlmTarget {
18+
name: name.into(), model: model.into(), llm_client: Some(client.clone()),
19+
};
2620

27-
Per request: `MultiLlmOrchestrator` → `OrchAlgo::process_request` → one or more `LlmTarget::call`
28-
(each **served** by an `LlmClient`, or **offloaded** to you) → `(trace, response)`.
21+
let algo = Arc::new(LlmClassifierOrchAlgo::new(
22+
"classifier", "strong", "weak", 0.5,
23+
LlmTargetSet::new(vec![
24+
t("classifier", "openai/gpt-4o-mini"),
25+
t("strong", "openai/gpt-4o"),
26+
t("weak", "openai/gpt-4o-mini"),
27+
]),
28+
));
29+
let orch = MultiLlmOrchestrator::new(algo);
2930

30-
---
31+
let req = OrchestratorRequest {
32+
llm_request: LlmRequest { model_name: "auto".into(), prompt: "explain tail latency".into() },
33+
raw_request: None, metadata: None,
34+
};
35+
let (trace, response) = orch.orchestrate_direct(req).await?; // one call in, trace + response out
36+
println!("routed to {}", trace.last().unwrap().model_decision());
37+
```
3138

32-
# For integrators
39+
Runnable: [`examples/research_agent.rs`](examples/research_agent.rs).
3340

34-
## The orchestrator — `MultiLlmOrchestrator`
41+
## Requests & responses
3542

3643
```rust
37-
impl MultiLlmOrchestrator {
38-
pub fn new(algo: Arc<dyn OrchAlgo>) -> Self; // the algorithm owns its target set
39-
40-
// libsy holds every client: makes the calls, returns (trace, response).
41-
// Errors up front if any target is client-less.
42-
pub async fn orchestrate_direct(&self, request: OrchestratorRequest)
43-
-> Result<(Vec<Arc<dyn DecisionTrace>>, OrchestratorResponse), Box<dyn Error + Send + Sync>>;
44-
45-
// "Ask, don't call": client-less targets stream back as `CallLlm` promises you
46-
// fulfill, then a final `ReturnToAgent`.
47-
pub fn orchestrate(&self, request: OrchestratorRequest)
48-
-> impl Stream<Item = OrchestratorStepResult>;
44+
pub struct OrchestratorRequest {
45+
pub llm_request: LlmRequest, // normalized: { model_name, prompt }
46+
pub raw_request: Option<serde_json::Value>, // original provider body, forwarded verbatim if present
47+
pub metadata: Option<Metadata>, // correlation: session / agent / task / correlation_id / extra
48+
}
4949

50-
pub async fn process_signals(&self, signals: AgentSysSignals) // out-of-band events
51-
-> Result<(), Box<dyn Error + Send + Sync>>;
50+
pub struct OrchestratorResponse {
51+
pub llm_response: LlmResponse, // normalized: { completion, raw_response? }
52+
pub metadata: Option<Metadata>,
5253
}
5354
```
5455

55-
One instance is cheap to share (`Arc<dyn OrchAlgo>` inside, no lock) and serves requests from many
56-
threads in parallel.
56+
## Building a target set / using a client
5757

58-
## Building a target set
59-
60-
Implement `LlmClient` over your transport, wrap each model as an `LlmTarget`, collect into an
61-
`LlmTargetSet`:
58+
An `LlmTarget` pairs a routing `name` with a provider `model` id and an optional `LlmClient`.
59+
Although `libsy` provides a `SwitchyardClient` and a reference implementation, `LlmClient`
60+
is designed to be implemented by the user.
6261

6362
```rust
6463
struct MyClient { /* http client, base url, key */ }
@@ -72,40 +71,42 @@ impl LlmClient for MyClient {
7271
Ok(OrchestratorResponse { llm_response: LlmResponse { completion, raw_response: None }, metadata: None })
7372
}
7473
}
75-
76-
// `name` is the routing label; `model` is the provider id the client calls. They can differ.
77-
let client = Arc::new(MyClient { /* .. */ }) as Arc<dyn LlmClient>;
78-
let target = |name: &str, model: &str| LlmTarget {
79-
name: name.into(), model: model.into(),
80-
llm_client: Some(client.clone()), // `None` -> offloaded (streaming mode)
81-
};
82-
let targets = LlmTargetSet::new(vec![
83-
target("classifier", "openai/gpt-4o-mini"),
84-
target("strong", "openai/gpt-4o"),
85-
target("weak", "openai/gpt-4o-mini"),
86-
]);
8774
```
8875

89-
## Running a request
76+
`name` is the label an algorithm routes by; `model` is the id the client calls — they can differ
77+
(`"strong"` -> `"openai/gpt-4o"`) or coincide. A target with `llm_client: None` is **offloaded**
78+
instead of served (streaming, below). The planned optional `SwitchyardClient` will provide an
79+
`LlmClient` that maps in Switchyard's OpenAI / Anthropic / Responses translations, so you needn't
80+
hand-roll one.
81+
82+
## The orchestrator — `MultiLlmOrchestrator`
9083

91-
`orchestrate_direct` when libsy holds the clients — one call in, trace + response out. Runnable:
92-
[`examples/research_agent.rs`](examples/research_agent.rs).
84+
The entry point. Cheap to share (`Arc<dyn OrchAlgo>` inside, no lock) and safe to drive from many
85+
threads at once:
9386

9487
```rust
95-
use libsy::llm_class::LlmClassifierOrchAlgo;
88+
impl MultiLlmOrchestrator {
89+
pub fn new(algo: Arc<dyn OrchAlgo>) -> Self; // the algorithm owns its target set
9690

97-
let algo = Arc::new(LlmClassifierOrchAlgo::new("classifier", "strong", "weak", 0.5, targets));
98-
let orch = MultiLlmOrchestrator::new(algo);
91+
// libsy holds every client: makes the calls, returns (trace, response).
92+
// Errors up front if any target is client-less.
93+
pub async fn orchestrate_direct(&self, request: OrchestratorRequest)
94+
-> Result<(Vec<Arc<dyn DecisionTrace>>, OrchestratorResponse), Box<dyn Error + Send + Sync>>;
9995

100-
let request = OrchestratorRequest {
101-
llm_request: LlmRequest { model_name: "auto".into(), prompt: "explain tail latency".into() },
102-
raw_request: None, metadata: None,
103-
};
104-
let (trace, response) = orch.orchestrate_direct(request).await?;
105-
println!("routed to {}", trace.last().unwrap().model_decision());
96+
// "Ask, don't call": client-less targets stream back as `CallLlm` promises you
97+
// fulfill, then a final `ReturnToAgent`.
98+
pub fn orchestrate(&self, request: OrchestratorRequest)
99+
-> impl Stream<Item = OrchestratorStepResult>;
100+
101+
pub async fn process_signals(&self, signals: AgentSysSignals) // out-of-band events
102+
-> Result<(), Box<dyn Error + Send + Sync>>;
103+
}
106104
```
107105

108-
`orchestrate` when your targets are client-less (`llm_client: None`) and *you* own the calls. Runnable:
106+
## Streaming — you own the model calls (`orchestrate`)
107+
108+
Build the targets client-less (`llm_client: None`) and every `target.call` is offloaded: `orchestrate`
109+
yields `CallLlm` promises you fulfill with your own transport, then a final `ReturnToAgent`. Runnable:
109110
[`examples/research_agent_core.rs`](examples/research_agent_core.rs).
110111

111112
```rust
@@ -122,21 +123,10 @@ while let Some(step) = stream.next().await {
122123
}
123124
```
124125

125-
## `SwitchyardClient` (planned, optional feature)
126-
127-
An optional-feature `LlmClient` that maps in Switchyard's provider translations (OpenAI / Anthropic /
128-
Responses) and calls an upstream endpoint for you — so integrators needn't hand-roll one while the
129-
default build ships no transport. The [`demo/libsy-proxy`](../../demo/libsy-proxy) binary hand-rolls it
130-
today.
131-
132-
---
133-
134-
# For algorithm developers
135-
136-
## The `OrchAlgo` trait
126+
## Building an algorithm (`OrchAlgo`)
137127

138-
`process_request` runs once per request and makes as many `LlmTarget::call`s as it needs (a router one,
139-
a classifier two, an ensemble many):
128+
Implement `OrchAlgo` to add a strategy. `process_request` runs once per request and makes as many
129+
`LlmTarget::call`s as it needs.
140130

141131
```rust
142132
#[async_trait]
@@ -151,32 +141,21 @@ pub trait OrchAlgo: Send + Sync {
151141
}
152142
```
153143

154-
Give it a `new(config.., target_set)` constructor; integrators `Arc`-wrap it for
155-
`MultiLlmOrchestrator::new`. There is no builder.
156-
157-
## Targets and decisions
144+
Give it a `new(config.., target_set)` constructor and `Arc`-wrap it for `MultiLlmOrchestrator::new` —
145+
there is no builder. A `target.call` serves the call if the target has a client, else offloads it via
146+
`ctx` — invisible to the algorithm. Attach a `DecisionTrace` to each call so a consumer can see *why*
147+
(it's a trait object, read uniformly, downcast when the algo is known):
158148

159149
```rust
160-
pub struct LlmTarget { pub name: String, pub model: String, pub llm_client: Option<Arc<dyn LlmClient>> }
161-
162150
pub trait DecisionTrace: Send + Sync {
163151
fn model_decision(&self) -> &str; // the model chosen
164152
fn reasoning(&self) -> Option<&str>; // human-readable "why"
165-
fn as_any(&self) -> &dyn std::any::Any; // downcast when the algo is known
153+
fn as_any(&self) -> &dyn std::any::Any; // downcast to the concrete decision
166154
}
167155
```
168156

169-
`LlmTarget::call(ctx, request, decision)` serves the call if the target has a client, else offloads it
170-
via `ctx`'s channel — invisible to the algorithm. The target stamps its `model` onto
171-
`request.llm_request.model_name`, so an algorithm routes by the logical `name` and the client still
172-
hits the concrete model. `DecisionTrace` is a trait object (not a generic), so a consumer reads any
173-
algorithm's decision uniformly.
174-
175-
## Implementing a router (LLM classifier, minimized)
176-
177-
Two `target.call`s in one `process_request`: classify, then route. (Full version in
178-
[`src/llm_class.rs`](src/llm_class.rs); random router in [`src/rand.rs`](src/rand.rs), ensemble in
179-
[`src/ensemble.rs`](src/ensemble.rs).)
157+
Example — the LLM classifier (classify, then route; full version in
158+
[`src/llm_class.rs`](src/llm_class.rs)):
180159

181160
```rust
182161
#[async_trait]
@@ -200,12 +179,26 @@ impl OrchAlgo for LlmClassifierOrchAlgo {
200179
}
201180
```
202181

203-
## Reference algorithms
182+
## Explore
183+
184+
**Reference algorithms** — implementations to read and route with:
185+
186+
- [`src/rand.rs`](src/rand.rs) — `RandomOrchAlgo`: uniform random over the set (one call).
187+
- [`src/llm_class.rs`](src/llm_class.rs) — `LlmClassifierOrchAlgo`: classify, then route strong/weak;
188+
fail open to strong.
189+
- [`src/ensemble.rs`](src/ensemble.rs) — `EnsembleOrchAlgo`: stateful — fan out to candidates, judge
190+
the best, commit to the winner after N exploration turns
191+
192+
**Runnable examples** (`cargo run -p libsy --example <name>`):
193+
194+
- [`examples/research_agent.rs`](examples/research_agent.rs) — client-backed targets, `orchestrate_direct`
195+
(libsy makes the calls).
196+
- [`examples/research_agent_core.rs`](examples/research_agent_core.rs) — client-less targets,
197+
`orchestrate` stream (the agent makes the calls).
204198

205-
- **`RandomOrchAlgo`** — uniform random over the set (one call).
206-
- **`LlmClassifierOrchAlgo`** — classify, then route strong/weak; fail open to strong.
207-
- **`EnsembleOrchAlgo`** — stateful: fan out to candidates, judge the best, commit after N exploration
208-
turns. State sits behind a `Mutex` over just its own fields.
199+
**Demo proxy** — [`demo/libsy-proxy`](../../demo/libsy-proxy): a real HTTP proxy where switchyard's
200+
crates serve the OpenAI / Anthropic / Responses APIs and translate formats, while *all* routing is
201+
libsy's classifier calling upstream through switchyard's backend.
209202

210203
## Not yet built
211204

0 commit comments

Comments
 (0)