use std::future::Future;
use std::time::Duration;
use super::error::{CapabilityError, ErrorCategory};
use crate::recall::{RecallCandidate, RecallQuery};
#[derive(Debug, Clone)]
pub enum RecallError {
IndexUnavailable {
message: String,
},
DimensionMismatch {
expected: usize,
got: usize,
},
EmbeddingFailed {
message: String,
transient: bool,
},
InvalidQuery {
message: String,
},
AuthFailed {
message: String,
},
RateLimited {
retry_after: Duration,
},
Timeout {
elapsed: Duration,
deadline: Duration,
},
NotFound {
id: String,
},
Internal {
message: String,
},
}
impl std::fmt::Display for RecallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IndexUnavailable { message } => {
write!(f, "recall index unavailable: {}", message)
}
Self::DimensionMismatch { expected, got } => {
write!(
f,
"dimension mismatch: expected {} dimensions, got {}",
expected, got
)
}
Self::EmbeddingFailed { message, .. } => {
write!(f, "embedding failed: {}", message)
}
Self::InvalidQuery { message } => {
write!(f, "invalid recall query: {}", message)
}
Self::AuthFailed { message } => {
write!(f, "recall auth failed: {}", message)
}
Self::RateLimited { retry_after } => {
write!(f, "rate limited, retry after {:?}", retry_after)
}
Self::Timeout { elapsed, deadline } => {
write!(
f,
"recall operation timed out after {:?} (deadline: {:?})",
elapsed, deadline
)
}
Self::NotFound { id } => {
write!(f, "recall record not found: {}", id)
}
Self::Internal { message } => {
write!(f, "internal recall error: {}", message)
}
}
}
}
impl std::error::Error for RecallError {}
impl CapabilityError for RecallError {
fn category(&self) -> ErrorCategory {
match self {
Self::IndexUnavailable { .. } => ErrorCategory::Unavailable,
Self::DimensionMismatch { .. } => ErrorCategory::InvalidInput,
Self::EmbeddingFailed { .. } => ErrorCategory::Internal,
Self::InvalidQuery { .. } => ErrorCategory::InvalidInput,
Self::AuthFailed { .. } => ErrorCategory::Auth,
Self::RateLimited { .. } => ErrorCategory::RateLimit,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::NotFound { .. } => ErrorCategory::NotFound,
Self::Internal { .. } => ErrorCategory::Internal,
}
}
fn is_transient(&self) -> bool {
match self {
Self::IndexUnavailable { .. } => true,
Self::DimensionMismatch { .. } => false,
Self::EmbeddingFailed { transient, .. } => *transient,
Self::InvalidQuery { .. } => false,
Self::AuthFailed { .. } => false,
Self::RateLimited { .. } => true,
Self::Timeout { .. } => true,
Self::NotFound { .. } => false,
Self::Internal { .. } => false,
}
}
fn is_retryable(&self) -> bool {
self.is_transient()
}
fn retry_after(&self) -> Option<Duration> {
match self {
Self::RateLimited { retry_after } => Some(*retry_after),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct RecallRecord {
pub id: String,
pub content: String,
pub embedding: Option<Vec<f32>>,
pub metadata: RecallRecordMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct RecallRecordMetadata {
pub source_type: Option<String>,
pub source_chain_id: Option<String>,
pub tenant_id: Option<String>,
pub created_at: Option<String>,
}
pub trait RecallReader: Send + Sync {
type QueryFut<'a>: Future<Output = Result<Vec<RecallCandidate>, RecallError>> + Send + 'a
where
Self: 'a;
fn query<'a>(&'a self, query: &'a RecallQuery) -> Self::QueryFut<'a>;
}
pub trait RecallWriter: Send + Sync {
type StoreFut<'a>: Future<Output = Result<(), RecallError>> + Send + 'a
where
Self: 'a;
type DeleteFut<'a>: Future<Output = Result<(), RecallError>> + Send + 'a
where
Self: 'a;
fn store<'a>(&'a self, record: RecallRecord) -> Self::StoreFut<'a>;
fn delete<'a>(&'a self, id: &'a str) -> Self::DeleteFut<'a>;
}
pub trait Recall: RecallReader + RecallWriter {}
impl<T: RecallReader + RecallWriter> Recall for T {}
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait DynRecallReader: Send + Sync {
fn query<'a>(
&'a self,
query: &'a RecallQuery,
) -> BoxFuture<'a, Result<Vec<RecallCandidate>, RecallError>>;
}
impl<T: RecallReader> DynRecallReader for T {
fn query<'a>(
&'a self,
query: &'a RecallQuery,
) -> BoxFuture<'a, Result<Vec<RecallCandidate>, RecallError>> {
Box::pin(RecallReader::query(self, query))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recall_error_display() {
let err = RecallError::DimensionMismatch {
expected: 1536,
got: 768,
};
assert!(err.to_string().contains("1536"));
assert!(err.to_string().contains("768"));
}
#[test]
fn recall_error_category_classification() {
assert_eq!(
RecallError::IndexUnavailable {
message: "test".to_string()
}
.category(),
ErrorCategory::Unavailable
);
assert_eq!(
RecallError::DimensionMismatch {
expected: 1536,
got: 768
}
.category(),
ErrorCategory::InvalidInput
);
assert_eq!(
RecallError::AuthFailed {
message: "test".to_string()
}
.category(),
ErrorCategory::Auth
);
assert_eq!(
RecallError::RateLimited {
retry_after: Duration::from_secs(60)
}
.category(),
ErrorCategory::RateLimit
);
}
#[test]
fn recall_error_transient_classification() {
assert!(
RecallError::IndexUnavailable {
message: "test".to_string()
}
.is_transient()
);
assert!(
RecallError::RateLimited {
retry_after: Duration::from_secs(60)
}
.is_transient()
);
assert!(
RecallError::Timeout {
elapsed: Duration::from_secs(30),
deadline: Duration::from_secs(30),
}
.is_transient()
);
assert!(
!RecallError::DimensionMismatch {
expected: 1536,
got: 768
}
.is_transient()
);
assert!(
!RecallError::AuthFailed {
message: "test".to_string()
}
.is_transient()
);
assert!(
!RecallError::NotFound {
id: "test".to_string()
}
.is_transient()
);
}
#[test]
fn recall_error_retry_after() {
let err = RecallError::RateLimited {
retry_after: Duration::from_secs(60),
};
assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
let err2 = RecallError::IndexUnavailable {
message: "test".to_string(),
};
assert_eq!(err2.retry_after(), None);
}
#[test]
fn recall_record_metadata_default() {
let meta = RecallRecordMetadata::default();
assert!(meta.source_type.is_none());
assert!(meta.tenant_id.is_none());
}
}