use crate::catalog::{Catalog, ToolDescriptor, ToolId};
use crate::config::Config;
use crate::embed::EMBEDDING_DIMENSIONS;
use crate::error::{BuildError, IndexError, QueryError, SelectionError};
use crate::model::Model;
use crate::policy::{self, Outcome, Shortlist};
use crate::rank::Index;
use crate::selected::{self, NearDuplicates};
#[cfg(test)]
mod tests;
#[non_exhaustive]
pub struct ToolPicker {
catalog: Catalog,
config: Config,
model: Model,
index: Index,
}
impl ToolPicker {
#[must_use = "a picker that is built and dropped did its costly work for nothing"]
pub fn build(catalog: Catalog, config: Config) -> Result<Self, BuildError> {
let model = Model::load()?;
Ok(Self::build_with_model(&model, catalog, config)?)
}
#[must_use = "a picker that is built and dropped did its work for nothing"]
pub fn build_with_model(
model: &Model,
catalog: Catalog,
config: Config,
) -> Result<Self, IndexError> {
let mut rows = Vec::with_capacity(catalog.len().saturating_mul(EMBEDDING_DIMENSIONS));
for tool in catalog.as_slice() {
let vector = model
.embed(&tool.enriched_text())
.map_err(IndexError::embed)?;
rows.extend_from_slice(&vector);
}
let index =
Index::new(rows, EMBEDDING_DIMENSIONS, catalog.len()).map_err(IndexError::layout)?;
Ok(Self {
catalog,
config,
model: model.clone(),
index,
})
}
#[must_use = "rebuild returns a new picker; the original is left unchanged"]
pub fn rebuild(&self, catalog: Catalog) -> Result<Self, IndexError> {
Self::build_with_model(&self.model, catalog, self.config.clone())
}
#[must_use]
pub fn len(&self) -> usize {
self.catalog.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.catalog.is_empty()
}
#[must_use = "iterators are lazy and visit nothing unless consumed"]
pub fn iter(&self) -> ToolIter<'_> {
ToolIter {
inner: self.catalog.as_slice().iter(),
}
}
#[must_use]
pub fn get(&self, id: &ToolId) -> Option<&ToolDescriptor> {
self.catalog.get(id)
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
pub fn resolve(&self, need: &str) -> Result<Outcome<'_>, QueryError> {
let query = self.model.embed(need)?;
let ranked = self.index.top_k(&query, self.config.top_k().get().max(2));
Ok(policy::decide(
&ranked,
self.catalog.as_slice(),
self.index.vectors(),
&self.config,
))
}
pub fn shortlist(&self, need: &str, limit: usize) -> Result<Shortlist<'_>, QueryError> {
if limit == 0 {
return Ok(policy::shortlist(
&[],
self.catalog.as_slice(),
&self.config,
));
}
let query = self.model.embed(need)?;
let ranked = self.index.top_k(&query, limit);
Ok(policy::shortlist(
&ranked,
self.catalog.as_slice(),
&self.config,
))
}
pub fn near_duplicates(&self, ids: &[ToolId]) -> Result<NearDuplicates<'_>, SelectionError> {
selected::near_duplicates(
self.catalog.as_slice(),
self.index.vectors(),
self.config.duplicate_threshold(),
ids,
)
}
#[cfg(test)]
pub(crate) fn shares_model(&self, other: &ToolPicker) -> bool {
self.model.shares_encoder(&other.model)
}
#[cfg(test)]
pub(crate) fn row(&self, index: usize) -> Option<&[f32]> {
self.index.vectors().row(index)
}
}
impl std::fmt::Debug for ToolPicker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolPicker")
.field("tools", &self.len())
.field("dimensions", &EMBEDDING_DIMENSIONS)
.field("model", &self.model)
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl<'a> IntoIterator for &'a ToolPicker {
type Item = &'a ToolDescriptor;
type IntoIter = ToolIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Debug, Clone)]
pub struct ToolIter<'a> {
inner: std::slice::Iter<'a, ToolDescriptor>,
}
impl<'a> Iterator for ToolIter<'a> {
type Item = &'a ToolDescriptor;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for ToolIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl ExactSizeIterator for ToolIter<'_> {}
impl std::iter::FusedIterator for ToolIter<'_> {}