diff --git a/docs/mintlify/examples/parsers.mdx b/docs/mintlify/examples/parsers.mdx index 7081894..837a75e 100644 --- a/docs/mintlify/examples/parsers.mdx +++ b/docs/mintlify/examples/parsers.mdx @@ -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 @@ -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)`. diff --git a/docs/mintlify/examples/quickstart.mdx b/docs/mintlify/examples/quickstart.mdx index 1abd18c..d6a5315 100644 --- a/docs/mintlify/examples/quickstart.mdx +++ b/docs/mintlify/examples/quickstart.mdx @@ -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 diff --git a/examples/chains/structured_extraction.rs b/examples/chains/structured_extraction.rs index 3e7cccc..70223db 100644 --- a/examples/chains/structured_extraction.rs +++ b/examples/chains/structured_extraction.rs @@ -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(()) } diff --git a/examples/chains/structured_parsing_demo.rs b/examples/chains/structured_parsing_demo.rs index 5097cbb..7f1a135 100644 --- a/examples/chains/structured_parsing_demo.rs +++ b/examples/chains/structured_parsing_demo.rs @@ -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 } @@ -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, diff --git a/examples/graphs/semantic_router.rs b/examples/graphs/semantic_router.rs index 8eaf51a..a622b60 100644 --- a/examples/graphs/semantic_router.rs +++ b/examples/graphs/semantic_router.rs @@ -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 diff --git a/examples/graphs/state_machine.rs b/examples/graphs/state_machine.rs index 8477e89..c79c6ae 100644 --- a/examples/graphs/state_machine.rs +++ b/examples/graphs/state_machine.rs @@ -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::*; @@ -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 bool + Send + Sync>; -#[tokio::main] -async fn main() -> Result<()> { - let poll = node_fn::("poll", |s, _| { +async fn run_once(label: &str, status_check: StatusCheck) -> Result { + let poll = node_fn::("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 { @@ -107,11 +115,28 @@ async fn main() -> Result<()> { } }); + println!("--- {label} ---"); let graph = Graph::::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(()) } diff --git a/examples/models/embedding_models.rs b/examples/models/embedding_models.rs index 3b13a92..2914d03 100644 --- a/examples/models/embedding_models.rs +++ b/examples/models/embedding_models.rs @@ -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 mug — 12oz, 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 keyboard — split layout, mechanical switches, designed for all-day typing. +//! 1. score=0.705 Wireless ergonomic keyboard — split 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 mug — 12oz, 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(); @@ -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 = 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 = Arc::new(OllamaEmbeddings::new("nomic-embed-text")); let products = [ "Waterproof hiking boots — full-grain leather, vibram sole, ankle support for rough trails.", diff --git a/examples/observability/evaluation_framework.rs b/examples/observability/evaluation_framework.rs index e41fbc9..390118b 100644 --- a/examples/observability/evaluation_framework.rs +++ b/examples/observability/evaluation_framework.rs @@ -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(()) } diff --git a/examples/resilience/error_handling.rs b/examples/resilience/error_handling.rs index cfb91db..dc86a20 100644 --- a/examples/resilience/error_handling.rs +++ b/examples/resilience/error_handling.rs @@ -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::() { Ok(n) if n > 0 => Ok::<_, CognisError>(n as u32), diff --git a/examples/retrieval/rag_pipeline.rs b/examples/retrieval/rag_pipeline.rs index f36b8eb..4e4bfc3 100644 --- a/examples/retrieval/rag_pipeline.rs +++ b/examples/retrieval/rag_pipeline.rs @@ -6,12 +6,13 @@ //! 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. //! @@ -19,19 +20,24 @@ //! 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] @@ -45,19 +51,24 @@ async fn main() -> Result<()> { .with_chunk_size(120) .split_all(&docs); - let emb: Arc = 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 = 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::>() .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?; diff --git a/examples/retrieval/reranking_retriever.rs b/examples/retrieval/reranking_retriever.rs index 42e4a37..a256759 100644 --- a/examples/retrieval/reranking_retriever.rs +++ b/examples/retrieval/reranking_retriever.rs @@ -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::().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::().ok().filter(|n| (0.0..=10.0).contains(n))) + .unwrap_or(5.0); scores.push(s); } Ok(scores) diff --git a/examples/tools/tool_orchestrator.rs b/examples/tools/tool_orchestrator.rs index c28dd0d..7137644 100644 --- a/examples/tools/tool_orchestrator.rs +++ b/examples/tools/tool_orchestrator.rs @@ -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: