Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-11-16 11:50:49 +00:00
parent 1bd664fd2a
commit 888e5d303c
33 changed files with 766 additions and 460 deletions

View File

@@ -1,7 +1,8 @@
use std::sync::Arc;
use bevy::prelude::*;
use parking_lot::Mutex;
use rusqlite::Connection;
use std::sync::Arc;
use crate::config::Config;

View File

@@ -1,13 +1,24 @@
use bevy::prelude::*;
use iroh::protocol::Router;
use iroh::Endpoint;
use iroh_gossip::api::{GossipReceiver, GossipSender};
use iroh_gossip::net::Gossip;
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use bevy::prelude::*;
use iroh::{
Endpoint,
protocol::Router,
};
use iroh_gossip::{
api::{
GossipReceiver,
GossipSender,
},
net::Gossip,
proto::TopicId,
};
use parking_lot::Mutex;
use serde::{
Deserialize,
Serialize,
};
/// Message envelope for gossip sync
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncMessage {
@@ -56,13 +67,9 @@ pub struct GossipTopic(pub TopicId);
/// Bevy resource for tracking gossip initialization task
#[derive(Resource)]
pub struct GossipInitTask(pub bevy::tasks::Task<Option<(
Endpoint,
Gossip,
Router,
GossipSender,
GossipReceiver,
)>>);
pub struct GossipInitTask(
pub bevy::tasks::Task<Option<(Endpoint, Gossip, Router, GossipSender, GossipReceiver)>>,
);
/// Bevy message: a new message that needs to be published to gossip
#[derive(Message, Clone, Debug)]
@@ -70,7 +77,8 @@ pub struct PublishMessageEvent {
pub message: lib::Message,
}
/// Bevy message: a message received from gossip that needs to be saved to SQLite
/// Bevy message: a message received from gossip that needs to be saved to
/// SQLite
#[derive(Message, Clone, Debug)]
pub struct GossipMessageReceived {
pub sync_message: SyncMessage,

View File

@@ -1,7 +1,16 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::{
fs,
path::Path,
};
use anyhow::{
Context,
Result,
};
use serde::{
Deserialize,
Serialize,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
@@ -45,8 +54,7 @@ impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = fs::read_to_string(path.as_ref())
.context(format!("Failed to read config file: {:?}", path.as_ref()))?;
let config: Config = toml::from_str(&content)
.context("Failed to parse config file")?;
let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
Ok(config)
}
@@ -68,15 +76,12 @@ impl Config {
hostname: "lonni-daemon".to_string(),
state_dir: "./tailscale-state".to_string(),
},
grpc: GrpcConfig {
port: 50051,
},
grpc: GrpcConfig { port: 50051 },
}
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)
.context("Failed to serialize config")?;
let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
fs::write(path.as_ref(), content)
.context(format!("Failed to write config file: {:?}", path.as_ref()))?;
Ok(())

View File

@@ -1,7 +1,22 @@
use crate::db::schema::{deserialize_embedding, serialize_embedding};
use crate::models::*;
use chrono::{TimeZone, Utc};
use rusqlite::{params, Connection, OptionalExtension, Result, Row};
use chrono::{
TimeZone,
Utc,
};
use rusqlite::{
Connection,
OptionalExtension,
Result,
Row,
params,
};
use crate::{
db::schema::{
deserialize_embedding,
serialize_embedding,
},
models::*,
};
/// Insert a new message into the database
pub fn insert_message(conn: &Connection, msg: &lib::Message) -> Result<i64> {
@@ -71,7 +86,10 @@ pub fn insert_message_embedding(
}
/// Get message embedding
pub fn get_message_embedding(conn: &Connection, message_id: i64) -> Result<Option<MessageEmbedding>> {
pub fn get_message_embedding(
conn: &Connection,
message_id: i64,
) -> Result<Option<MessageEmbedding>> {
conn.query_row(
"SELECT id, message_id, embedding, model_name, created_at
FROM message_embeddings WHERE message_id = ?1",
@@ -203,7 +221,7 @@ pub fn list_emotions(
) -> Result<Vec<Emotion>> {
let mut query = String::from(
"SELECT id, message_id, emotion, confidence, model_version, created_at, updated_at
FROM emotions WHERE 1=1"
FROM emotions WHERE 1=1",
);
if emotion_filter.is_some() {

View File

@@ -1,4 +1,7 @@
use rusqlite::{Connection, Result};
use rusqlite::{
Connection,
Result,
};
use tracing::info;
pub fn initialize_database(conn: &Connection) -> Result<()> {
@@ -9,14 +12,17 @@ pub fn initialize_database(conn: &Connection) -> Result<()> {
// Try to load the vector extension (non-fatal if it fails for now)
match unsafe { conn.load_extension_enable() } {
Ok(_) => {
| Ok(_) => {
match unsafe { conn.load_extension(vec_path, None::<&str>) } {
Ok(_) => info!("Loaded sqlite-vec extension"),
Err(e) => info!("Could not load sqlite-vec extension: {}. Vector operations will not be available.", e),
| Ok(_) => info!("Loaded sqlite-vec extension"),
| Err(e) => info!(
"Could not load sqlite-vec extension: {}. Vector operations will not be available.",
e
),
}
let _ = unsafe { conn.load_extension_disable() };
}
Err(e) => info!("Extension loading not enabled: {}", e),
},
| Err(e) => info!("Extension loading not enabled: {}", e),
}
// Create messages table
@@ -172,10 +178,7 @@ pub fn initialize_database(conn: &Connection) -> Result<()> {
/// Helper function to serialize f32 vector to bytes for storage
pub fn serialize_embedding(embedding: &[f32]) -> Vec<u8> {
embedding
.iter()
.flat_map(|f| f.to_le_bytes())
.collect()
embedding.iter().flat_map(|f| f.to_le_bytes()).collect()
}
/// Helper function to deserialize bytes back to f32 vector

View File

@@ -1,9 +1,16 @@
use anyhow::Result;
use iroh::protocol::Router;
use iroh::Endpoint;
use iroh_gossip::api::{GossipReceiver, GossipSender};
use iroh_gossip::net::Gossip;
use iroh_gossip::proto::TopicId;
use iroh::{
Endpoint,
protocol::Router,
};
use iroh_gossip::{
api::{
GossipReceiver,
GossipSender,
},
net::Gossip,
proto::TopicId,
};
/// Initialize Iroh endpoint and gossip for the given topic
pub async fn init_iroh_gossip(

View File

@@ -8,20 +8,24 @@ mod models;
mod services;
mod systems;
use anyhow::{Context, Result};
use std::{
path::Path,
sync::Arc,
};
use anyhow::{
Context,
Result,
};
use bevy::prelude::*;
use config::Config;
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use rusqlite::Connection;
use std::path::Path;
use std::sync::Arc;
// Re-export init function
pub use iroh_sync::init_iroh_gossip;
// Import components and systems
use components::*;
use config::Config;
use iroh_gossip::proto::TopicId;
// Re-export init function
pub use iroh_sync::init_iroh_gossip;
use parking_lot::Mutex;
use rusqlite::Connection;
use systems::*;
fn main() {
@@ -29,11 +33,11 @@ fn main() {
// Load configuration and initialize database
let (config, us_db) = match initialize_app() {
Ok(data) => data,
Err(e) => {
| Ok(data) => data,
| Err(e) => {
eprintln!("Failed to initialize app: {}", e);
return;
}
},
};
// Create a topic ID for gossip (use a fixed topic for now)
@@ -85,8 +89,7 @@ fn initialize_app() -> Result<(Config, Arc<Mutex<Connection>>)> {
// Initialize database
println!("Initializing database at {}", config.database.path);
let conn =
Connection::open(&config.database.path).context("Failed to open database")?;
let conn = Connection::open(&config.database.path).context("Failed to open database")?;
db::initialize_database(&conn).context("Failed to initialize database schema")?;

View File

@@ -1,5 +1,11 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use chrono::{
DateTime,
Utc,
};
use serde::{
Deserialize,
Serialize,
};
/// Represents a message stored in our database
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -1,13 +1,30 @@
use crate::db;
use anyhow::{Context, Result};
use std::{
path::Path,
sync::Arc,
time::Duration,
};
use anyhow::{
Context,
Result,
};
use chrono::Utc;
use rusqlite::Connection;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tokio::time;
use tracing::{debug, error, info, warn};
use tokio::{
sync::{
Mutex,
mpsc,
},
time,
};
use tracing::{
debug,
error,
info,
warn,
};
use crate::db;
pub struct ChatPollerService {
chat_db_path: String,
@@ -33,12 +50,15 @@ impl ChatPollerService {
pub async fn run(&self) -> Result<()> {
info!("Starting chat poller service");
info!("Polling {} every {:?}", self.chat_db_path, self.poll_interval);
info!(
"Polling {} every {:?}",
self.chat_db_path, self.poll_interval
);
// Get last processed rowid from database
let us_db = self.us_db.lock().await;
let mut last_rowid = db::get_last_processed_rowid(&us_db)
.context("Failed to get last processed rowid")?;
let mut last_rowid =
db::get_last_processed_rowid(&us_db).context("Failed to get last processed rowid")?;
drop(us_db);
info!("Starting from rowid: {}", last_rowid);
@@ -49,7 +69,7 @@ impl ChatPollerService {
interval.tick().await;
match self.poll_messages(last_rowid).await {
Ok(new_messages) => {
| Ok(new_messages) => {
if !new_messages.is_empty() {
info!("Found {} new messages", new_messages.len());
@@ -74,10 +94,10 @@ impl ChatPollerService {
} else {
debug!("No new messages");
}
}
Err(e) => {
},
| Err(e) => {
error!("Error polling messages: {}", e);
}
},
}
}
}
@@ -85,12 +105,14 @@ impl ChatPollerService {
async fn poll_messages(&self, last_rowid: i64) -> Result<Vec<lib::Message>> {
// Check if chat.db exists
if !Path::new(&self.chat_db_path).exists() {
return Err(anyhow::anyhow!("chat.db not found at {}", self.chat_db_path));
return Err(anyhow::anyhow!(
"chat.db not found at {}",
self.chat_db_path
));
}
// Open chat.db (read-only)
let chat_db = lib::ChatDb::open(&self.chat_db_path)
.context("Failed to open chat.db")?;
let chat_db = lib::ChatDb::open(&self.chat_db_path).context("Failed to open chat.db")?;
// Get messages with rowid > last_rowid
// We'll use the existing get_our_messages but need to filter by rowid

View File

@@ -1,9 +1,18 @@
use crate::db;
use std::sync::Arc;
use anyhow::Result;
use rusqlite::Connection;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tracing::{error, info, warn};
use tokio::sync::{
Mutex,
mpsc,
};
use tracing::{
error,
info,
warn,
};
use crate::db;
/// Service responsible for generating embeddings for messages and words
pub struct EmbeddingService {
@@ -47,11 +56,11 @@ impl EmbeddingService {
// Get message ID from our database
let us_db = self.us_db.lock().await;
let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? {
Some(id) => id,
None => {
| Some(id) => id,
| None => {
warn!("Message {} not found in database, skipping", msg.rowid);
return Ok(());
}
},
};
// Check if embedding already exists
@@ -61,8 +70,8 @@ impl EmbeddingService {
// Skip if message has no text
let text = match &msg.text {
Some(t) if !t.is_empty() => t,
_ => return Ok(()),
| Some(t) if !t.is_empty() => t,
| _ => return Ok(()),
};
drop(us_db);

View File

@@ -1,9 +1,18 @@
use crate::db;
use std::sync::Arc;
use anyhow::Result;
use rusqlite::Connection;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tracing::{error, info, warn};
use tokio::sync::{
Mutex,
mpsc,
};
use tracing::{
error,
info,
warn,
};
use crate::db;
/// Service responsible for classifying emotions in messages
pub struct EmotionService {
@@ -56,11 +65,11 @@ impl EmotionService {
// Get message ID from our database
let us_db = self.us_db.lock().await;
let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? {
Some(id) => id,
None => {
| Some(id) => id,
| None => {
warn!("Message {} not found in database, skipping", msg.rowid);
return Ok(());
}
},
};
// Check if emotion classification already exists
@@ -70,8 +79,8 @@ impl EmotionService {
// Skip if message has no text
let text = match &msg.text {
Some(t) if !t.is_empty() => t,
_ => return Ok(()),
| Some(t) if !t.is_empty() => t,
| _ => return Ok(()),
};
drop(us_db);
@@ -82,7 +91,13 @@ impl EmotionService {
// Store emotion classification
let us_db = self.us_db.lock().await;
db::insert_emotion(&us_db, message_id, &emotion, confidence, &self.model_version)?;
db::insert_emotion(
&us_db,
message_id,
&emotion,
confidence,
&self.model_version,
)?;
// Randomly add to training set based on sample rate
if rand::random::<f64>() < self.training_sample_rate {

View File

@@ -4,4 +4,4 @@ pub mod emotion_service;
pub use chat_poller::ChatPollerService;
pub use embedding_service::EmbeddingService;
pub use emotion_service::EmotionService;
pub use emotion_service::EmotionService;

View File

@@ -3,10 +3,7 @@ use bevy::prelude::*;
use crate::components::*;
/// System: Poll chat.db for new messages using Bevy's task system
pub fn poll_chat_db(
_config: Res<AppConfig>,
_db: Res<Database>,
) {
pub fn poll_chat_db(_config: Res<AppConfig>, _db: Res<Database>) {
// TODO: Use Bevy's AsyncComputeTaskPool to poll chat.db
// This will replace the tokio::spawn chat poller
}

View File

@@ -1,17 +1,17 @@
use std::sync::Arc;
use bevy::prelude::*;
use parking_lot::Mutex;
use std::sync::Arc;
use crate::components::*;
/// System: Poll the gossip init task and insert resources when complete
pub fn poll_gossip_init(
mut commands: Commands,
mut init_task: Option<ResMut<GossipInitTask>>,
) {
pub fn poll_gossip_init(mut commands: Commands, mut init_task: Option<ResMut<GossipInitTask>>) {
if let Some(mut task) = init_task {
// Check if the task is finished (non-blocking)
if let Some(result) = bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.0)) {
if let Some(result) =
bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.0))
{
if let Some((endpoint, gossip, router, sender, receiver)) = result {
println!("Inserting gossip resources");
@@ -72,17 +72,19 @@ pub fn publish_to_gossip(
// Serialize the message
match serialize_sync_message(&sync_message) {
Ok(bytes) => {
| Ok(bytes) => {
// TODO: Publish to gossip
// For now, just log that we would publish
println!("Would publish {} bytes to gossip", bytes.len());
// Note: Direct async broadcasting from Bevy systems is tricky due to Sync requirements
// We'll need to use a different approach, possibly with channels or a dedicated task
}
Err(e) => {
// Note: Direct async broadcasting from Bevy systems is tricky
// due to Sync requirements We'll need to use a
// different approach, possibly with channels or a dedicated
// task
},
| Err(e) => {
eprintln!("Failed to serialize sync message: {}", e);
}
},
}
}
}
@@ -98,19 +100,18 @@ pub fn receive_from_gossip(
}
// TODO: Implement proper async message reception
// This will require spawning a long-running task that listens for gossip events
// and sends them as Bevy messages. For now, this is a placeholder.
// This will require spawning a long-running task that listens for gossip
// events and sends them as Bevy messages. For now, this is a
// placeholder.
}
/// System: Save received gossip messages to SQLite
pub fn save_gossip_messages(
mut events: MessageReader<GossipMessageReceived>,
_db: Res<Database>,
) {
pub fn save_gossip_messages(mut events: MessageReader<GossipMessageReceived>, _db: Res<Database>) {
for event in events.read() {
println!("Received message {} from gossip (published by {})",
event.sync_message.message.rowid,
event.sync_message.publisher_node_id);
println!(
"Received message {} from gossip (published by {})",
event.sync_message.message.rowid, event.sync_message.publisher_node_id
);
// TODO: Save to SQLite if we don't already have it
}
}

View File

@@ -1,5 +1,7 @@
use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool;
use bevy::{
prelude::*,
tasks::AsyncComputeTaskPool,
};
use crate::components::*;