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,
|
||||
password: String,
|
||||
rate_limit_per_min: Option<u32>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL: &str = "claude-sonnet-4-6";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct PersistedSession {
|
||||
homeserver: String,
|
||||
|
|
@ -45,6 +48,8 @@ struct ChatMessage {
|
|||
sender: OwnedUserId,
|
||||
body: String,
|
||||
is_self: bool,
|
||||
/// Unix seconds. 0 if unknown.
|
||||
ts: i64,
|
||||
}
|
||||
|
||||
struct DaemonState {
|
||||
|
|
@ -56,6 +61,7 @@ struct DaemonState {
|
|||
rate_budget: u32,
|
||||
rate_limit_per_min: u32,
|
||||
last_rate_reset: std::time::Instant,
|
||||
model: String,
|
||||
}
|
||||
|
||||
const MAX_HISTORY: usize = 20;
|
||||
|
|
@ -82,6 +88,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
let rate_limit_per_min = config
|
||||
.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() {
|
||||
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();
|
||||
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)
|
||||
client
|
||||
|
|
@ -105,6 +120,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
rate_budget: rate_limit_per_min,
|
||||
rate_limit_per_min,
|
||||
last_rate_reset: std::time::Instant::now(),
|
||||
model,
|
||||
}));
|
||||
|
||||
let processor_state = state.clone();
|
||||
|
|
@ -368,6 +384,19 @@ fn chrono_now() -> String {
|
|||
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.
|
||||
fn days_to_ymd(z: i64) -> (i64, u32, u32) {
|
||||
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;
|
||||
state.own_user_id.clone()
|
||||
(state.own_user_id.clone(), state.model.clone())
|
||||
};
|
||||
|
||||
let chat_msgs: Vec<ChatMessage> = history
|
||||
.iter()
|
||||
.map(|(_, sender, body)| ChatMessage {
|
||||
.map(|(_, sender, body, ts)| ChatMessage {
|
||||
sender: sender.clone(),
|
||||
body: body.clone(),
|
||||
is_self: sender == &own_user,
|
||||
ts: *ts,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Determine seen split: everything before (and including) prev_last_shown is "seen"
|
||||
let seen_idx = prev_last_shown
|
||||
.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);
|
||||
|
||||
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)) => {
|
||||
if let Some(target_room) = client.get_room(&response.room) {
|
||||
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.
|
||||
/// 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(
|
||||
room: &Room,
|
||||
limit: usize,
|
||||
) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String)>> {
|
||||
) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String, i64)>> {
|
||||
use matrix_sdk::ruma::events::AnySyncTimelineEvent;
|
||||
|
||||
let (cache, _handles) = room.event_cache().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() {
|
||||
if out.len() >= limit {
|
||||
break;
|
||||
|
|
@ -514,10 +544,13 @@ async fn load_recent_messages(
|
|||
) = msg
|
||||
{
|
||||
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((
|
||||
orig.event_id.clone(),
|
||||
orig.sender.clone(),
|
||||
text.body.clone(),
|
||||
ts_secs,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -538,6 +571,7 @@ async fn invoke_claude(
|
|||
room_name: &str,
|
||||
history: &[ChatMessage],
|
||||
seen_idx: usize,
|
||||
model: &str,
|
||||
) -> anyhow::Result<Option<ClaudeResponse>> {
|
||||
let identity_dir = paths::identity_dir();
|
||||
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();
|
||||
for msg in old {
|
||||
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 {
|
||||
for msg in new {
|
||||
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");
|
||||
cmd.args([
|
||||
"--print",
|
||||
"--model",
|
||||
model,
|
||||
"--add-dir",
|
||||
&identity_str,
|
||||
"--allowedTools",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue