blob: aab2c8b08fdf6cd6508ba062ae61201fa45c4ee0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::search::History;
use super::{
rank::{Hit, Source, excluded},
reader::{Reader, failed},
types::{Passage, PassageMessage},
};
use maka_agent_facts::tools::ToolError;
use maka_agent_ports::session::history::Chunk;
use maka_capabilities::call::Scope;
use std::sync::Arc;
use unicode_normalization::UnicodeNormalization;
pub(super) async fn build(
history: Arc<dyn History>,
call: &Scope,
sources: &[Source],
hits: &[Hit],
terms: &[String],
span: (usize, usize),
) -> Result<Vec<Passage>, ToolError> {
let mut passages: Vec<_> = hits
.iter()
.map(|hit| Passage {
session_id: sources[hit.session].summary.id.clone(),
session_title: sources[hit.session].summary.title.clone(),
anchor_message_id: hit.message_id.clone(),
anchor_position: hit.row_position,
turn_id: hit.turn_id.clone(),
messages: Vec::new(),
matched_terms: terms
.iter()
.zip(&hit.frequencies)
.filter_map(|(term, count)| (*count > 0).then_some(term.clone()))
.collect(),
score: hit.score,
has_more_before: false,
has_more_after: false,
truncated: false,
})
.collect();
for (session, source) in sources.iter().enumerate() {
if !hits.iter().any(|hit| hit.session == session) {
continue;
}
let mut reader = Reader::new(
history.clone(),
call.clone(),
source.summary.id.clone(),
source.through,
);
let mut position = 0;
while let Some(message) = reader.next().await? {
let ordinal = position;
position += 1;
if excluded(call, &source.summary.id, &message) {
continue;
}
let mut targets = Vec::new();
for (index, hit) in hits
.iter()
.enumerate()
.filter(|(_, hit)| hit.session == session)
{
if ordinal < hit.position.saturating_sub(span.0) {
passages[index].has_more_before = true;
} else if ordinal > hit.position.saturating_add(span.1) {
passages[index].has_more_after = true;
} else {
let anchor = ordinal == hit.position;
targets.push((index, anchor, if anchor { hit.offset } else { 0 }));
}
}
if targets.is_empty() {
continue;
}
let excerpts = tokio::task::spawn_blocking(move || {
let text: String = message.text.nfc().collect();
targets
.into_iter()
.map(|(index, anchor, offset)| {
(index, excerpt(&message, &text, anchor, offset))
})
.collect::<Vec<_>>()
})
.await
.map_err(failed)?;
for (index, excerpt) in excerpts {
passages[index].messages.push(excerpt);
}
}
}
for passage in &mut passages {
if !passage.messages.iter().any(|message| message.is_anchor) {
return Err(failed("Recall anchor is no longer readable"));
}
// Keep the anchor first in the byte allocation, then nearest neighbors.
let anchor = passage
.messages
.iter()
.position(|message| message.is_anchor)
.unwrap();
let mut order: Vec<_> = (0..passage.messages.len()).collect();
order.sort_by_key(|index| index.abs_diff(anchor));
let mut budget = 12 * 1024;
let mut keep = std::collections::BTreeSet::new();
for index in order {
let size = passage.messages[index].text.len()
+ 256
+ passage.messages[index]
.attachments
.iter()
.map(|a| a.text_bytes() + 128)
.sum::<usize>();
if index == anchor || size <= budget {
keep.insert(index);
budget = budget.saturating_sub(size);
} else {
passage.truncated = true;
if index < anchor {
passage.has_more_before = true;
} else {
passage.has_more_after = true;
}
}
}
let mut index = 0;
passage.messages.retain(|_| {
let retain = keep.contains(&index);
index += 1;
retain
});
passage.truncated |= passage
.messages
.iter()
.any(|message| message.offset > 0 || message.next_offset.is_some());
}
Ok(passages)
}
fn excerpt(message: &Chunk, text: &str, anchor: bool, requested: usize) -> PassageMessage {
let offset = text.floor_char_boundary(requested.min(text.len()));
let end = text.floor_char_boundary(offset.saturating_add(4 * 1024).min(text.len()));
PassageMessage {
message_id: message.message_id.clone(),
role: message.role,
text: text[offset..end].into(),
timestamp: message.timestamp,
is_anchor: anchor,
offset,
next_offset: (end < text.len()).then_some(end),
attachments: message.attachments.clone(),
}
}