Adding some IPC.

This commit is contained in:
Micheal Smith
2025-11-09 00:02:38 -06:00
parent 7f7981d6cd
commit 5d390ee9f3
11 changed files with 174 additions and 64 deletions

2
Cargo.lock generated
View File

@@ -1907,6 +1907,8 @@ dependencies = [
"genai", "genai",
"human-panic", "human-panic",
"irc", "irc",
"serde",
"serde_json",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",

View File

@@ -14,7 +14,9 @@ futures = "0.3"
human-panic = "2.0" human-panic = "2.0"
genai = "0.4.3" genai = "0.4.3"
irc = "1.1" irc = "1.1"
tokio = { version = "1", features = [ "macros", "rt-multi-thread" ] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = [ "io-util", "macros", "net", "rt-multi-thread", "sync" ] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"

View File

@@ -3,5 +3,5 @@ style_edition = "2024"
comment_width = 100 comment_width = 100
format_code_in_doc_comments = true format_code_in_doc_comments = true
imports_granularity = "Crate" imports_granularity = "Crate"
imports_layout = "Vertical" imports_layout = "HorizontalVertical"
wrap_comments = true wrap_comments = true

View File

@@ -1,24 +1,10 @@
use color_eyre::{ use color_eyre::{Result, eyre::WrapErr};
Result,
eyre::{
OptionExt,
WrapErr,
},
};
// Lots of namespace confusion potential // Lots of namespace confusion potential
use crate::qna::LLMHandle; use crate::qna::LLMHandle;
use config::Config as MainConfig; use config::Config as MainConfig;
use futures::StreamExt; use futures::StreamExt;
use irc::client::prelude::{ use irc::client::prelude::{Client as IRCClient, Command, Config as IRCConfig};
Client as IRCClient, use tracing::{Level, event, instrument};
Command,
Config as IRCConfig,
};
use tracing::{
Level,
event,
instrument,
};
#[derive(Debug)] #[derive(Debug)]
pub struct Chat { pub struct Chat {
@@ -73,7 +59,9 @@ impl Chat {
// Make it all one line. // Make it all one line.
msg.retain(|c| c != '\n' && c != '\r'); msg.retain(|c| c != '\n' && c != '\r');
msg.truncate(500); msg.truncate(500);
client.send_privmsg(&channel, msg).wrap_err("Could not send to {channel}")?; client
.send_privmsg(&channel, msg)
.wrap_err("Could not send to {channel}")?;
} }
} }

View File

@@ -1,8 +1,5 @@
use color_eyre::Result; use color_eyre::Result;
use std::path::{ use std::path::{Path, PathBuf};
Path,
PathBuf,
};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Root { pub struct Root {
@@ -16,7 +13,7 @@ impl Root {
} }
} }
pub fn run_command(cmd_string: impl AsRef<str>) -> Result<()> { pub fn run_command(_cmd_string: impl AsRef<str>) -> Result<()> {
todo!(); todo!();
} }
} }

14
src/event.rs Normal file
View File

@@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Event {
message: String,
}
impl Event {
pub fn new(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
}
}
}

89
src/event_manager.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use color_eyre::Result;
use tokio::{
io::AsyncWriteExt,
net::{UnixListener, UnixStream},
sync::{RwLock, broadcast},
};
use tracing::error;
use crate::event::Event;
// Hard coding for now. Maybe make this a parameter to new.
const EVENT_BUF_MAX: usize = 1000;
// Manager for communication with plugins.
pub struct EventManager {
announce: broadcast::Sender<String>, // Everything broadcasts here.
events: Arc<RwLock<VecDeque<String>>>, // Ring buffer.
}
impl EventManager {
pub fn new() -> Result<Self> {
let (announce, _) = broadcast::channel(100);
Ok(Self {
announce,
events: Arc::new(RwLock::new(VecDeque::<String>::new())),
})
}
pub async fn broadcast(&self, event: &Event) -> Result<()> {
let msg = serde_json::to_string(event)? + "\n";
let mut events = self.events.write().await;
if events.len() >= EVENT_BUF_MAX {
events.pop_front();
}
events.push_back(msg.clone());
drop(events);
let _ = self.announce.send(msg);
Ok(())
}
pub async fn start_listening(self: Arc<Self>, path: impl AsRef<Path>) {
let listener = UnixListener::bind(path).unwrap();
loop {
match listener.accept().await {
Ok((stream, _)) => {
// Spawn a new stream for the plugin. The loop
// runs recursively from there.
let broadcaster = Arc::clone(&self);
tokio::spawn(async move {
// send events.
let _ = broadcaster.send_events(stream).await;
});
}
Err(e) => error!("Accept error: {e}"),
}
}
}
async fn send_events(&self, stream: UnixStream) -> Result<()> {
let mut writer = stream;
// Take care of history.
let events = self.events.read().await;
for event in events.iter() {
writer.write_all(event.as_bytes()).await?;
}
drop(events);
// Now just broadcast the new events.
let mut rx = self.announce.subscribe();
while let Ok(event) = rx.recv().await {
if writer.write_all(event.as_bytes()).await.is_err() {
// *click*
break;
}
}
Ok(())
}
}

26
src/ipc.rs Normal file
View File

@@ -0,0 +1,26 @@
// Provides an IPC socket to communicate with other processes.
use std::path::Path;
use color_eyre::Result;
use tokio::net::UnixListener;
pub struct IPC {
listener: UnixListener,
}
impl IPC {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let listener = UnixListener::bind(path)?;
Ok(Self { listener })
}
pub async fn run(&self) -> Result<()> {
loop {
match self.listener.accept().await {
Ok((_stream, _addr)) => {}
Err(e) => return Err(e.into()),
}
}
}
}

View File

@@ -1,17 +1,14 @@
use color_eyre::{ use color_eyre::{Result, eyre::WrapErr};
Result,
eyre::WrapErr,
};
use human_panic::setup_panic; use human_panic::setup_panic;
use std::os::unix::fs; use std::{os::unix::fs, sync::Arc};
use tracing::{ use tracing::{Level, info};
Level,
info,
};
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
use crate::event_manager::EventManager;
mod chat; mod chat;
mod commands; mod event;
mod event_manager;
mod qna; mod qna;
mod setup; mod setup;
@@ -47,18 +44,17 @@ async fn main() -> Result<()> {
} }
// Setup root path for commands. // Setup root path for commands.
let cmd_root = if let Ok(command_path) = config.get_string("command-path") { // let cmd_root = if let Ok(command_path) = config.get_string("command-path") {
Some(commands::Root::new(command_path)) // Some(commands::Root::new(command_path))
} else { // } else {
None // None
}; // };
let handle = qna::LLMHandle::new( let handle = qna::LLMHandle::new(
config.get_string("api-key").wrap_err("API missing.")?, config.get_string("api-key").wrap_err("API missing.")?,
config config
.get_string("base-url") .get_string("base-url")
.wrap_err("base-url missing.")?, .wrap_err("base-url missing.")?,
cmd_root,
config config
.get_string("model") .get_string("model")
.wrap_err("model string missing.")?, .wrap_err("model string missing.")?,
@@ -67,9 +63,23 @@ async fn main() -> Result<()> {
.unwrap_or_else(|_| DEFAULT_INSTRUCT.to_string()), .unwrap_or_else(|_| DEFAULT_INSTRUCT.to_string()),
) )
.wrap_err("Couldn't initialize LLM handle.")?; .wrap_err("Couldn't initialize LLM handle.")?;
let ev_manager = Arc::new(EventManager::new()?);
let ev_manager_clone = Arc::clone(&ev_manager);
ev_manager_clone
.broadcast(&event::Event::new("Starting..."))
.await?;
let mut c = chat::new(&config, &handle).await?; let mut c = chat::new(&config, &handle).await?;
c.run().await.unwrap(); tokio::select! {
_ = ev_manager_clone.start_listening("/tmp/robo.sock") => {
// Event listener ended
}
result = c.run() => {
result.unwrap();
}
}
Ok(()) Ok(())
} }

View File

@@ -1,19 +1,10 @@
use crate::commands;
use color_eyre::Result; use color_eyre::Result;
use futures::StreamExt; use futures::StreamExt;
use genai::{ use genai::{
Client, Client,
ModelIden, ModelIden,
chat::{ chat::{ChatMessage, ChatRequest, ChatStreamEvent, StreamChunk},
ChatMessage, resolver::{AuthData, AuthResolver},
ChatRequest,
ChatStreamEvent,
StreamChunk,
},
resolver::{
AuthData,
AuthResolver,
},
}; };
use tracing::info; use tracing::info;
@@ -23,7 +14,6 @@ use tracing::info;
pub struct LLMHandle { pub struct LLMHandle {
chat_request: ChatRequest, chat_request: ChatRequest,
client: Client, client: Client,
cmd_root: Option<commands::Root>,
model: String, model: String,
} }
@@ -31,7 +21,6 @@ impl LLMHandle {
pub fn new( pub fn new(
api_key: String, api_key: String,
_base_url: impl AsRef<str>, _base_url: impl AsRef<str>,
cmd_root: Option<commands::Root>,
model: impl Into<String>, model: impl Into<String>,
system_role: String, system_role: String,
) -> Result<LLMHandle> { ) -> Result<LLMHandle> {
@@ -51,7 +40,6 @@ impl LLMHandle {
Ok(LLMHandle { Ok(LLMHandle {
client, client,
chat_request, chat_request,
cmd_root,
model: model.into(), model: model.into(),
}) })
} }

View File

@@ -1,15 +1,9 @@
use clap::Parser; use clap::Parser;
use color_eyre::{ use color_eyre::{Result, eyre::WrapErr};
Result,
eyre::WrapErr,
};
use config::Config; use config::Config;
use directories::ProjectDirs; use directories::ProjectDirs;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{ use tracing::{info, instrument};
info,
instrument,
};
// TODO: use [clap(long, short, help_heading = Some(section))] // TODO: use [clap(long, short, help_heading = Some(section))]
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]