use ragrig::{AttachStrategy, AttachedDocument};
const RESERVED_PROMPT_TOKENS: usize = 1024;
const CHARS_PER_TOKEN: usize = 3;
const ATTACHMENT_PERCENT: usize = 70;
#[derive(Clone, Debug, Default)]
pub struct ScicoAttach {
context_tokens: Option<usize>,
}
impl ScicoAttach {
pub fn new() -> Self {
Self::default()
}
pub fn with_context_tokens(tokens: usize) -> Self {
Self {
context_tokens: Some(tokens),
}
}
pub fn budget_chars(&self) -> Option<usize> {
self.context_tokens.map(|tokens| {
tokens
.saturating_sub(RESERVED_PROMPT_TOKENS)
.saturating_mul(CHARS_PER_TOKEN)
})
}
fn attachment_budget(&self) -> usize {
self.budget_chars()
.map_or(usize::MAX, |budget| budget * ATTACHMENT_PERCENT / 100)
}
fn retrieval_budget(&self, attachments: &[AttachedDocument]) -> usize {
match self.budget_chars() {
None => usize::MAX,
Some(budget) if attachments.is_empty() => budget,
Some(budget) => budget - budget * ATTACHMENT_PERCENT / 100,
}
}
}
impl AttachStrategy for ScicoAttach {
fn merge(&self, retrieved: &str, attachments: &[AttachedDocument]) -> String {
let mut context = String::new();
if !attachments.is_empty() {
context.push_str("<scico:v1:context source=\"creator\">\n");
let mut remaining = self.attachment_budget();
let share = remaining / attachments.len();
for (index, attachment) in attachments.iter().enumerate() {
if is_container(&attachment.content) {
let content = attachment.content.trim_end();
remaining = remaining.saturating_sub(content.chars().count());
context.push_str(content);
context.push('\n');
continue;
}
let limit = if index + 1 == attachments.len() {
remaining
} else {
share.min(remaining)
};
let total = attachment.content.chars().count();
let (content, truncated) = truncate(&attachment.content, limit);
remaining = remaining.saturating_sub(content.chars().count());
context.push_str(&format!(
"<scico:v1:document name=\"{}\">{content}",
attribute(&attachment.name)
));
if truncated {
context.push_str(&format!(
"<scico:v1:truncated shown=\"{}\" total=\"{total}\"/>",
content.chars().count()
));
}
context.push_str("</scico:v1:document>\n");
}
context.push_str("</scico:v1:context>");
}
let retrieved = retrieved.trim_end_matches('\n');
let (retrieved, _) = truncate(retrieved, self.retrieval_budget(attachments));
if !retrieved.trim().is_empty() {
if !context.is_empty() {
context.push_str("\n\n");
}
context.push_str("<scico:v1:context source=\"corpus\">\n");
context.push_str(&retrieved);
context.push_str("\n</scico:v1:context>");
}
context
}
fn name(&self) -> &'static str {
"scico"
}
}
fn is_container(text: &str) -> bool {
text.trim_start().starts_with("<scico:v1")
}
fn truncate(text: &str, max: usize) -> (String, bool) {
let count = text.chars().count();
if count <= max {
return (text.to_owned(), false);
}
(text.chars().take(max).collect(), true)
}
fn attribute(value: &str) -> String {
value
.replace('&', "&")
.replace('"', """)
.replace('<', "<")
}
#[cfg(test)]
mod tests {
use super::*;
fn document(name: &str, content: &str) -> AttachedDocument {
AttachedDocument {
name: name.to_owned(),
content: content.to_owned(),
}
}
#[test]
fn attachments_and_retrieval_use_the_documented_containers() {
let merged = ScicoAttach::new().merge(
"[doc: Kano2025]\nretrieved passage",
&[
document("@Kano2025", "PAPER BODY"),
document("@Kobayashi2011", "OTHER BODY"),
],
);
assert_eq!(
merged,
"<scico:v1:context source=\"creator\">\n\
<scico:v1:document name=\"@Kano2025\">PAPER BODY</scico:v1:document>\n\
<scico:v1:document name=\"@Kobayashi2011\">OTHER BODY</scico:v1:document>\n\
</scico:v1:context>\n\n\
<scico:v1:context source=\"corpus\">\n\
[doc: Kano2025]\nretrieved passage\n\
</scico:v1:context>"
);
}
#[test]
fn empty_parts_do_not_leave_empty_containers() {
assert_eq!(ScicoAttach::new().merge("", &[]), "");
assert_eq!(
ScicoAttach::new().merge("chunks", &[]),
"<scico:v1:context source=\"corpus\">\nchunks\n</scico:v1:context>"
);
assert_eq!(
ScicoAttach::new().merge("", &[document("@Key", "BODY")]),
"<scico:v1:context source=\"creator\">\n\
<scico:v1:document name=\"@Key\">BODY</scico:v1:document>\n\
</scico:v1:context>"
);
}
#[test]
fn prebuilt_containers_pass_through_verbatim() {
let selection = "<scico:v1:selection name=\"draft.md\" range=\"2..8\" chars=\"6\" digest=\"b3:abc\">\nmarked\n</scico:v1:selection>";
let merged = ScicoAttach::new().merge(
"",
&[document("ignored", selection), document("@Key", "PAPER")],
);
assert!(
merged.contains(selection),
"the richer container is kept: {merged}"
);
assert!(
!merged.contains("<scico:v1:document name=\"ignored\">"),
"a container is not wrapped twice: {merged}"
);
assert!(merged.contains("<scico:v1:document name=\"@Key\">PAPER</scico:v1:document>"));
}
#[test]
fn attribute_values_are_escaped() {
let merged = ScicoAttach::new().merge("", &[document("a\"<&", "BODY")]);
assert!(merged.contains("name=\"a"<&\""));
}
#[test]
fn a_token_budget_keeps_the_whole_context_within_the_window() {
let merged = ScicoAttach::with_context_tokens(1124)
.merge(&"r".repeat(1_000), &[document("@Key", &"a".repeat(1_000))]);
assert!(
merged
.contains("<scico:v1:truncated shown=\"210\" total=\"1000\"/></scico:v1:document>")
);
assert!(merged.contains(&format!("source=\"corpus\">\n{}\n", "r".repeat(90))));
let body = merged
.split_once("name=\"@Key\">")
.expect("document body")
.1
.split_once("<scico:v1:truncated")
.expect("truncation marker")
.0;
assert_eq!(body.chars().count(), 210);
}
#[test]
fn short_papers_do_not_get_a_marker_and_leave_room_for_retrieval() {
let merged = ScicoAttach::with_context_tokens(1124)
.merge(&"r".repeat(1_000), &[document("@Key", "SHORT")]);
assert!(!merged.contains("<scico:v1:truncated"));
assert!(merged.contains(">SHORT</scico:v1:document>"));
assert!(merged.contains(&"r".repeat(90)));
}
}