use crate::fleet::Fleet;
use crate::store;
pub const CHARS4: &str = "chars4";
pub const OFF_ENV: &str = "SEMLITH_LEDGER";
pub fn enabled() -> bool {
!matches!(
std::env::var(OFF_ENV).as_deref(),
Ok("0") | Ok("off") | Ok("false")
)
}
pub fn estimate(text: &str) -> i64 {
text.len().div_ceil(4) as i64
}
#[derive(Clone, Copy)]
pub enum Counter<'a> {
Model(&'a tokenizers::Tokenizer),
Chars4,
}
impl Counter<'_> {
pub fn count(&self, text: &str) -> i64 {
match self {
Counter::Model(tokenizer) => match tokenizer.encode(text, false) {
Ok(encoded) => encoded.len() as i64,
Err(_) => estimate(text),
},
Counter::Chars4 => estimate(text),
}
}
pub fn count_bytes(&self, bytes: u64) -> i64 {
bytes.div_ceil(4) as i64
}
pub fn label(&self) -> &'static str {
match self {
Counter::Model(_) => MODEL,
Counter::Chars4 => CHARS4,
}
}
}
pub const MODEL: &str = "model";
fn query_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{at:x}-{:x}", SEQ.fetch_add(1, Ordering::Relaxed))
}
pub fn search(
fleet: &Fleet,
who: &Who<'_>,
query: &str,
hits: &[crate::Hit],
elapsed: std::time::Duration,
) {
let counter = fleet.counter();
if !enabled() {
return;
}
let id = query_id();
let labelled = hits.iter().any(|h| h.store.is_some());
let mut first = true;
for (label, store) in fleet.each() {
let mine: Vec<&crate::Hit> = if labelled {
hits.iter()
.filter(|h| h.store.as_deref() == Some(label))
.collect()
} else if first {
hits.iter().collect()
} else {
Vec::new()
};
first = false;
if mine.is_empty() && !hits.is_empty() {
continue;
}
let excerpt: i64 = mine.iter().map(|h| counter.count(&h.text)).sum();
let paths: std::collections::BTreeSet<&str> =
mine.iter().map(|h| h.path.as_str()).collect();
let whole: i64 = paths
.iter()
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| counter.count_bytes(m.len()))
.sum();
let stale = mine.iter().filter(|h| !h.fresh).count() as i64;
let _ = store::record_retrieval(
store.db(),
&store::NewRetrieval {
client: who.client,
session: who.session,
tool: "search",
query,
hits: mine.len() as i64,
micros: elapsed.as_micros() as i64,
excerpt_tokens: excerpt,
whole_file_tokens: whole,
stale_hits: stale,
tokenizer: counter.label(),
query_id: &id,
},
);
if hits.is_empty() {
break;
}
}
}
pub fn reply(
fleet: &Fleet,
who: &Who<'_>,
tool: &str,
query: &str,
body: &str,
elapsed: std::time::Duration,
) {
if !enabled() {
return;
}
let counter = fleet.counter();
let paths = paths_in(body);
let whole: i64 = paths
.iter()
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| counter.count_bytes(m.len()))
.sum();
let hits = paths.len() as i64;
if let Some((_, store)) = fleet.each().next() {
let _ = store::record_retrieval(
store.db(),
&store::NewRetrieval {
client: who.client,
session: who.session,
tool,
query,
hits,
micros: elapsed.as_micros() as i64,
excerpt_tokens: counter.count(body),
whole_file_tokens: whole,
stale_hits: body.matches("· stale").count() as i64,
tokenizer: counter.label(),
query_id: &query_id(),
},
);
}
}
fn paths_in(body: &str) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
for line in body.lines() {
let trimmed = line
.trim_start_matches(|c: char| c == '[' || c.is_ascii_digit() || c == ']')
.trim();
if trimmed.is_empty() {
continue;
}
if !trimmed.contains(' ') && std::path::Path::new(trimmed).is_file() {
out.insert(trimmed.to_string());
continue;
}
if let Some((path, _)) = trimmed.split_once(':')
&& std::path::Path::new(path).is_file()
{
out.insert(path.to_string());
}
}
out
}
pub fn graph(
fleet: &Fleet,
who: &Who<'_>,
tool: &str,
name: &str,
reply: &str,
found: bool,
elapsed: std::time::Duration,
) {
if !enabled() {
return;
}
let counter = fleet.counter();
let id = query_id();
for (_, store) in fleet.each() {
let whole = match store::grep_cost(store.db(), name) {
Ok(bytes) => counter.count_bytes(bytes as u64),
Err(_) => 0,
};
if whole == 0 {
continue;
}
let _ = store::record_retrieval(
store.db(),
&store::NewRetrieval {
client: who.client,
session: who.session,
tool,
query: name,
hits: i64::from(found),
micros: elapsed.as_micros() as i64,
excerpt_tokens: counter.count(reply),
whole_file_tokens: whole,
stale_hits: 0,
tokenizer: counter.label(),
query_id: &id,
},
);
}
}
pub fn brief(
fleet: &Fleet,
who: &Who<'_>,
question: &str,
brief: &crate::brief::Brief,
body: &str,
elapsed: std::time::Duration,
) {
if !enabled() {
return;
}
let counter = fleet.counter();
let paths: std::collections::BTreeSet<&str> =
brief.spans.iter().map(|s| s.path.as_str()).collect();
let whole: i64 = paths
.iter()
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| counter.count_bytes(m.len()))
.sum();
if let Some((_, store)) = fleet.each().next() {
let _ = store::record_retrieval(
store.db(),
&store::NewRetrieval {
client: who.client,
session: who.session,
tool: "brief",
query: question,
hits: paths.len() as i64,
micros: elapsed.as_micros() as i64,
excerpt_tokens: counter.count(body),
whole_file_tokens: whole,
stale_hits: 0,
tokenizer: counter.label(),
query_id: &query_id(),
},
);
}
}
pub const RAW_READ: &str = "raw-read";
pub fn raw_read(fleet: &Fleet, client: &str, session: &str, path: &str) -> bool {
if !enabled() {
return false;
}
let counter = fleet.counter();
let whole = std::fs::metadata(path)
.map(|m| counter.count_bytes(m.len()))
.unwrap_or(0);
for (_, store) in fleet.each() {
if !store::holds_path(store.db(), path).unwrap_or(false) {
continue;
}
let recorded = store::record_retrieval(
store.db(),
&store::NewRetrieval {
client,
session,
tool: RAW_READ,
query: path,
hits: 0,
micros: 0,
excerpt_tokens: 0,
whole_file_tokens: whole,
stale_hits: 0,
tokenizer: CHARS4,
query_id: &query_id(),
},
);
return recorded.is_ok();
}
false
}
#[derive(Debug, Clone, Copy)]
pub struct Who<'a> {
pub client: &'a str,
pub session: &'a str,
}