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
4 changes: 2 additions & 2 deletions docs/mintlify/examples/parsers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Parser examples enact what happens when the LLM gets the JSON contract slightly
| Name | Scenario | Source |
|---|---|---|
| `parsers_fixing` | LLM emits malformed JSON — `OutputFixingParser` re-prompts with the parse error, the model usually fixes it on the second try. | [src](https://github.com/0xvasanth/cognis/blob/main/examples/parsers/fixing_parser.rs) |
| `parsers_retry` | When the prompt itself needs revision — `RetryParser` re-runs the entire prompt N times with stricter guidance. | [src](https://github.com/0xvasanth/cognis/blob/main/examples/parsers/retry_parser.rs) |
| `parsers_retry` | When one fix isn't enough — `RetryParser` loops the fixer + parse cycle up to N times until parsing succeeds, surfacing the last error if all attempts fail. | [src](https://github.com/0xvasanth/cognis/blob/main/examples/parsers/retry_parser.rs) |

## How to run

Expand All @@ -21,7 +21,7 @@ cargo run -p cognis-examples --example parsers_retry
## When to use which

- **`OutputFixingParser`** — model is *capable* of valid JSON but slipped this time. Cheaper: one repair attempt with the parse error in the prompt.
- **`RetryParser`** — original prompt is the problem and the fix needs the original context. More expensive but more robust.
- **`RetryParser`** — one repair pass isn't enough; loop fixer + parse up to N times. More expensive but more robust against models that produce different-but-still-broken JSON each attempt.

In production, layer them: `RetryParser::with_retries(OutputFixingParser::new(inner, fixer), 3)`.

Expand Down
2 changes: 1 addition & 1 deletion docs/mintlify/examples/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Eight numbered demos that walk the V2 surface — Runnable, agents
sidebarTitle: "Quickstart V2"
---

The numbered V2 demos are the fastest way to learn the shape of Cognis end-to-end. Each is a self-contained `.rs` file under [`examples/v2/`](https://github.com/0xvasanth/cognis/tree/main/examples/v2). All eight default to local Ollama; set `COGNIS_PROVIDER=openai` (etc.) to swap.
The numbered V2 demos are the fastest way to learn the shape of Cognis end-to-end. Each is a self-contained `.rs` file under [`examples/v2/`](https://github.com/0xvasanth/cognis/tree/main/examples/v2). The LLM-backed demos (02, 03, 04, 06, 07, 08) read `COGNIS_PROVIDER` from the env — set it to `ollama` for local, or any of the other providers. Demos 01 and 05 are pure-Rust offline (no provider needed).

## How to run

Expand Down
17 changes: 8 additions & 9 deletions examples/chains/structured_extraction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,14 @@ async fn main() -> Result<()> {
let raw = reply.content().to_string();
println!("--- raw model output ---\n{raw}\n");

match parser.parse(&raw) {
Ok(items) => {
println!("--- parsed action items ---");
for it in &items {
let due = it.due.as_deref().unwrap_or("(no date)");
println!(" [{due}] {}: {}", it.who, it.what);
}
}
Err(e) => eprintln!("parse failed: {e}"),
// If the model wandered off the JSON contract, propagate the error
// so a CI run fails loudly. In production, wrap with `OutputFixingParser`
// so a second LLM call repairs the output instead of aborting.
let items = parser.parse(&raw)?;
println!("--- parsed action items ---");
for it in &items {
let due = it.due.as_deref().unwrap_or("(no date)");
println!(" [{due}] {}: {}", it.who, it.what);
}
Ok(())
}
10 changes: 1 addition & 9 deletions examples/chains/structured_parsing_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@
//! cargo run -p cognis-examples --example chains_structured_parsing
//!
//! Sample output (against ollama / llama3.1):
//! warning: fields `label` and `confidence` are never read
//! --> crates/examples/../../examples/chains/structured_parsing_demo.rs:30:5
//! |
//! 29 | struct Sentiment {
//! | --------- fields in this struct
//! 30 | label: String,
//! | ^^^^^
//! 31 | confidence: f32,
//! ...
//! ok: Sentiment { label: "positive", confidence: 0.92 }
//! ok: Sentiment { label: "negative", confidence: 0.7 }
//! ok: Sentiment { label: "neutral", confidence: 0.55 }
Expand All @@ -37,6 +28,7 @@ use cognis_core::schemars::{self, JsonSchema};
use serde::Deserialize;

#[derive(Debug, Deserialize, JsonSchema)]
#[allow(dead_code)] // the printout uses the Debug impl; explicit reads aren't needed.
struct Sentiment {
label: String,
confidence: f32,
Expand Down
8 changes: 5 additions & 3 deletions examples/graphs/semantic_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
//! is identical: read state, return `Goto::node(...)`.
//!
//! Scenario:
//! A classifier node inspects the incoming user message. If it ends
//! in a `?` it's a real question — route to the `qa` branch.
//! Otherwise treat it as small talk and route to `echo`.
//! A classifier node inspects the incoming user message. If it
//! contains a `?` it's a real question — route to the `qa` branch.
//! Otherwise treat it as small talk and route to `echo`. Real code
//! would call an LLM (or a small classifier) here; the routing
//! pattern is the same.
//!
//! Run with:
//! cargo run -p cognis-examples --example graphs_semantic_router
Expand Down
59 changes: 42 additions & 17 deletions examples/graphs/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,33 @@
//! smarter backoff strategy without rewriting the loop.
//!
//! Scenario:
//! The agent kicked off a long-running export and got back a job
//! ID. We poll the (stubbed) status endpoint up to 5 times. If
//! the job finishes, we end successfully; if we hit the cap, we
//! end with `gave_up = true` so the caller can surface that to the
//! user.
//! The agent kicked off a long-running export and got back a job ID.
//! We poll the (stubbed) status endpoint up to 5 times. We run the
//! graph twice: once with a stub that finishes on attempt 3 (success
//! path), once with a stub that never finishes (timeout path,
//! `gave_up = true`).
//!
//! Run with:
//! cargo run -p cognis-examples --example graphs_state_machine
//!
//! Sample output (against ollama / llama3.1):
//! --- success path: completes on attempt 3 ---
//! [poll] attempt 1/5
//! [poll] attempt 2/5
//! [poll] attempt 3/5
//! [poll] job complete on attempt 3
//!
//! final: State { attempts: 3, finished: true, gave_up: false }
//!
//! --- timeout path: never finishes ---
//! [poll] attempt 1/5
//! [poll] attempt 2/5
//! [poll] attempt 3/5
//! [poll] attempt 4/5
//! [poll] attempt 5/5
//! [poll] giving up after 5 attempts
//! final: State { attempts: 5, finished: false, gave_up: true }

use std::sync::Arc;

use cognis::prelude::*;

Expand Down Expand Up @@ -57,22 +68,19 @@ impl GraphState for State {

const MAX_ATTEMPTS: u32 = 5;

/// Stand-in for a status check. The real call would be HTTP — the
/// shape of the loop is the same.
fn job_done(attempt: u32) -> bool {
// Pretend the job finishes on attempt 3.
attempt >= 3
}
/// Status-check stub: takes the attempt number, returns whether the
/// (pretend) job is done. Real code would be an HTTP call.
type StatusCheck = Arc<dyn Fn(u32) -> bool + Send + Sync>;

#[tokio::main]
async fn main() -> Result<()> {
let poll = node_fn::<State, _, _>("poll", |s, _| {
async fn run_once(label: &str, status_check: StatusCheck) -> Result<State> {
let poll = node_fn::<State, _, _>("poll", move |s, _| {
let already = s.attempts;
let check = status_check.clone();
async move {
let attempt = already + 1;
println!("[poll] attempt {attempt}/{MAX_ATTEMPTS}");

if job_done(attempt) {
if check(attempt) {
println!("[poll] job complete on attempt {attempt}");
return Ok(NodeOut {
update: Update {
Expand Down Expand Up @@ -107,11 +115,28 @@ async fn main() -> Result<()> {
}
});

println!("--- {label} ---");
let graph = Graph::<State>::new()
.node("poll", poll)
.start_at("poll")
.compile()?;
let final_state = graph.invoke(State::default(), Default::default()).await?;
println!("\nfinal: {final_state:?}");
println!("final: {final_state:?}\n");
Ok(final_state)
}

#[tokio::main]
async fn main() -> Result<()> {
// Path 1 — success: job finishes on attempt 3.
run_once(
"success path: completes on attempt 3",
Arc::new(|attempt| attempt >= 3),
)
.await?;

// Path 2 — timeout: status check never returns true; the
// max-attempts cap kicks in.
run_once("timeout path: never finishes", Arc::new(|_attempt| false)).await?;

Ok(())
}
32 changes: 17 additions & 15 deletions examples/models/embedding_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
//!
//! Why this matters:
//! Embeddings + cosine similarity is the bedrock of any vector
//! search or RAG pipeline. The trait abstracts the backend: swap in
//! `OllamaEmbeddings`, `OpenAIEmbeddings`, or `VoyageEmbeddings`
//! later and the ranking code never changes. `FakeEmbeddings` lets
//! you test the surrounding shape without paying API tokens.
//! search or RAG pipeline. The trait abstracts the backend: swap
//! `OllamaEmbeddings` for `OpenAIEmbeddings` or `VoyageEmbeddings`
//! later and the ranking code never changes.
//!
//! Scenario:
//! You have three product descriptions in a tiny catalogue:
//! waterproof hiking boots, a ceramic coffee mug, and a wireless
//! ergonomic keyboard. A shopper searches for "something for typing
//! all day" — the keyboard should win on similarity.
//! all day" — the keyboard should rank top on semantic similarity.
//!
//! Run with:
//! cargo run -p cognis-examples --example models_embedding
//! COGNIS_PROVIDER=ollama COGNIS_OLLAMA_MODEL=llama3.1 \
//! cargo run -p cognis-examples --example models_embedding
//!
//! Sample output (against ollama / llama3.1):
//! Requires `ollama pull nomic-embed-text` for the embedder.
//!
//! Sample output (against ollama / nomic-embed-text):
//! query: "something for typing all day"
//! 1. score=-0.082 Ceramic coffee mug12oz, dishwasher safe, glossy navy glaze.
//! 2. score=-0.086 Waterproof hiking boots — full-grain leather, vibram sole, ankle support for rough trails.
//! 3. score=-0.107 Wireless ergonomic keyboardsplit layout, mechanical switches, designed for all-day typing.
//! 1. score=0.705 Wireless ergonomic keyboardsplit layout, mechanical switches, designed for all-day typing.
//! 2. score=0.356 Waterproof hiking boots — full-grain leather, vibram sole, ankle support for rough trails.
//! 3. score=0.334 Ceramic coffee mug12oz, dishwasher safe, glossy navy glaze.

use std::sync::Arc;

use cognis::prelude::*;
use cognis_rag::{Embeddings, FakeEmbeddings};
use cognis_rag::{Embeddings, OllamaEmbeddings};

fn cosine(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
Expand All @@ -43,10 +45,10 @@ fn cosine(a: &[f32], b: &[f32]) -> f32 {

#[tokio::main]
async fn main() -> Result<()> {
// FakeEmbeddings is deterministic — fine for showing the shape of
// the pipeline. Swap to `OllamaEmbeddings::new("nomic-embed-text")`
// for real semantic ranking.
let emb: Arc<dyn Embeddings> = Arc::new(FakeEmbeddings::new(64));
// Real Ollama embeddings. `nomic-embed-text` is small (~270 MB) and
// semantically ranks similar text correctly. The Ollama daemon must
// be running with that model pulled.
let emb: Arc<dyn Embeddings> = Arc::new(OllamaEmbeddings::new("nomic-embed-text"));

let products = [
"Waterproof hiking boots — full-grain leather, vibram sole, ankle support for rough trails.",
Expand Down
6 changes: 4 additions & 2 deletions examples/observability/evaluation_framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ async fn main() -> Result<()> {
}
println!("\nresult: {pass}/{} passed", cases.len());
if pass != cases.len() {
// In CI you'd `std::process::exit(1)` here.
eprintln!("(some cases regressed — fail the build)");
// Exit non-zero so CI flags the regression. The runner job sees
// the failure code and the build turns red.
eprintln!("FAIL: {} case(s) regressed", cases.len() - pass);
std::process::exit(1);
}
Ok(())
}
8 changes: 6 additions & 2 deletions examples/resilience/error_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ use cognis_core::runnable_ext::RunnableExt;

#[tokio::main]
async fn main() -> Result<()> {
// Stage 1: validate. Returns a typed `Validation` error so the
// caller can match on it.
// Stage 1: validate. We use `CognisError::Internal` here for
// brevity — production code would define its own validation
// variant or wrap a domain error. The point is that the same
// `CognisError` enum surfaces through `.pipe()` regardless of
// which stage failed, so a single match arm at the top covers all
// signup-flow failures.
let parse_age = lambda(|raw: String| async move {
match raw.trim().parse::<i32>() {
Ok(n) if n > 0 => Ok::<_, CognisError>(n as u32),
Expand Down
35 changes: 23 additions & 12 deletions examples/retrieval/rag_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,38 @@
//! Why this matters:
//! This is the canonical RAG pattern every Cognis user will reach
//! for. The pieces — splitter, embeddings, vector store, retriever,
//! client — are all swappable behind their traits, so swapping
//! `FakeEmbeddings` for `OllamaEmbeddings` or in-memory for sqlite
//! is a one-line change.
//! client — are all swappable behind their traits. Swap
//! `OllamaEmbeddings` for `OpenAIEmbeddings`, or in-memory for
//! FAISS / Qdrant / Pinecone — one-line changes.
//!
//! Scenario:
//! Three short docs describe Cognis. We chunk, embed, and index them,
//! Three short docs describe Cognis. We chunk, embed (with real
//! `nomic-embed-text` so similarity is meaningful), and index them,
//! then ask "What does cognis-rag include?". The retriever finds the
//! matching chunk and the LLM answers grounded in only that context.
//!
//! Run with:
//! COGNIS_PROVIDER=ollama COGNIS_OLLAMA_MODEL=llama3.1 \
//! cargo run -p cognis-examples --example retrieval_rag_pipeline
//!
//! Sample output (against ollama / llama3.1):
//! Requires `ollama pull nomic-embed-text` for the embedder.
//!
//! Sample output (against ollama / llama3.1 + nomic-embed-text):
//! --- context ---
//! - Cognis is a Rust LLM framework.
//! - cognisgraph offers a Pregel-style stateful graph engine.
//! - cognis-rag bundles embeddings, vector stores, and retrievers.
//! --- answer ---
//! cognis-rag includes a relational algebra layer.
//! Cognis-RAG includes:
//! 1. Embeddings — vector representations of each query or prompt.
//! 2. Vector stores — databases optimized for storing dense vectors.
//! 3. Retrievers — algorithms that use the store to fetch the most
//! relevant documents based on similarity scores.

use std::sync::Arc;

use cognis::prelude::*;
use cognis_rag::{
Document, Embeddings, FakeEmbeddings, InMemoryVectorStore, RecursiveCharSplitter, TextSplitter,
VectorStore,
Document, Embeddings, InMemoryVectorStore, OllamaEmbeddings, RecursiveCharSplitter,
TextSplitter, VectorStore,
};

#[tokio::main]
Expand All @@ -45,19 +51,24 @@ async fn main() -> Result<()> {
.with_chunk_size(120)
.split_all(&docs);

let emb: Arc<dyn Embeddings> = Arc::new(FakeEmbeddings::new(32));
// Real semantic embeddings — `nomic-embed-text` is a small (~270 MB)
// local model that does the job for short docs. Swap to OpenAI /
// Voyage for production quality at higher latency + cost.
let emb: Arc<dyn Embeddings> = Arc::new(OllamaEmbeddings::new("nomic-embed-text"));
let mut store = InMemoryVectorStore::new(emb);
let texts: Vec<_> = chunks.iter().map(|c| c.content.clone()).collect();
store.add_texts(texts, None).await?;

let q = "What does cognis-rag include?";
let hits = store.similarity_search(q, 2).await?;
// Top 1 — semantic search should pick the cognis-rag doc first.
let hits = store.similarity_search(q, 1).await?;
let context: String = hits
.iter()
.map(|h| format!("- {}", h.text))
.collect::<Vec<_>>()
.join("\n");

// Ground the LLM in retrieved context only — no general knowledge.
let client = Client::from_env()?;
let prompt = format!("Answer using only:\n{context}\n\nQ: {q}\nA:");
let resp = client.invoke(vec![Message::human(prompt)]).await?;
Expand Down
17 changes: 11 additions & 6 deletions examples/retrieval/reranking_retriever.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,17 @@ impl CrossEncoder for LlmJudge {
d.content
);
let resp = self.client.invoke(vec![Message::human(prompt)]).await?;
let s = resp
.content()
.split_whitespace()
.next()
.and_then(|w| w.parse::<f32>().ok())
.unwrap_or(0.0);
// Models often pad the answer with prose ("Score: 7.5", "I'd say
// 8 / 10"). Sweep the reply for the first numeric token; if
// nothing parseable shows up, treat that as a neutral
// mid-scale score rather than 0.0 — a hard zero would
// catastrophically demote a doc just because the reranker
// model mis-formatted its reply.
let raw = resp.content();
let s = raw
.split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
.find_map(|w| w.parse::<f32>().ok().filter(|n| (0.0..=10.0).contains(n)))
.unwrap_or(5.0);
scores.push(s);
}
Ok(scores)
Expand Down
2 changes: 1 addition & 1 deletion examples/tools/tool_orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//! A price-comparison flow. Three "vendor" stubs each fetch a
//! price for the same SKU; a fourth step depends on all three
//! completing and picks the lowest. With a sequential plan the
//! total wait would be ~240ms — the orchestrator runs the three
//! total wait would be ~300ms — the orchestrator runs the three
//! fetches in parallel so the elapsed time is closer to ~100ms.
//!
//! Run with:
Expand Down
Loading