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";
pub fn search(
fleet: &Fleet,
who: &Who<'_>,
query: &str,
hits: &[crate::Hit],
elapsed: std::time::Duration,
) {
let counter = fleet.counter();
if !enabled() {
return;
}
let excerpt: i64 = hits.iter().map(|h| counter.count(&h.text)).sum();
for (label, store) in fleet.each() {
let mine: Vec<&crate::Hit> = hits
.iter()
.filter(|h| h.store.as_deref().is_none_or(|s| s == label))
.collect();
if mine.is_empty() && !hits.is_empty() {
continue;
}
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(),
},
);
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(),
},
);
}
}
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();
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(),
},
);
}
}
#[derive(Debug, Clone, Copy)]
pub struct Who<'a> {
pub client: &'a str,
pub session: &'a str,
}