feat: implement full pipeline (pgvector, embeddings, ingest, query, HTTP)

- Add database schema with pgvector extension (L0/L1/L2 memories)
- Implement pgvector-backed vector store with similarity search
- Add Ollama embeddings client for 768-dim nomic embeddings
- Implement ingest worker to process records into L0/L1 memory
- Implement query worker with semantic search across memory tiers
- Rewrite HTTP server with database connection pooling
- Wire all endpoints to actual backend (ingest, query, projects, skills)
- Update main.rs to use DATABASE_URL from environment
- All code compiles, ready for Docker build and deployment
This commit is contained in:
Story Crater Bot
2026-08-23 18:32:24 -07:00
parent b5f77cbc3f
commit 33eaf1b4f8
17 changed files with 2191 additions and 237 deletions
+2
View File
@@ -14,3 +14,5 @@ thiserror = { workspace = true }
reqwest = { workspace = true }
tracing = { workspace = true }
chrono = { workspace = true }
pgvector = { workspace = true }
uuid = { workspace = true }
+63
View File
@@ -0,0 +1,63 @@
use anyhow::{anyhow, Result};
use pgvector::Vector;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
/// Embeddings client for Ollama
#[derive(Clone)]
pub struct EmbeddingsClient {
base_url: String,
model: String,
#[allow(dead_code)]
http: Client,
}
#[derive(Debug, Serialize)]
struct EmbeddingRequest {
model: String,
input: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct EmbeddingResponse {
embeddings: Vec<Vec<f32>>,
model: String,
}
impl EmbeddingsClient {
/// Create from environment (OLLAMA_BASE_URL, EMBEDDINGS_MODEL)
pub fn from_env() -> Result<Self> {
let base_url = env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
let model = env::var("EMBEDDINGS_MODEL").unwrap_or_else(|_| "nomic-embed-text-v2-moe".to_string());
Ok(Self {
base_url,
model,
http: Client::new(),
})
}
/// Embed a single text string
pub async fn embed(&self, text: &str) -> Result<Vector> {
let embeddings = self.embed_batch(&[text.to_string()]).await?;
Ok(embeddings.into_iter().next().ok_or_else(|| anyhow::anyhow!("empty embedding response"))?)
}
/// Embed multiple texts in a batch
pub async fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vector>> {
let req = EmbeddingRequest {
model: self.model.clone(),
input: texts.to_vec(),
};
let url = format!("{}/api/embed", self.base_url);
let resp: EmbeddingResponse = self.http.post(&url).json(&req).send().await?.json().await?;
Ok(resp
.embeddings
.into_iter()
.map(Vector::from)
.collect())
}
}
+2
View File
@@ -1,5 +1,7 @@
pub mod chat;
pub mod rerank;
pub mod embeddings;
pub use chat::{ChatClient, Completion, Usage};
pub use rerank::RerankClient;
pub use embeddings::EmbeddingsClient;