lash-plugin-tool-discovery 0.1.0-alpha.39

Tool-discovery plugin for the lash agent runtime.
Documentation
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};

use serde_json::{Value, json};

pub(crate) const DEFAULT_LIMIT: usize = 10;
pub(crate) const MAX_LIMIT: usize = 100;
pub(crate) const LLM_CANDIDATE_LIMIT: usize = 100;
pub(crate) const FUZZY_SCORE_CAP: f64 = 1.25;
pub(crate) const SEMANTIC_CANDIDATE_FLOOR: usize = 50;
pub(crate) const RRF_K: f64 = 60.0;

pub(crate) fn tokenize(text: &str) -> Vec<String> {
    text.split(|ch: char| !ch.is_ascii_alphanumeric())
        .filter(|token| !token.is_empty())
        .map(|token| token.to_ascii_lowercase())
        .collect()
}

pub(crate) fn limit_from_args(args: &Value) -> usize {
    args.get("limit")
        .and_then(Value::as_i64)
        .and_then(|value| usize::try_from(value).ok())
        .map(|value| value.clamp(1, MAX_LIMIT))
        .unwrap_or(DEFAULT_LIMIT)
}

pub(crate) fn args_with_limit(args: &Value, limit: usize) -> Value {
    let mut args = args.as_object().cloned().unwrap_or_default();
    args.insert("limit".to_string(), json!(limit.clamp(1, MAX_LIMIT)));
    args.insert("debug".to_string(), json!(false));
    Value::Object(args)
}

pub(crate) fn module_filter(value: Option<&Value>) -> Vec<String> {
    match value {
        Some(Value::String(module)) => module
            .split(',')
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        Some(Value::Array(values)) => values
            .iter()
            .filter_map(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        _ => Vec::new(),
    }
}

pub(crate) fn exclude_filter(value: Option<&Value>) -> BTreeSet<String> {
    match value {
        Some(Value::String(name)) => name
            .split(',')
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        Some(Value::Array(values)) => values
            .iter()
            .filter_map(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        _ => BTreeSet::new(),
    }
}

pub(crate) fn round_score(score: f64) -> f64 {
    (score * 100.0).round() / 100.0
}

pub(crate) fn string_vec(value: Option<&Value>) -> Vec<String> {
    match value {
        Some(Value::Array(items)) => items
            .iter()
            .filter_map(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect(),
        Some(Value::String(value)) => vec![value.trim().to_string()],
        _ => Vec::new(),
    }
}

pub(crate) fn catalog_key(catalog: &[Value]) -> u64 {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    catalog.len().hash(&mut hasher);
    for value in catalog {
        hash_json_value(value, &mut hasher);
    }
    hasher.finish()
}

fn hash_json_value(value: &Value, state: &mut impl Hasher) {
    match value {
        Value::Null => 0_u8.hash(state),
        Value::Bool(value) => {
            1_u8.hash(state);
            value.hash(state);
        }
        Value::Number(value) => {
            2_u8.hash(state);
            value.to_string().hash(state);
        }
        Value::String(value) => {
            3_u8.hash(state);
            value.hash(state);
        }
        Value::Array(values) => {
            4_u8.hash(state);
            values.len().hash(state);
            for value in values {
                hash_json_value(value, state);
            }
        }
        Value::Object(values) => {
            5_u8.hash(state);
            values.len().hash(state);
            for (key, value) in values {
                key.hash(state);
                hash_json_value(value, state);
            }
        }
    }
}