use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};
use super::{ConfidenceScore, EvidenceSpan, FactStatus, GraphVersion, GraphVersionRange};
pub const RECIPROCAL_RANK_FUSION_K: f64 = 60.0;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FreshnessPolicy {
#[default]
AllowStale,
WaitUntilFresh,
GraphOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetrievalMode {
Hybrid,
GraphOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetrieverSource {
Bm25,
GraphEvidence,
CodeGraph,
Semantic,
Vector,
GraphPath,
Temporal,
CommunitySummary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RerankMode {
Local,
External,
Disabled,
}
impl RerankMode {
pub fn parse(value: &str) -> Result<Self, RerankModeError> {
match value.trim().to_ascii_lowercase().as_str() {
"local" => Ok(Self::Local),
"external" => Ok(Self::External),
"disabled" => Ok(Self::Disabled),
other => Err(RerankModeError {
value: other.to_owned(),
}),
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Local => "local",
Self::External => "external",
Self::Disabled => "disabled",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RerankModeError {
pub value: String,
}
impl fmt::Display for RerankModeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"rerank backend '{}' must be local, external, or disabled",
self.value
)
}
}
impl Error for RerankModeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetrievalBackendState {
Available,
Degraded,
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RetrievalBackendStatus {
pub source: RetrieverSource,
pub state: RetrievalBackendState,
pub scope_post_filter: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_graph_version: Option<GraphVersion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl RetrieverSource {
pub const fn as_str(self) -> &'static str {
match self {
Self::Bm25 => "bm25",
Self::GraphEvidence => "graph_evidence",
Self::CodeGraph => "code_graph",
Self::Semantic => "semantic",
Self::Vector => "vector",
Self::GraphPath => "graph_path",
Self::Temporal => "temporal",
Self::CommunitySummary => "community_summary",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retriever_source_labels_match_wire_values() {
assert_eq!(RetrieverSource::Bm25.as_str(), "bm25");
assert_eq!(RetrieverSource::GraphEvidence.as_str(), "graph_evidence");
assert_eq!(RetrieverSource::CodeGraph.as_str(), "code_graph");
assert_eq!(RetrieverSource::Semantic.as_str(), "semantic");
assert_eq!(RetrieverSource::Vector.as_str(), "vector");
assert_eq!(RetrieverSource::GraphPath.as_str(), "graph_path");
assert_eq!(RetrieverSource::Temporal.as_str(), "temporal");
assert_eq!(
RetrieverSource::CommunitySummary.as_str(),
"community_summary"
);
}
#[test]
fn rerank_mode_labels_match_wire_values() {
assert_eq!(
RerankMode::parse("local").expect("local"),
RerankMode::Local
);
assert_eq!(
RerankMode::parse("external").expect("external"),
RerankMode::External
);
assert_eq!(
RerankMode::parse("disabled").expect("disabled"),
RerankMode::Disabled
);
assert_eq!(RerankMode::Local.as_str(), "local");
assert_eq!(RerankMode::External.as_str(), "external");
assert_eq!(RerankMode::Disabled.as_str(), "disabled");
}
#[test]
fn graph_path_preserves_fact_provenance() {
let fact = ContextGraphFact {
fact_id: "rel-1".to_owned(),
kind: ContextGraphFactKind::Relation,
subject: "relay-knowledge".to_owned(),
predicate: "uses".to_owned(),
object: Some("BM25".to_owned()),
evidence_ids: vec!["ev-1".to_owned()],
confidence: ConfidenceScore { basis_points: 9000 },
status: FactStatus::Accepted,
version_range: GraphVersionRange::open_from(GraphVersion::new(1)),
};
let path = ContextGraphPath::from_fact(&fact);
assert_eq!(path.path_id, "path:rel-1");
assert_eq!(path.nodes, ["relay-knowledge", "BM25"]);
assert_eq!(path.edges[0].evidence_ids, ["ev-1"]);
assert_eq!(path.edges[0].confidence.basis_points, 9000);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RankingSignal {
pub source: RetrieverSource,
pub rank: usize,
pub score: f64,
pub explanation: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RerankSignal {
pub mode: RerankMode,
pub score: f64,
pub explanation: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RetrievalBudgetUsed {
pub limit: usize,
pub candidate_count: usize,
pub returned_count: usize,
pub context_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FusionDiagnostics {
pub algorithm: String,
pub k: f64,
pub candidate_count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RerankDiagnostics {
pub requested_mode: RerankMode,
pub effective_mode: RerankMode,
pub algorithm: String,
pub candidate_count: usize,
pub returned_count: usize,
pub degraded: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetrievedContextPack {
pub graph_version: GraphVersion,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_scope: Option<String>,
pub freshness: FreshnessPolicy,
pub truncated: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub backend_statuses: Vec<RetrievalBackendStatus>,
pub items: Vec<ContextPackItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextEntity {
pub id: String,
pub label: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextGraphFactKind {
Relation,
Claim,
Event,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextGraphFact {
pub fact_id: String,
pub kind: ContextGraphFactKind,
pub subject: String,
pub predicate: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
pub evidence_ids: Vec<String>,
pub confidence: ConfidenceScore,
pub status: FactStatus,
pub version_range: GraphVersionRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextGraphPath {
pub path_id: String,
pub nodes: Vec<String>,
pub edges: Vec<ContextGraphPathEdge>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextGraphPathEdge {
pub fact_id: String,
pub kind: ContextGraphFactKind,
pub from: String,
pub predicate: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
pub evidence_ids: Vec<String>,
pub confidence: ConfidenceScore,
pub status: FactStatus,
pub version_range: GraphVersionRange,
}
impl ContextGraphPath {
pub fn from_fact(fact: &ContextGraphFact) -> Self {
let mut nodes = vec![fact.subject.clone()];
if let Some(object) = &fact.object
&& !nodes.contains(object)
{
nodes.push(object.clone());
}
Self {
path_id: format!("path:{}", fact.fact_id),
nodes,
edges: vec![ContextGraphPathEdge {
fact_id: fact.fact_id.clone(),
kind: fact.kind,
from: fact.subject.clone(),
predicate: fact.predicate.clone(),
to: fact.object.clone(),
evidence_ids: fact.evidence_ids.clone(),
confidence: fact.confidence,
status: fact.status,
version_range: fact.version_range,
}],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeGraphArtifactKind {
Symbol,
Chunk,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeGraphArtifact {
pub kind: CodeGraphArtifactKind,
pub artifact_id: String,
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextPackItem {
pub result_id: String,
pub source_scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_span: Option<EvidenceSpan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entities: Vec<ContextEntity>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub graph_facts: Vec<ContextGraphFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub graph_paths: Vec<ContextGraphPath>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_artifact: Option<CodeGraphArtifact>,
pub retriever_sources: Vec<RetrieverSource>,
pub ranking: Vec<RankingSignal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rerank: Option<RerankSignal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RetrievalHit {
pub evidence_id: String,
pub source_scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_span: Option<EvidenceSpan>,
pub content: String,
pub entity_labels: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entities: Vec<ContextEntity>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub graph_facts: Vec<ContextGraphFact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_artifact: Option<CodeGraphArtifact>,
pub retriever_sources: Vec<RetrieverSource>,
pub ranking: Vec<RankingSignal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rerank: Option<RerankSignal>,
pub score: f64,
}