diff --git a/common/src/error.rs b/common/src/error.rs index c68559e..17c9d27 100644 --- a/common/src/error.rs +++ b/common/src/error.rs @@ -9,7 +9,7 @@ use crate::storage::types::file_info::FileError; #[derive(Error, Debug)] pub enum EmbeddingError { #[error("openai error: {0}")] - OpenAI(#[from] OpenAIError), + OpenAI(Box), #[error("fastembed error: {0}")] FastEmbed(String), #[error("task join error: {0}")] @@ -24,6 +24,12 @@ pub enum EmbeddingError { UnknownModel(String), } +impl From for EmbeddingError { + fn from(err: OpenAIError) -> Self { + Self::OpenAI(Box::new(err)) + } +} + impl EmbeddingError { pub(crate) fn fastembed(err: impl std::fmt::Display) -> Self { Self::FastEmbed(err.to_string()) @@ -39,9 +45,9 @@ impl EmbeddingError { #[derive(Error, Debug)] pub enum AppError { #[error("database error: {0}")] - Database(#[from] surrealdb::Error), + Database(Box), #[error("openai error: {0}")] - OpenAI(#[from] OpenAIError), + OpenAI(Box), #[error("embedding error: {0}")] Embedding(#[from] EmbeddingError), #[error("file error: {0}")] @@ -61,17 +67,47 @@ pub enum AppError { #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("reqwest error: {0}")] - Reqwest(#[from] reqwest::Error), + Reqwest(Box), #[error("storage error: {0}")] - Storage(#[from] object_store::Error), + Storage(Box), #[error("ingestion processing error: {0}")] Processing(String), #[error("dom smoothie error: {0}")] - DomSmoothie(#[from] dom_smoothie::ReadabilityError), + DomSmoothie(Box), #[error("internal service error: {0}")] InternalError(String), } +impl From for AppError { + fn from(err: surrealdb::Error) -> Self { + Self::Database(Box::new(err)) + } +} + +impl From for AppError { + fn from(err: OpenAIError) -> Self { + Self::OpenAI(Box::new(err)) + } +} + +impl From for AppError { + fn from(err: reqwest::Error) -> Self { + Self::Reqwest(Box::new(err)) + } +} + +impl From for AppError { + fn from(err: object_store::Error) -> Self { + Self::Storage(Box::new(err)) + } +} + +impl From for AppError { + fn from(err: dom_smoothie::ReadabilityError) -> Self { + Self::DomSmoothie(Box::new(err)) + } +} + impl AppError { /// Builds an [`AppError::InternalError`] from a displayable message. #[must_use] @@ -79,3 +115,17 @@ impl AppError { Self::InternalError(msg.to_string()) } } + +#[cfg(test)] +mod tests { + use super::AppError; + + #[test] + fn app_error_is_reasonably_sized() { + assert!( + std::mem::size_of::() <= 64, + "AppError is {} bytes", + std::mem::size_of::() + ); + } +} diff --git a/common/src/storage/types/file_info.rs b/common/src/storage/types/file_info.rs index 48b41c5..24e450f 100644 --- a/common/src/storage/types/file_info.rs +++ b/common/src/storage/types/file_info.rs @@ -36,7 +36,7 @@ pub enum FileError { /// Database operation on the file record failed. #[error("surrealdb error: {0}")] - SurrealError(#[from] surrealdb::Error), + SurrealError(Box), /// Failed to persist the temporary file to its final location. #[error("failed to persist file: {0}")] @@ -48,7 +48,19 @@ pub enum FileError { /// The underlying object store operation failed. #[error("object store error: {0}")] - ObjectStore(#[from] ObjectStoreError), + ObjectStore(Box), +} + +impl From for FileError { + fn from(err: surrealdb::Error) -> Self { + Self::SurrealError(Box::new(err)) + } +} + +impl From for FileError { + fn from(err: ObjectStoreError) -> Self { + Self::ObjectStore(Box::new(err)) + } } stored_object!(FileInfo, "file", { @@ -163,7 +175,7 @@ impl FileInfo { match db_client.get_item::(id).await { Ok(Some(file_info)) => Ok(file_info), Ok(None) => Err(FileError::FileNotFound(id.to_string())), - Err(e) => Err(FileError::SurrealError(e)), + Err(e) => Err(FileError::from(e)), } } @@ -233,7 +245,7 @@ impl FileInfo { if let Ok(existing) = Self::get_by_sha(&sha256, user_id, db_client).await { return Ok(existing); } - Err(FileError::SurrealError(e)) + Err(FileError::from(e)) } } } @@ -263,7 +275,7 @@ impl FileInfo { storage .delete_prefix(&parent_prefix) .await - .map_err(AppError::Storage)?; + .map_err(AppError::from)?; info!( "Removed object prefix {} and its contents via StorageManager", parent_prefix @@ -286,7 +298,7 @@ impl FileInfo { &self, storage: &StorageManager, ) -> Result { - storage.get(&self.path).await.map_err(AppError::Storage) + storage.get(&self.path).await.map_err(AppError::from) } /// Persist bytes to storage using StorageManager. diff --git a/common/src/storage/types/knowledge_entity.rs b/common/src/storage/types/knowledge_entity.rs index 965de4f..732067f 100644 --- a/common/src/storage/types/knowledge_entity.rs +++ b/common/src/storage/types/knowledge_entity.rs @@ -183,9 +183,9 @@ impl KnowledgeEntity { .bind(("sources", source_ids.to_vec())) .bind(("user_id", user_id.to_owned())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .take(0) - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(entities) } @@ -203,9 +203,9 @@ impl KnowledgeEntity { .bind(("table", Self::table_name())) .bind(("source_id", source_id.to_owned())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -248,9 +248,9 @@ impl KnowledgeEntity { .bind(("entity", entity)) .bind(("emb", emb)) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -289,11 +289,11 @@ impl KnowledgeEntity { .bind(("embedding", query_embedding)) .bind(("user_id", user_id.to_string())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; - response = response.check().map_err(AppError::Database)?; + response = response.check().map_err(AppError::from)?; - let rows: Vec = response.take::>(0).map_err(AppError::Database)?; + let rows: Vec = response.take::>(0).map_err(AppError::from)?; Ok(rows .into_iter() @@ -321,7 +321,7 @@ impl KnowledgeEntity { let entity: KnowledgeEntity = db_client .get_item(id) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .ok_or_else(|| AppError::NotFound(format!("entity {id} not found")))?; let settings = SystemSettings::get_current(db_client).await?; @@ -355,9 +355,9 @@ impl KnowledgeEntity { .bind(("emb", emb)) .bind(("description", description.to_string())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -453,9 +453,9 @@ impl KnowledgeEntity { KnowledgeEntityEmbedding::table_name() )) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; db.client .query(format!( @@ -463,9 +463,9 @@ impl KnowledgeEntity { KnowledgeEntityEmbedding::table_name() )) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; // Perform DB updates in a single transaction info!("Applying embedding updates in a transaction..."); @@ -504,9 +504,9 @@ impl KnowledgeEntity { db.client .query(transaction_query) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; info!("Re-embedding process for knowledge entities completed successfully."); Ok(()) diff --git a/common/src/storage/types/knowledge_entity_embedding.rs b/common/src/storage/types/knowledge_entity_embedding.rs index a9cea67..a4e5c35 100644 --- a/common/src/storage/types/knowledge_entity_embedding.rs +++ b/common/src/storage/types/knowledge_entity_embedding.rs @@ -29,8 +29,8 @@ impl KnowledgeEntityEmbedding { dimension, ); - let res = db.client.query(query).await.map_err(AppError::Database)?; - res.check().map_err(AppError::Database)?; + let res = db.client.query(query).await.map_err(AppError::from)?; + res.check().map_err(AppError::from)?; Ok(()) } @@ -78,8 +78,8 @@ impl KnowledgeEntityEmbedding { .query(query) .bind(("entity_id", entity_id.clone())) .await - .map_err(AppError::Database)?; - let embeddings: Vec = result.take(0).map_err(AppError::Database)?; + .map_err(AppError::from)?; + let embeddings: Vec = result.take(0).map_err(AppError::from)?; Ok(embeddings.into_iter().next()) } @@ -101,8 +101,8 @@ impl KnowledgeEntityEmbedding { .query(query) .bind(("entity_ids", entity_ids.to_vec())) .await - .map_err(AppError::Database)?; - let embeddings: Vec = result.take(0).map_err(AppError::Database)?; + .map_err(AppError::from)?; + let embeddings: Vec = result.take(0).map_err(AppError::from)?; Ok(embeddings .into_iter() @@ -123,9 +123,9 @@ impl KnowledgeEntityEmbedding { .query(query) .bind(("entity_id", entity_id.clone())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -142,9 +142,9 @@ impl KnowledgeEntityEmbedding { .query(query) .bind(("source_id", source_id.to_owned())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } } diff --git a/common/src/storage/types/knowledge_relationship.rs b/common/src/storage/types/knowledge_relationship.rs index fdd3f0a..64e383a 100644 --- a/common/src/storage/types/knowledge_relationship.rs +++ b/common/src/storage/types/knowledge_relationship.rs @@ -69,9 +69,9 @@ impl KnowledgeRelationship { .bind(("source_id", self.metadata.source_id.clone())) .bind(("relationship_type", self.metadata.relationship_type.clone())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -89,9 +89,9 @@ impl KnowledgeRelationship { .bind(("source_id", source_id.to_owned())) .bind(("user_id", user_id.to_owned())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -109,9 +109,9 @@ impl KnowledgeRelationship { .bind(("id", id.to_owned())) .bind(("user_id", user_id.to_owned())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; let deleted: Vec = - delete_result.take(0).map_err(AppError::Database)?; + delete_result.take(0).map_err(AppError::from)?; if !deleted.is_empty() { return Ok(()); @@ -122,9 +122,9 @@ impl KnowledgeRelationship { .query("SELECT * FROM type::thing('relates_to', $id)") .bind(("id", id.to_owned())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; let existing: Option = - exists_result.take(0).map_err(AppError::Database)?; + exists_result.take(0).map_err(AppError::from)?; if existing.is_some() { Err(AppError::Auth( diff --git a/common/src/storage/types/text_chunk.rs b/common/src/storage/types/text_chunk.rs index 6d97efc..4aae9ed 100644 --- a/common/src/storage/types/text_chunk.rs +++ b/common/src/storage/types/text_chunk.rs @@ -55,9 +55,9 @@ impl TextChunk { .bind(("source_id", source_id.to_owned())) .bind(("table", Self::table_name())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -97,9 +97,9 @@ impl TextChunk { .bind(("chunk", chunk)) .bind(("emb", emb)) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -140,11 +140,11 @@ impl TextChunk { .bind(("embedding", query_embedding)) .bind(("user_id", user_id.to_string())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; - response = response.check().map_err(AppError::Database)?; + response = response.check().map_err(AppError::from)?; - let rows: Vec = response.take::>(0).map_err(AppError::Database)?; + let rows: Vec = response.take::>(0).map_err(AppError::from)?; Ok(rows .into_iter() @@ -208,11 +208,11 @@ impl TextChunk { .bind(("user_id", user_id.to_owned())) .bind(("limit", limit)) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; - response = response.check().map_err(AppError::Database)?; + response = response.check().map_err(AppError::from)?; - let rows: Vec = response.take::>(0).map_err(AppError::Database)?; + let rows: Vec = response.take::>(0).map_err(AppError::from)?; Ok(rows .into_iter() @@ -314,16 +314,16 @@ impl TextChunk { TextChunkEmbedding::table_name() )) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; db.client .query(format!("DELETE FROM {};", TextChunkEmbedding::table_name())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; // Perform DB updates in a single transaction against the embedding table info!("Applying embedding updates in a transaction..."); @@ -366,9 +366,9 @@ impl TextChunk { db.client .query(transaction_query) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; info!("Re-embedding process for text chunks completed successfully."); Ok(()) diff --git a/common/src/storage/types/text_chunk_embedding.rs b/common/src/storage/types/text_chunk_embedding.rs index 1f1bf5d..9f85841 100644 --- a/common/src/storage/types/text_chunk_embedding.rs +++ b/common/src/storage/types/text_chunk_embedding.rs @@ -33,8 +33,8 @@ impl TextChunkEmbedding { dimension, ); - let res = db.client.query(query).await.map_err(AppError::Database)?; - res.check().map_err(AppError::Database)?; + let res = db.client.query(query).await.map_err(AppError::from)?; + res.check().map_err(AppError::from)?; Ok(()) } @@ -85,9 +85,9 @@ impl TextChunkEmbedding { .query(query) .bind(("chunk_id", chunk_id.clone())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; - let embeddings: Vec = result.take(0).map_err(AppError::Database)?; + let embeddings: Vec = result.take(0).map_err(AppError::from)?; Ok(embeddings.into_iter().next()) } @@ -106,9 +106,9 @@ impl TextChunkEmbedding { .query(query) .bind(("chunk_id", chunk_id.clone())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } @@ -129,9 +129,9 @@ impl TextChunkEmbedding { .query(query) .bind(("source_id", source_id.to_owned())) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .check() - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } diff --git a/common/src/storage/types/text_content.rs b/common/src/storage/types/text_content.rs index 9b49d9b..c18c109 100644 --- a/common/src/storage/types/text_content.rs +++ b/common/src/storage/types/text_content.rs @@ -115,7 +115,7 @@ impl TextContent { surrealdb::Datetime::from(now), )) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; if updated.is_none() { return Err(AppError::NotFound(format!("text content {id} not found"))); @@ -138,10 +138,10 @@ impl TextContent { .bind(("file_id", file_id.to_owned())) .bind(("exclude_id", exclude_id.to_owned())) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; let existing: Option = - response.take(0).map_err(AppError::Database)?; + response.take(0).map_err(AppError::from)?; Ok(existing.is_some()) } @@ -193,9 +193,9 @@ impl TextContent { .bind(("user_id", user_id.to_owned())) .bind(("limit", limit)) .await - .map_err(AppError::Database)? + .map_err(AppError::from)? .take(0) - .map_err(AppError::Database) + .map_err(AppError::from) } /// Builds a fallback display label for a source id when no matching content row exists. @@ -239,9 +239,9 @@ impl TextContent { .bind(("user_id", user_id.to_owned())) .bind(("record_ids", record_ids)) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; - let contents: Vec = response.take(0).map_err(AppError::Database)?; + let contents: Vec = response.take(0).map_err(AppError::from)?; tracing::debug!( source_id_count = source_ids.len(), diff --git a/common/src/storage/types/user.rs b/common/src/storage/types/user.rs index a39940a..c96e393 100644 --- a/common/src/storage/types/user.rs +++ b/common/src/storage/types/user.rs @@ -688,7 +688,7 @@ impl User { db.delete_item::(id) .await - .map_err(AppError::Database)?; + .map_err(AppError::from)?; Ok(()) } diff --git a/html-router/assets/style.css b/html-router/assets/style.css index 57fbde4..fd43572 100644 --- a/html-router/assets/style.css +++ b/html-router/assets/style.css @@ -44,6 +44,7 @@ --leading-snug: 1.375; --leading-relaxed: 1.625; --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -284,6 +285,37 @@ } } } + .drawer-open { + > .drawer-side { + overflow-y: auto; + } + > .drawer-toggle { + display: none; + & ~ .drawer-side { + pointer-events: auto; + visibility: visible; + position: sticky; + display: block; + width: auto; + overscroll-behavior: auto; + opacity: 100%; + & > .drawer-overlay { + cursor: default; + background-color: transparent; + } + & > *:not(.drawer-overlay) { + translate: 0%; + [dir="rtl"] & { + translate: 0%; + } + } + } + &:checked ~ .drawer-side { + pointer-events: auto; + visibility: visible; + } + } + } .drawer-toggle { position: fixed; height: calc(0.25rem * 0); @@ -1042,6 +1074,22 @@ grid-row-start: 1; min-width: calc(0.25rem * 0); } + .chat-image { + grid-row: span 2 / span 2; + align-self: flex-end; + } + .chat-footer { + grid-row-start: 3; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } + .chat-header { + grid-row-start: 1; + display: flex; + gap: calc(0.25rem * 1); + font-size: 0.6875rem; + } .container { width: 100%; @media (width >= 40rem) { @@ -1748,6 +1796,9 @@ .w-10 { width: calc(var(--spacing) * 10); } + .w-11 { + width: calc(var(--spacing) * 11); + } .w-11\/12 { width: calc(11/12 * 100%); } @@ -1811,6 +1862,9 @@ .flex-none { flex: none; } + .flex-shrink { + flex-shrink: 1; + } .flex-shrink-0 { flex-shrink: 0; } @@ -1823,6 +1877,13 @@ .grow { flex-grow: 1; } + .border-collapse { + border-collapse: collapse; + } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1895,6 +1956,9 @@ .justify-start { justify-content: flex-start; } + .gap-0 { + gap: calc(var(--spacing) * 0); + } .gap-0\.5 { gap: calc(var(--spacing) * 0.5); } @@ -2027,6 +2091,9 @@ .border-base-200 { border-color: var(--color-base-200); } + .border-base-content { + border-color: var(--color-base-content); + } .border-base-content\/10 { border-color: var(--color-base-content); @supports (color: color-mix(in lab, red, red)) { @@ -2063,6 +2130,9 @@ .bg-transparent { background-color: transparent; } + .bg-warning { + background-color: var(--color-warning); + } .bg-warning\/10 { background-color: var(--color-warning); @supports (color: color-mix(in lab, red, red)) { @@ -2081,6 +2151,9 @@ .loading-spinner { mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); } + .mask-repeat { + mask-repeat: repeat; + } .fill-current { fill: currentcolor; } @@ -2111,6 +2184,9 @@ .p-8 { padding: calc(var(--spacing) * 8); } + .px-1 { + padding-inline: calc(var(--spacing) * 1); + } .px-1\.5 { padding-inline: calc(var(--spacing) * 1.5); } @@ -2265,6 +2341,9 @@ --tw-tracking: var(--tracking-widest); letter-spacing: var(--tracking-widest); } + .text-wrap { + text-wrap: wrap; + } .break-words { overflow-wrap: break-word; } @@ -2331,6 +2410,17 @@ .italic { font-style: italic; } + .underline { + text-decoration-line: underline; + } + .swap-active { + .swap-off { + opacity: 0%; + } + .swap-on { + opacity: 100%; + } + } .opacity-0 { opacity: 0%; } @@ -2424,6 +2514,10 @@ --tw-duration: 300ms; transition-duration: 300ms; } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } .ease-out { --tw-ease: var(--ease-out); transition-timing-function: var(--ease-out); diff --git a/html-router/src/routes/admin/handlers.rs b/html-router/src/routes/admin/handlers.rs index 0c328ed..b46b51c 100644 --- a/html-router/src/routes/admin/handlers.rs +++ b/html-router/src/routes/admin/handlers.rs @@ -233,8 +233,7 @@ fn plan_embedding_settings_update( .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| current.embedding_model.clone()); + .map_or_else(|| current.embedding_model.clone(), ToOwned::to_owned); if !is_valid_fastembed_model_code(&embedding_model) { return Err(AppError::Validation(format!( diff --git a/ingestion-pipeline/src/utils/file_text_extraction.rs b/ingestion-pipeline/src/utils/file_text_extraction.rs index 1a4bfdf..90df7c7 100644 --- a/ingestion-pipeline/src/utils/file_text_extraction.rs +++ b/ingestion-pipeline/src/utils/file_text_extraction.rs @@ -77,7 +77,7 @@ pub async fn extract_text_from_file( let file_bytes = storage .get(&file_info.path) .await - .map_err(AppError::Storage)?; + .map_err(AppError::from)?; let local_path = resolve_existing_local_path(storage, &file_info.path).await; match file_info.mime_type.as_str() {