open-standards/eoc/eoc-rs/crates/eoc-rag/src/hyde.rs
Transaction Science e11c20b1eb ingest wave A3: 3 crates · op-ewallets-asia + smart-byte-zk + eoc-rag
OpenPay (1 crate, 3277 LOC, 18 tests)
  op-ewallets-asia    APAC e-wallet rail adapters. Alipay Cross-Border v3,
                      WeChat Pay (JSAPI/Native/H5/MicroPay) with constant-time
                      HMAC notify, UPI 2.0 (collect/intent/mandate/VPA),
                      Paytm, GrabPay, GoPay (Midtrans), Touch'n Go.
                      EMVCo MPM TLV + CRC-16/CCITT-FALSE encoder for
                      PromptPay (TH), PayNow (SG), DuitNow (MY), QRIS (ID).
                      Transport-free traits — operators inject HTTP + RSA-SHA256
                      signers (keeps POS firmware / WASM consumers reqwest-free).

Smart Byte (1 crate, 1310 LOC, 15 tests)
  smart-byte-zk       Selective-disclosure ZK primitives.
                      Real: Bulletproofs over Ristretto255 (range proofs at
                      8/16/32/64-bit), RangePredicate / InequalityPredicate /
                      SetMembershipPredicate all delegating to Bulletproofs.
                      Stubs (documented): Groth16, PLONK — deterministic
                      placeholder proofs satisfying ZkScheme trait for future
                      arkworks integration. AnonCred sketch + VerifiablePresentation
                      builder.

EOC (1 crate, ~3000 LOC, 36 tests)
  eoc-rag             RAG orchestrator. Naive (Lewis 2020), HyDE (Gao 2022),
                      RAG-Fusion + RRF (Cormack 2009), Self-RAG (Asai 2023)
                      with Retrieve/ISREL/ISSUP/ISUSE critique tokens,
                      Corrective RAG (Yan 2024) with retrieval evaluator
                      + external fallback, Adaptive RAG (Jeong 2024)
                      complexity router. Chunking: sentence-window, semantic,
                      recursive, late-chunking. Span-backed citations with
                      3-mode enforcement. SelfCheckGPT consistency guard.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 12:08:49 -04:00

198 lines
6.5 KiB
Rust

//! HyDE — Hypothetical Document Embeddings (Gao et al. 2022,
//! arXiv:2212.10496).
//!
//! Instead of embedding the (short, often under-specified) user query,
//! HyDE first generates a hypothetical answer with an LLM, then
//! embeds *that* and retrieves against it. The hypothetical document
//! is in the same semantic neighbourhood as the real evidence, which
//! makes dense retrieval more robust to short queries.
//!
//! The reference [`HeuristicHydeGenerator`] is a deterministic
//! fallback used in tests; vendor backends plug in via the
//! [`HydeGenerator`] trait.
use std::sync::Arc;
use async_trait::async_trait;
use eoc_core::JouleCost;
use crate::citation::{CitationEnforcement, CitationPolicy, derive_citations};
use crate::error::{RagError, RagResult};
use crate::pipeline::{Answer, Pipeline, RagRequest, Stage, Trace, TraceEvent};
use crate::store::DocumentStore;
/// Generates a hypothetical answer to a query.
#[async_trait]
pub trait HydeGenerator: Send + Sync {
/// Produce one (or more) hypothetical documents for `query`.
/// Implementations should keep these short — a single paragraph.
async fn hypothesize(&self, query: &str) -> RagResult<Vec<String>>;
}
/// Deterministic fallback generator. Concatenates the query with
/// hard-coded scaffolding text that mimics a typical encyclopedia
/// answer.
pub struct HeuristicHydeGenerator;
#[async_trait]
impl HydeGenerator for HeuristicHydeGenerator {
async fn hypothesize(&self, query: &str) -> RagResult<Vec<String>> {
// Expand the query with anchor phrases that often appear in
// matching evidence. This is enough to give the dense
// retriever extra surface area in the deterministic
// in-memory store, mirroring the empirical lift HyDE shows
// on short queries.
let stripped = strip_interrogative(query);
Ok(vec![
format!(
"{stripped} refers to a well-known concept. In general, {stripped} can be described in detail as follows."
),
])
}
}
fn strip_interrogative(query: &str) -> String {
let trimmed = query.trim().trim_end_matches('?').trim();
let lower = trimmed.to_lowercase();
let prefixes = [
"what is the ",
"what is ",
"how does ",
"how do ",
"why is ",
"why does ",
];
for p in prefixes {
if lower.starts_with(p) {
let start = p.len();
return trimmed[start..].trim().to_string();
}
}
trimmed.to_string()
}
/// HyDE pipeline. Replaces the query at the retrieval step with the
/// concatenation of the hypothetical document(s) plus the original
/// query (so we don't lose exact-match recall).
pub struct HydePipeline {
/// Store to retrieve against.
pub store: Arc<dyn DocumentStore>,
/// The hypothetical-document generator.
pub generator: Arc<dyn HydeGenerator>,
/// Citation policy.
pub citation_policy: CitationPolicy,
/// Per-stage joule estimates.
pub hyde_microjoules: u64,
/// Retrieval cost.
pub retrieve_microjoules: u64,
/// Generation cost.
pub generate_microjoules: u64,
}
impl HydePipeline {
/// Construct with the deterministic [`HeuristicHydeGenerator`].
pub fn new(store: Arc<dyn DocumentStore>) -> Self {
Self {
store,
generator: Arc::new(HeuristicHydeGenerator),
citation_policy: CitationPolicy::Optional,
hyde_microjoules: 30_000,
retrieve_microjoules: 5_000,
generate_microjoules: 50_000,
}
}
/// Construct with a caller-supplied generator.
pub fn with_generator(store: Arc<dyn DocumentStore>, generator: Arc<dyn HydeGenerator>) -> Self {
Self {
store,
generator,
citation_policy: CitationPolicy::Optional,
hyde_microjoules: 30_000,
retrieve_microjoules: 5_000,
generate_microjoules: 50_000,
}
}
/// Override citation policy (consumes `self`).
pub fn with_citation_policy(mut self, policy: CitationPolicy) -> Self {
self.citation_policy = policy;
self
}
}
#[async_trait]
impl Pipeline for HydePipeline {
async fn answer(&self, req: &RagRequest) -> RagResult<Answer> {
if req.top_k == 0 {
return Err(RagError::Config("top_k must be >= 1".into()));
}
let mut trace = Trace::new();
let hypos = self.generator.hypothesize(&req.query).await?;
trace.record(TraceEvent::new(
Stage::HypotheticalDocument,
JouleCost::estimated(self.hyde_microjoules),
format!("{} hypothetical(s)", hypos.len()),
));
// Concatenate hypotheticals with the original query so we
// keep exact-match recall.
let mut composite = req.query.clone();
for h in &hypos {
composite.push(' ');
composite.push_str(h);
}
let chunks = self.store.retrieve(&composite, req.top_k).await?;
trace.record(TraceEvent::new(
Stage::Retrieve,
JouleCost::estimated(self.retrieve_microjoules),
format!("retrieved {} chunks via HyDE", chunks.len()),
));
if chunks.is_empty() {
return Err(RagError::NoChunks);
}
let answer_text = chunks[0].chunk.text.clone();
trace.record(TraceEvent::new(
Stage::Generate,
JouleCost::estimated(self.generate_microjoules),
"hyde stuffed generation",
));
let citations = derive_citations(&answer_text, &chunks);
CitationEnforcement::new(self.citation_policy).enforce(&answer_text, &chunks, &citations)?;
Ok(Answer::new(answer_text, chunks, trace).with_citations(citations))
}
fn name(&self) -> &str {
"hyde"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::{Chunk, InMemoryStore};
#[tokio::test]
async fn hyde_returns_chunks_for_short_query() {
let store: Arc<dyn DocumentStore> = Arc::new(InMemoryStore::from_chunks(
"test",
vec![
Chunk::new(
"d1",
0,
"RRF refers to a well-known consensus algorithm described in detail.",
),
Chunk::new("d2", 0, "Random text about turtles."),
],
));
let p = HydePipeline::new(store);
let req = RagRequest::new("RRF", 2);
let ans = p.answer(&req).await.expect("ok");
assert!(!ans.chunks.is_empty());
}
}