1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::HashMap;
6
7pub type MemoryId = i64;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Memory {
13 pub id: MemoryId,
15 pub content: String,
17 #[serde(rename = "type")]
19 pub memory_type: MemoryType,
20 #[serde(default)]
22 pub tags: Vec<String>,
23 #[serde(default)]
25 pub metadata: HashMap<String, serde_json::Value>,
26 #[serde(default = "default_importance")]
28 pub importance: f32,
29 #[serde(default)]
31 pub access_count: i32,
32 pub created_at: DateTime<Utc>,
34 pub updated_at: DateTime<Utc>,
36 pub last_accessed_at: Option<DateTime<Utc>>,
38 pub owner_id: Option<String>,
40 #[serde(default)]
42 pub visibility: Visibility,
43 #[serde(default)]
45 pub scope: MemoryScope,
46 #[serde(default = "default_workspace")]
48 pub workspace: String,
49 #[serde(default)]
51 pub tier: MemoryTier,
52 #[serde(default = "default_version")]
54 pub version: i32,
55 #[serde(default)]
57 pub has_embedding: bool,
58 pub expires_at: Option<DateTime<Utc>>,
60 pub content_hash: Option<String>,
62 pub event_time: Option<DateTime<Utc>>,
65 pub event_duration_seconds: Option<i64>,
67 pub trigger_pattern: Option<String>,
69 #[serde(default)]
71 pub procedure_success_count: i32,
72 #[serde(default)]
74 pub procedure_failure_count: i32,
75 pub summary_of_id: Option<MemoryId>,
77 #[serde(default)]
80 pub lifecycle_state: LifecycleState,
81 pub media_url: Option<String>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[serde(rename_all = "lowercase")]
89pub enum LifecycleState {
90 #[default]
92 Active,
93 Stale,
95 Archived,
97}
98
99impl std::fmt::Display for LifecycleState {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 LifecycleState::Active => write!(f, "active"),
103 LifecycleState::Stale => write!(f, "stale"),
104 LifecycleState::Archived => write!(f, "archived"),
105 }
106 }
107}
108
109impl std::str::FromStr for LifecycleState {
110 type Err = String;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 match s.to_lowercase().as_str() {
114 "active" => Ok(LifecycleState::Active),
115 "stale" => Ok(LifecycleState::Stale),
116 "archived" => Ok(LifecycleState::Archived),
117 _ => Err(format!("Unknown lifecycle state: {}", s)),
118 }
119 }
120}
121
122fn default_workspace() -> String {
123 "default".to_string()
124}
125
126pub const RESERVED_WORKSPACES: &[&str] = &["_system", "_archive"];
128
129pub const MAX_WORKSPACE_LENGTH: usize = 64;
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum WorkspaceError {
135 Empty,
136 TooLong,
137 InvalidChars,
138 Reserved,
139}
140
141impl std::fmt::Display for WorkspaceError {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 WorkspaceError::Empty => write!(f, "Workspace name cannot be empty"),
145 WorkspaceError::TooLong => write!(f, "Workspace name exceeds {} characters", MAX_WORKSPACE_LENGTH),
146 WorkspaceError::InvalidChars => write!(f, "Workspace name can only contain lowercase letters, numbers, hyphens, and underscores"),
147 WorkspaceError::Reserved => write!(f, "Workspace name is reserved"),
148 }
149 }
150}
151
152impl std::error::Error for WorkspaceError {}
153
154pub fn normalize_workspace(s: &str) -> Result<String, WorkspaceError> {
163 let normalized = s.trim().to_lowercase();
164
165 if normalized.is_empty() {
166 return Err(WorkspaceError::Empty);
167 }
168
169 if normalized.len() > MAX_WORKSPACE_LENGTH {
170 return Err(WorkspaceError::TooLong);
171 }
172
173 if !normalized
174 .chars()
175 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
176 {
177 return Err(WorkspaceError::InvalidChars);
178 }
179
180 if normalized.starts_with('_') || RESERVED_WORKSPACES.contains(&normalized.as_str()) {
181 return Err(WorkspaceError::Reserved);
182 }
183
184 Ok(normalized)
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WorkspaceStats {
190 pub workspace: String,
192 pub memory_count: i64,
194 pub permanent_count: i64,
196 pub daily_count: i64,
198 pub first_memory_at: Option<DateTime<Utc>>,
200 pub last_memory_at: Option<DateTime<Utc>>,
202 #[serde(default)]
204 pub top_tags: Vec<(String, i64)>,
205 pub avg_importance: Option<f32>,
207}
208
209fn default_importance() -> f32 {
210 0.5
211}
212
213fn default_version() -> i32 {
214 1
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
219#[serde(rename_all = "lowercase")]
220pub enum MemoryType {
221 #[default]
222 Note,
223 Todo,
224 Issue,
225 Decision,
226 Preference,
227 Learning,
228 Context,
229 Credential,
230 Custom,
231 TranscriptChunk,
234 Episodic,
238 Procedural,
241 Summary,
244 Checkpoint,
247 Image,
250 Audio,
252 Video,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
266#[serde(rename_all = "lowercase")]
267pub enum MemoryTier {
268 #[default]
270 Permanent,
271 Daily,
273}
274
275impl MemoryTier {
276 pub fn as_str(&self) -> &'static str {
277 match self {
278 MemoryTier::Permanent => "permanent",
279 MemoryTier::Daily => "daily",
280 }
281 }
282
283 pub fn default_ttl_seconds(&self) -> Option<i64> {
285 match self {
286 MemoryTier::Permanent => None,
287 MemoryTier::Daily => Some(24 * 60 * 60), }
289 }
290}
291
292impl std::str::FromStr for MemoryTier {
293 type Err = String;
294
295 fn from_str(s: &str) -> Result<Self, Self::Err> {
296 match s.to_lowercase().as_str() {
297 "permanent" => Ok(MemoryTier::Permanent),
298 "daily" => Ok(MemoryTier::Daily),
299 _ => Err(format!("Unknown memory tier: {}", s)),
300 }
301 }
302}
303
304impl MemoryType {
305 pub fn as_str(&self) -> &'static str {
306 match self {
307 MemoryType::Note => "note",
308 MemoryType::Todo => "todo",
309 MemoryType::Issue => "issue",
310 MemoryType::Decision => "decision",
311 MemoryType::Preference => "preference",
312 MemoryType::Learning => "learning",
313 MemoryType::Context => "context",
314 MemoryType::Credential => "credential",
315 MemoryType::Custom => "custom",
316 MemoryType::TranscriptChunk => "transcript_chunk",
317 MemoryType::Episodic => "episodic",
318 MemoryType::Procedural => "procedural",
319 MemoryType::Summary => "summary",
320 MemoryType::Checkpoint => "checkpoint",
321 MemoryType::Image => "image",
322 MemoryType::Audio => "audio",
323 MemoryType::Video => "video",
324 }
325 }
326
327 pub fn excluded_from_default_search(&self) -> bool {
329 matches!(self, MemoryType::TranscriptChunk)
330 }
331
332 pub fn is_multimodal(&self) -> bool {
334 matches!(self, MemoryType::Image | MemoryType::Audio | MemoryType::Video)
335 }
336}
337
338impl std::str::FromStr for MemoryType {
339 type Err = String;
340
341 fn from_str(s: &str) -> Result<Self, Self::Err> {
342 match s.to_lowercase().as_str() {
343 "note" => Ok(MemoryType::Note),
344 "todo" => Ok(MemoryType::Todo),
345 "issue" => Ok(MemoryType::Issue),
346 "decision" => Ok(MemoryType::Decision),
347 "preference" => Ok(MemoryType::Preference),
348 "learning" => Ok(MemoryType::Learning),
349 "context" => Ok(MemoryType::Context),
350 "credential" => Ok(MemoryType::Credential),
351 "custom" => Ok(MemoryType::Custom),
352 "transcript_chunk" => Ok(MemoryType::TranscriptChunk),
353 "episodic" => Ok(MemoryType::Episodic),
354 "procedural" => Ok(MemoryType::Procedural),
355 "summary" => Ok(MemoryType::Summary),
356 "checkpoint" => Ok(MemoryType::Checkpoint),
357 "image" => Ok(MemoryType::Image),
358 "audio" => Ok(MemoryType::Audio),
359 "video" => Ok(MemoryType::Video),
360 _ => Err(format!("Unknown memory type: {}", s)),
361 }
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
367#[serde(rename_all = "lowercase")]
368pub enum Visibility {
369 #[default]
370 Private,
371 Shared,
372 Public,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
383#[serde(rename_all = "lowercase")]
384pub enum MemoryScope {
385 User { user_id: String },
387 Session { session_id: String },
389 Agent { agent_id: String },
391 #[default]
393 Global,
394}
395
396impl MemoryScope {
397 pub fn user(user_id: impl Into<String>) -> Self {
399 MemoryScope::User {
400 user_id: user_id.into(),
401 }
402 }
403
404 pub fn session(session_id: impl Into<String>) -> Self {
406 MemoryScope::Session {
407 session_id: session_id.into(),
408 }
409 }
410
411 pub fn agent(agent_id: impl Into<String>) -> Self {
413 MemoryScope::Agent {
414 agent_id: agent_id.into(),
415 }
416 }
417
418 pub fn scope_type(&self) -> &'static str {
420 match self {
421 MemoryScope::User { .. } => "user",
422 MemoryScope::Session { .. } => "session",
423 MemoryScope::Agent { .. } => "agent",
424 MemoryScope::Global => "global",
425 }
426 }
427
428 pub fn scope_id(&self) -> Option<&str> {
430 match self {
431 MemoryScope::User { user_id } => Some(user_id.as_str()),
432 MemoryScope::Session { session_id } => Some(session_id.as_str()),
433 MemoryScope::Agent { agent_id } => Some(agent_id.as_str()),
434 MemoryScope::Global => None,
435 }
436 }
437
438 pub fn can_access(&self, other: &MemoryScope) -> bool {
441 match (self, other) {
442 (MemoryScope::Global, _) => true,
444 (MemoryScope::User { user_id: a }, MemoryScope::User { user_id: b }) => a == b,
446 (MemoryScope::Session { session_id: a }, MemoryScope::Session { session_id: b }) => {
447 a == b
448 }
449 (MemoryScope::Agent { agent_id: a }, MemoryScope::Agent { agent_id: b }) => a == b,
450 (_, MemoryScope::Global) => true,
452 _ => false,
454 }
455 }
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct CrossReference {
461 pub from_id: MemoryId,
463 pub to_id: MemoryId,
465 pub edge_type: EdgeType,
467 pub score: f32,
469 #[serde(default = "default_confidence")]
471 pub confidence: f32,
472 #[serde(default = "default_strength")]
474 pub strength: f32,
475 #[serde(default)]
477 pub source: RelationSource,
478 pub source_context: Option<String>,
480 pub created_at: DateTime<Utc>,
482 pub valid_from: DateTime<Utc>,
484 pub valid_to: Option<DateTime<Utc>>,
486 #[serde(default)]
488 pub pinned: bool,
489 #[serde(default)]
491 pub metadata: HashMap<String, serde_json::Value>,
492}
493
494fn default_confidence() -> f32 {
495 1.0
496}
497
498fn default_strength() -> f32 {
499 1.0
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
504#[serde(rename_all = "snake_case")]
505pub enum EdgeType {
506 #[default]
507 RelatedTo,
508 Supersedes,
509 Contradicts,
510 Implements,
511 Extends,
512 References,
513 DependsOn,
514 Blocks,
515 FollowsUp,
516}
517
518impl EdgeType {
519 pub fn as_str(&self) -> &'static str {
520 match self {
521 EdgeType::RelatedTo => "related_to",
522 EdgeType::Supersedes => "supersedes",
523 EdgeType::Contradicts => "contradicts",
524 EdgeType::Implements => "implements",
525 EdgeType::Extends => "extends",
526 EdgeType::References => "references",
527 EdgeType::DependsOn => "depends_on",
528 EdgeType::Blocks => "blocks",
529 EdgeType::FollowsUp => "follows_up",
530 }
531 }
532
533 pub fn all() -> &'static [EdgeType] {
534 &[
535 EdgeType::RelatedTo,
536 EdgeType::Supersedes,
537 EdgeType::Contradicts,
538 EdgeType::Implements,
539 EdgeType::Extends,
540 EdgeType::References,
541 EdgeType::DependsOn,
542 EdgeType::Blocks,
543 EdgeType::FollowsUp,
544 ]
545 }
546}
547
548impl std::str::FromStr for EdgeType {
549 type Err = String;
550
551 fn from_str(s: &str) -> Result<Self, Self::Err> {
552 match s.to_lowercase().as_str() {
553 "related_to" | "related" => Ok(EdgeType::RelatedTo),
554 "supersedes" => Ok(EdgeType::Supersedes),
555 "contradicts" => Ok(EdgeType::Contradicts),
556 "implements" => Ok(EdgeType::Implements),
557 "extends" => Ok(EdgeType::Extends),
558 "references" => Ok(EdgeType::References),
559 "depends_on" => Ok(EdgeType::DependsOn),
560 "blocks" => Ok(EdgeType::Blocks),
561 "follows_up" => Ok(EdgeType::FollowsUp),
562 _ => Err(format!("Unknown edge type: {}", s)),
563 }
564 }
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
569#[serde(rename_all = "lowercase")]
570pub enum RelationSource {
571 #[default]
572 Auto,
573 Manual,
574 Llm,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct SearchResult {
580 pub memory: Memory,
582 pub score: f32,
584 pub match_info: MatchInfo,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct MatchInfo {
591 pub strategy: SearchStrategy,
593 #[serde(default)]
595 pub matched_terms: Vec<String>,
596 #[serde(default)]
598 pub highlights: Vec<String>,
599 pub semantic_score: Option<f32>,
601 pub keyword_score: Option<f32>,
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
607#[serde(rename_all = "snake_case")]
608pub enum SearchStrategy {
609 #[serde(alias = "keyword")]
610 KeywordOnly,
611 #[serde(alias = "semantic")]
612 SemanticOnly,
613 #[default]
614 Hybrid,
615}
616
617impl SearchStrategy {
618 pub fn parse_str(s: &str) -> Option<Self> {
619 match s.to_lowercase().as_str() {
620 "keyword" | "keyword_only" => Some(SearchStrategy::KeywordOnly),
621 "semantic" | "semantic_only" => Some(SearchStrategy::SemanticOnly),
622 "hybrid" => Some(SearchStrategy::Hybrid),
623 _ => None,
624 }
625 }
626}
627
628fn deserialize_search_strategy_opt<'de, D>(
629 deserializer: D,
630) -> Result<Option<SearchStrategy>, D::Error>
631where
632 D: Deserializer<'de>,
633{
634 let opt = Option::<String>::deserialize(deserializer)?;
635 match opt.as_deref() {
636 None => Ok(None),
637 Some("auto") => Ok(None),
638 Some(other) => SearchStrategy::parse_str(other).map(Some).ok_or_else(|| {
639 <D::Error as serde::de::Error>::custom(format!("Invalid search strategy: {}", other))
640 }),
641 }
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct MemoryVersion {
647 pub version: i32,
649 pub content: String,
651 pub tags: Vec<String>,
653 pub metadata: HashMap<String, serde_json::Value>,
655 pub created_at: DateTime<Utc>,
657 pub created_by: Option<String>,
659 pub change_summary: Option<String>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize, Default)]
665pub struct StorageStats {
666 pub total_memories: i64,
667 pub total_tags: i64,
668 pub total_crossrefs: i64,
669 pub total_versions: i64,
670 pub total_identities: i64,
671 pub total_entities: i64,
672 pub db_size_bytes: i64,
673 pub memories_with_embeddings: i64,
674 pub memories_pending_embedding: i64,
675 pub last_sync: Option<DateTime<Utc>>,
676 pub sync_pending: bool,
677 pub storage_mode: String,
678 pub schema_version: i32,
679 pub workspaces: HashMap<String, i64>,
680 pub type_counts: HashMap<String, i64>,
681 pub tier_counts: HashMap<String, i64>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct StorageConfig {
687 pub db_path: String,
689 #[serde(default)]
691 pub storage_mode: StorageMode,
692 pub cloud_uri: Option<String>,
694 #[serde(default)]
696 pub encrypt_cloud: bool,
697 #[serde(default = "default_half_life")]
699 pub confidence_half_life_days: f32,
700 #[serde(default = "default_true")]
702 pub auto_sync: bool,
703 #[serde(default = "default_sync_debounce")]
705 pub sync_debounce_ms: u64,
706}
707
708fn default_half_life() -> f32 {
709 30.0
710}
711
712fn default_true() -> bool {
713 true
714}
715
716fn default_sync_debounce() -> u64 {
717 5000
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
722#[serde(rename_all = "kebab-case")]
723pub enum StorageMode {
724 #[default]
725 Local,
726 CloudSafe,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct EmbeddingConfig {
732 pub model: String,
734 pub api_key: Option<String>,
736 pub base_url: Option<String>,
739 pub embedding_model: Option<String>,
741 pub model_path: Option<String>,
743 pub dimensions: usize,
746 #[serde(default = "default_batch_size")]
748 pub batch_size: usize,
749}
750
751fn default_batch_size() -> usize {
752 100
753}
754
755impl Default for EmbeddingConfig {
756 fn default() -> Self {
757 Self {
758 model: "tfidf".to_string(),
759 api_key: None,
760 base_url: None,
761 embedding_model: None,
762 model_path: None,
763 dimensions: 384,
764 batch_size: 100,
765 }
766 }
767}
768
769#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
771#[serde(rename_all = "lowercase")]
772pub enum DedupMode {
773 Reject,
775 Merge,
777 Skip,
779 #[default]
781 Allow,
782}
783
784#[derive(Debug, Clone, Serialize, Deserialize, Default)]
786pub struct CreateMemoryInput {
787 pub content: String,
788 #[serde(default, alias = "type")]
789 pub memory_type: MemoryType,
790 #[serde(default)]
791 pub tags: Vec<String>,
792 #[serde(default)]
793 pub metadata: HashMap<String, serde_json::Value>,
794 pub importance: Option<f32>,
795 #[serde(default)]
797 pub scope: MemoryScope,
798 pub workspace: Option<String>,
800 #[serde(default)]
802 pub tier: MemoryTier,
803 #[serde(default)]
805 pub defer_embedding: bool,
806 pub ttl_seconds: Option<i64>,
810 #[serde(default)]
812 pub dedup_mode: DedupMode,
813 pub dedup_threshold: Option<f32>,
815 pub event_time: Option<DateTime<Utc>>,
818 pub event_duration_seconds: Option<i64>,
820 pub trigger_pattern: Option<String>,
822 pub summary_of_id: Option<MemoryId>,
824 pub media_url: Option<String>,
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct UpdateMemoryInput {
831 pub content: Option<String>,
832 #[serde(alias = "type")]
833 pub memory_type: Option<MemoryType>,
834 pub tags: Option<Vec<String>>,
835 pub metadata: Option<HashMap<String, serde_json::Value>>,
836 pub importance: Option<f32>,
837 pub scope: Option<MemoryScope>,
839 pub ttl_seconds: Option<i64>,
841 pub event_time: Option<Option<DateTime<Utc>>>,
845 pub trigger_pattern: Option<Option<String>>,
848 pub media_url: Option<Option<String>>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct CreateCrossRefInput {
856 pub from_id: MemoryId,
857 pub to_id: MemoryId,
858 #[serde(default)]
859 pub edge_type: EdgeType,
860 pub strength: Option<f32>,
861 pub source_context: Option<String>,
862 #[serde(default)]
863 pub pinned: bool,
864}
865
866#[derive(Debug, Clone, Default, Serialize, Deserialize)]
868pub struct ListOptions {
869 pub limit: Option<i64>,
870 pub offset: Option<i64>,
871 pub tags: Option<Vec<String>>,
872 #[serde(alias = "type")]
873 pub memory_type: Option<MemoryType>,
874 pub sort_by: Option<SortField>,
875 pub sort_order: Option<SortOrder>,
876 pub metadata_filter: Option<HashMap<String, serde_json::Value>>,
879 pub scope: Option<MemoryScope>,
881 pub workspace: Option<String>,
883 pub workspaces: Option<Vec<String>>,
885 pub tier: Option<MemoryTier>,
887 pub filter: Option<serde_json::Value>,
890 #[serde(default)]
893 pub include_archived: bool,
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
898#[serde(rename_all = "snake_case")]
899pub enum SortField {
900 #[default]
901 CreatedAt,
902 UpdatedAt,
903 LastAccessedAt,
904 Importance,
905 AccessCount,
906}
907
908#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
910#[serde(rename_all = "lowercase")]
911pub enum SortOrder {
912 Asc,
913 #[default]
914 Desc,
915}
916
917#[derive(Debug, Clone, Default, Serialize, Deserialize)]
919pub struct SearchOptions {
920 pub limit: Option<i64>,
921 pub min_score: Option<f32>,
922 pub tags: Option<Vec<String>>,
923 #[serde(alias = "type")]
924 pub memory_type: Option<MemoryType>,
925 #[serde(default, deserialize_with = "deserialize_search_strategy_opt")]
927 pub strategy: Option<SearchStrategy>,
928 #[serde(default)]
930 pub explain: bool,
931 pub scope: Option<MemoryScope>,
933 pub workspace: Option<String>,
935 pub workspaces: Option<Vec<String>>,
937 pub tier: Option<MemoryTier>,
939 #[serde(default)]
942 pub include_transcripts: bool,
943 pub filter: Option<serde_json::Value>,
946 #[serde(default)]
949 pub include_archived: bool,
950 pub scope_path: Option<String>,
956}
957
958#[derive(Debug, Clone, Serialize, Deserialize)]
960pub struct SyncStatus {
961 pub pending_changes: i64,
962 pub last_sync: Option<DateTime<Utc>>,
963 pub last_error: Option<String>,
964 pub is_syncing: bool,
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
969pub struct EmbeddingStatus {
970 pub memory_id: MemoryId,
971 pub status: EmbeddingState,
972 pub queued_at: Option<DateTime<Utc>>,
973 pub completed_at: Option<DateTime<Utc>>,
974 pub error: Option<String>,
975}
976
977#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
979#[serde(rename_all = "lowercase")]
980pub enum EmbeddingState {
981 Pending,
982 Processing,
983 Complete,
984 Failed,
985}