configurable model (default sonnet 4.6), timestamps in chat log
This commit is contained in:
parent
84bb5165ef
commit
5ab592071e
1 changed files with 50 additions and 12 deletions
62
src/main.rs
62
src/main.rs
|
|
@ -29,8 +29,11 @@ struct Config {
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
rate_limit_per_min: Option<u32>,
|
rate_limit_per_min: Option<u32>,
|
||||||
|
model: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MODEL: &str = "claude-sonnet-4-6";
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct PersistedSession {
|
struct PersistedSession {
|
||||||
homeserver: String,
|
homeserver: String,
|
||||||
|
|
@ -45,6 +48,8 @@ struct ChatMessage {
|
||||||
sender: OwnedUserId,
|
sender: OwnedUserId,
|
||||||
body: String,
|
body: String,
|
||||||
is_self: bool,
|
is_self: bool,
|
||||||
|
/// Unix seconds. 0 if unknown.
|
||||||
|
ts: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DaemonState {
|
struct DaemonState {
|
||||||
|
|
@ -56,6 +61,7 @@ struct DaemonState {
|
||||||
rate_budget: u32,
|
rate_budget: u32,
|
||||||
rate_limit_per_min: u32,
|
rate_limit_per_min: u32,
|
||||||
last_rate_reset: std::time::Instant,
|
last_rate_reset: std::time::Instant,
|
||||||
|
model: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_HISTORY: usize = 20;
|
const MAX_HISTORY: usize = 20;
|
||||||
|
|
@ -82,6 +88,10 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let rate_limit_per_min = config
|
let rate_limit_per_min = config
|
||||||
.rate_limit_per_min
|
.rate_limit_per_min
|
||||||
.unwrap_or(DEFAULT_RATE_LIMIT_PER_MIN);
|
.unwrap_or(DEFAULT_RATE_LIMIT_PER_MIN);
|
||||||
|
let model = config
|
||||||
|
.model
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| DEFAULT_MODEL.to_owned());
|
||||||
|
|
||||||
let (client, sync_token) = if session_file.exists() {
|
let (client, sync_token) = if session_file.exists() {
|
||||||
restore_session(&session_file).await?
|
restore_session(&session_file).await?
|
||||||
|
|
@ -90,7 +100,12 @@ async fn main() -> anyhow::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let own_user_id = client.user_id().context("not logged in")?.to_owned();
|
let own_user_id = client.user_id().context("not logged in")?.to_owned();
|
||||||
tracing::info!(user = %own_user_id, rate_limit = rate_limit_per_min, "ready");
|
tracing::info!(
|
||||||
|
user = %own_user_id,
|
||||||
|
rate_limit = rate_limit_per_min,
|
||||||
|
model = %model,
|
||||||
|
"ready"
|
||||||
|
);
|
||||||
|
|
||||||
// Enable persistent event cache (matrix-sdk's sqlite store keeps the timeline)
|
// Enable persistent event cache (matrix-sdk's sqlite store keeps the timeline)
|
||||||
client
|
client
|
||||||
|
|
@ -105,6 +120,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
rate_budget: rate_limit_per_min,
|
rate_budget: rate_limit_per_min,
|
||||||
rate_limit_per_min,
|
rate_limit_per_min,
|
||||||
last_rate_reset: std::time::Instant::now(),
|
last_rate_reset: std::time::Instant::now(),
|
||||||
|
model,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let processor_state = state.clone();
|
let processor_state = state.clone();
|
||||||
|
|
@ -368,6 +384,19 @@ fn chrono_now() -> String {
|
||||||
format!("{y:04}-{m:02}-{d:02}")
|
format!("{y:04}-{m:02}-{d:02}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a unix-seconds timestamp as `YYYY-MM-DD HH:MM` UTC. Returns "?" for 0.
|
||||||
|
fn format_ts(secs: i64) -> String {
|
||||||
|
if secs == 0 {
|
||||||
|
return "?".into();
|
||||||
|
}
|
||||||
|
let days = secs.div_euclid(86400);
|
||||||
|
let day_secs = secs.rem_euclid(86400);
|
||||||
|
let (y, m, d) = days_to_ymd(days);
|
||||||
|
let h = day_secs / 3600;
|
||||||
|
let min = (day_secs % 3600) / 60;
|
||||||
|
format!("{y:04}-{m:02}-{d:02} {h:02}:{min:02}")
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert days-since-1970-01-01 to (year, month, day). Civil-date algorithm.
|
/// Convert days-since-1970-01-01 to (year, month, day). Civil-date algorithm.
|
||||||
fn days_to_ymd(z: i64) -> (i64, u32, u32) {
|
fn days_to_ymd(z: i64) -> (i64, u32, u32) {
|
||||||
let z = z + 719_468;
|
let z = z + 719_468;
|
||||||
|
|
@ -429,29 +458,30 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let own_user = {
|
let (own_user, model) = {
|
||||||
let state = state.lock().await;
|
let state = state.lock().await;
|
||||||
state.own_user_id.clone()
|
(state.own_user_id.clone(), state.model.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat_msgs: Vec<ChatMessage> = history
|
let chat_msgs: Vec<ChatMessage> = history
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, sender, body)| ChatMessage {
|
.map(|(_, sender, body, ts)| ChatMessage {
|
||||||
sender: sender.clone(),
|
sender: sender.clone(),
|
||||||
body: body.clone(),
|
body: body.clone(),
|
||||||
is_self: sender == &own_user,
|
is_self: sender == &own_user,
|
||||||
|
ts: *ts,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Determine seen split: everything before (and including) prev_last_shown is "seen"
|
// Determine seen split: everything before (and including) prev_last_shown is "seen"
|
||||||
let seen_idx = prev_last_shown
|
let seen_idx = prev_last_shown
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|id| history.iter().position(|(eid, _, _)| eid == id))
|
.and_then(|id| history.iter().position(|(eid, _, _, _)| eid == id))
|
||||||
.map_or(0, |pos| pos + 1);
|
.map_or(0, |pos| pos + 1);
|
||||||
|
|
||||||
let new_last_event_id = history.last().map(|(eid, _, _)| eid.clone());
|
let new_last_event_id = history.last().map(|(eid, _, _, _)| eid.clone());
|
||||||
|
|
||||||
match invoke_claude(&room_id, &room_name, &chat_msgs, seen_idx).await {
|
match invoke_claude(&room_id, &room_name, &chat_msgs, seen_idx, &model).await {
|
||||||
Ok(Some(response)) => {
|
Ok(Some(response)) => {
|
||||||
if let Some(target_room) = client.get_room(&response.room) {
|
if let Some(target_room) = client.get_room(&response.room) {
|
||||||
let content = RoomMessageEventContent::text_plain(&response.body);
|
let content = RoomMessageEventContent::text_plain(&response.body);
|
||||||
|
|
@ -489,17 +519,17 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the last N text messages from the room's persistent event cache.
|
/// Load the last N text messages from the room's persistent event cache.
|
||||||
/// Returns oldest-first list of (event_id, sender, body).
|
/// Returns oldest-first list of (event_id, sender, body, ts_secs).
|
||||||
async fn load_recent_messages(
|
async fn load_recent_messages(
|
||||||
room: &Room,
|
room: &Room,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String)>> {
|
) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String, i64)>> {
|
||||||
use matrix_sdk::ruma::events::AnySyncTimelineEvent;
|
use matrix_sdk::ruma::events::AnySyncTimelineEvent;
|
||||||
|
|
||||||
let (cache, _handles) = room.event_cache().await?;
|
let (cache, _handles) = room.event_cache().await?;
|
||||||
let events = cache.events().await;
|
let events = cache.events().await;
|
||||||
|
|
||||||
let mut out: Vec<(OwnedEventId, OwnedUserId, String)> = Vec::new();
|
let mut out: Vec<(OwnedEventId, OwnedUserId, String, i64)> = Vec::new();
|
||||||
for ev in events.iter().rev() {
|
for ev in events.iter().rev() {
|
||||||
if out.len() >= limit {
|
if out.len() >= limit {
|
||||||
break;
|
break;
|
||||||
|
|
@ -514,10 +544,13 @@ async fn load_recent_messages(
|
||||||
) = msg
|
) = msg
|
||||||
{
|
{
|
||||||
if let MessageType::Text(text) = &orig.content.msgtype {
|
if let MessageType::Text(text) = &orig.content.msgtype {
|
||||||
|
let ts_ms: u64 = orig.origin_server_ts.0.into();
|
||||||
|
let ts_secs: i64 = i64::try_from(ts_ms).unwrap_or(0) / 1000;
|
||||||
out.push((
|
out.push((
|
||||||
orig.event_id.clone(),
|
orig.event_id.clone(),
|
||||||
orig.sender.clone(),
|
orig.sender.clone(),
|
||||||
text.body.clone(),
|
text.body.clone(),
|
||||||
|
ts_secs,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -538,6 +571,7 @@ async fn invoke_claude(
|
||||||
room_name: &str,
|
room_name: &str,
|
||||||
history: &[ChatMessage],
|
history: &[ChatMessage],
|
||||||
seen_idx: usize,
|
seen_idx: usize,
|
||||||
|
model: &str,
|
||||||
) -> anyhow::Result<Option<ClaudeResponse>> {
|
) -> anyhow::Result<Option<ClaudeResponse>> {
|
||||||
let identity_dir = paths::identity_dir();
|
let identity_dir = paths::identity_dir();
|
||||||
let identity_str = identity_dir.to_string_lossy();
|
let identity_str = identity_dir.to_string_lossy();
|
||||||
|
|
@ -577,7 +611,8 @@ async fn invoke_claude(
|
||||||
writeln!(prompt, "\n[previously seen messages — for context]").unwrap();
|
writeln!(prompt, "\n[previously seen messages — for context]").unwrap();
|
||||||
for msg in old {
|
for msg in old {
|
||||||
let prefix = if msg.is_self { "(you) " } else { "" };
|
let prefix = if msg.is_self { "(you) " } else { "" };
|
||||||
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap();
|
let ts = format_ts(msg.ts);
|
||||||
|
writeln!(prompt, "[{ts}] {prefix}{}: {}", msg.sender, msg.body).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,7 +622,8 @@ async fn invoke_claude(
|
||||||
} else {
|
} else {
|
||||||
for msg in new {
|
for msg in new {
|
||||||
let prefix = if msg.is_self { "(you) " } else { "" };
|
let prefix = if msg.is_self { "(you) " } else { "" };
|
||||||
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap();
|
let ts = format_ts(msg.ts);
|
||||||
|
writeln!(prompt, "[{ts}] {prefix}{}: {}", msg.sender, msg.body).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,6 +634,8 @@ async fn invoke_claude(
|
||||||
let mut cmd = Command::new("claude");
|
let mut cmd = Command::new("claude");
|
||||||
cmd.args([
|
cmd.args([
|
||||||
"--print",
|
"--print",
|
||||||
|
"--model",
|
||||||
|
model,
|
||||||
"--add-dir",
|
"--add-dir",
|
||||||
&identity_str,
|
&identity_str,
|
||||||
"--allowedTools",
|
"--allowedTools",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue