1use async_trait::async_trait;
2use serde_json::json;
3use synaptic_core::{Embeddings, SynapticError};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum NomicModel {
7 NomicEmbedTextV1_5,
8 NomicEmbedTextV1,
9 Custom(String),
10}
11
12impl NomicModel {
13 pub fn as_str(&self) -> &str {
14 match self {
15 NomicModel::NomicEmbedTextV1_5 => "nomic-embed-text-v1.5",
16 NomicModel::NomicEmbedTextV1 => "nomic-embed-text-v1",
17 NomicModel::Custom(s) => s.as_str(),
18 }
19 }
20}
21
22impl std::fmt::Display for NomicModel {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "{}", self.as_str())
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum NomicTaskType {
31 SearchDocument,
32 SearchQuery,
33 Classification,
34 Clustering,
35}
36
37impl NomicTaskType {
38 pub fn as_str(&self) -> &str {
39 match self {
40 NomicTaskType::SearchDocument => "search_document",
41 NomicTaskType::SearchQuery => "search_query",
42 NomicTaskType::Classification => "classification",
43 NomicTaskType::Clustering => "clustering",
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct NomicConfig {
50 pub api_key: String,
51 pub model: String,
52 pub base_url: String,
53}
54
55impl NomicConfig {
56 pub fn new(api_key: impl Into<String>) -> Self {
57 Self {
58 api_key: api_key.into(),
59 model: NomicModel::NomicEmbedTextV1_5.to_string(),
60 base_url: "https://api-atlas.nomic.ai/v1".to_string(),
61 }
62 }
63
64 pub fn with_model(mut self, model: NomicModel) -> Self {
65 self.model = model.to_string();
66 self
67 }
68}
69
70pub struct NomicEmbeddings {
71 config: NomicConfig,
72 client: reqwest::Client,
73}
74
75impl NomicEmbeddings {
76 pub fn new(config: NomicConfig) -> Self {
77 Self {
78 config,
79 client: reqwest::Client::new(),
80 }
81 }
82
83 async fn embed_with_task(
84 &self,
85 texts: &[&str],
86 task_type: NomicTaskType,
87 ) -> Result<Vec<Vec<f32>>, SynapticError> {
88 let body = json!({
89 "model": self.config.model,
90 "texts": texts,
91 "task_type": task_type.as_str(),
92 });
93 let resp = self
94 .client
95 .post(format!("{}/embedding/text", self.config.base_url))
96 .header("Authorization", format!("Bearer {}", self.config.api_key))
97 .header("Content-Type", "application/json")
98 .json(&body)
99 .send()
100 .await
101 .map_err(|e| SynapticError::Embedding(format!("Nomic request: {e}")))?;
102 let status = resp.status().as_u16();
103 let json: serde_json::Value = resp
104 .json()
105 .await
106 .map_err(|e| SynapticError::Embedding(format!("Nomic parse: {e}")))?;
107 if status != 200 {
108 return Err(SynapticError::Embedding(format!(
109 "Nomic API error ({}): {}",
110 status, json
111 )));
112 }
113 let embeddings = json
114 .get("embeddings")
115 .and_then(|e| e.as_array())
116 .ok_or_else(|| SynapticError::Embedding("missing 'embeddings' field".to_string()))?;
117 let result = embeddings
118 .iter()
119 .map(|row| {
120 row.as_array()
121 .unwrap_or(&vec![])
122 .iter()
123 .map(|v| v.as_f64().unwrap_or(0.0) as f32)
124 .collect()
125 })
126 .collect();
127 Ok(result)
128 }
129}
130
131#[async_trait]
132impl Embeddings for NomicEmbeddings {
133 async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, SynapticError> {
134 self.embed_with_task(texts, NomicTaskType::SearchDocument)
135 .await
136 }
137
138 async fn embed_query(&self, text: &str) -> Result<Vec<f32>, SynapticError> {
139 let mut results = self
140 .embed_with_task(&[text], NomicTaskType::SearchQuery)
141 .await?;
142 results
143 .pop()
144 .ok_or_else(|| SynapticError::Embedding("empty response".to_string()))
145 }
146}