use std::sync::Arc;
use async_trait::async_trait;
use frunk::{HCons, HNil, hlist::HList};
use sea_orm::DatabaseConnection;
use serde_json::Value;
use crate::{
app::App,
capability::{ApplyHooks, CapStore, Capability, mount_with_hooks},
genai::FunctionDeclaration,
plugins::filesystem::storage::DynFilestore,
rune_env::RuneEnvCapability,
tag::Tagged,
traits::add::{AddCapability, CapTagAbsent},
};
pub struct LlmToolsTag;
pub struct ToolCtx<'a> {
pub db: &'a DatabaseConnection,
pub store: Arc<DynFilestore>,
pub cse_api_key: &'a str,
pub cse_cx: &'a str,
pub rune_env: &'a RuneEnvCapability,
pub session_id: Option<i64>,
}
#[async_trait]
pub trait LlmTool: Send + Sync {
fn name(&self) -> &str;
fn declaration(&self) -> FunctionDeclaration;
async fn run(&self, ctx: &ToolCtx<'_>, args: Value) -> Result<Value, String>;
}
pub type DynLlmTool = Arc<dyn LlmTool>;
pub trait ToolsRegistrar {
fn register_tools(self, tools: &mut LlmToolsCapability);
}
#[derive(Clone, Default)]
pub struct LlmToolsCapability {
tools: Vec<DynLlmTool>,
}
impl LlmToolsCapability {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, tool: impl LlmTool + 'static) -> &mut Self {
let name = tool.name().to_string();
let arc: DynLlmTool = Arc::new(tool);
if let Some(existing) = self.tools.iter_mut().find(|t| t.name() == name) {
*existing = arc;
} else {
self.tools.push(arc);
}
self
}
pub fn get(&self, name: &str) -> Option<DynLlmTool> {
self.tools.iter().find(|t| t.name() == name).cloned()
}
pub fn all(&self) -> &[DynLlmTool] {
&self.tools
}
pub fn declarations(&self) -> Vec<FunctionDeclaration> {
self.tools.iter().map(|t| t.declaration()).collect()
}
}
pub type LlmToolsCap<Hooks> = CapStore<LlmToolsTag, Hooks, LlmToolsCapability>;
impl<Hooks> LlmToolsCap<Hooks> {
pub fn resolve_hooks<Proof>(self) -> LlmToolsCap<HNil>
where
Hooks: ApplyHooks<LlmToolsCapability, Proof, Output = LlmToolsCapability>,
{
CapStore::with_items(self.hooks.apply_hooks(self.items))
}
}
impl<Plugin, H, Tail, TailProof> ApplyHooks<LlmToolsCapability, (TailProof, ())>
for HCons<Tagged<Plugin, H>, Tail>
where
Tail: ApplyHooks<LlmToolsCapability, TailProof, Output = LlmToolsCapability>,
H: ToolsRegistrar,
{
type Output = LlmToolsCapability;
fn apply_hooks(self, items: LlmToolsCapability) -> Self::Output {
let mut items = self.tail.apply_hooks(items);
self.head.value.register_tools(&mut items);
items
}
}
impl<Hooks> Capability for LlmToolsCap<Hooks>
where
Hooks: ApplyHooks<LlmToolsCapability, (), Output = LlmToolsCapability>,
{
type Value = Arc<LlmToolsCapability>;
type Output = Tagged<LlmToolsTag, Arc<LlmToolsCapability>>;
type Hooks = Hooks;
type Items = LlmToolsCapability;
fn mount(self) -> Self::Output {
mount_with_hooks(self, Arc::new)
}
}
pub fn with_llm_tools<L, Proof>(app: App<L>) -> App<HCons<LlmToolsCap<HNil>, L>>
where
L: HList + CapTagAbsent<LlmToolsTag, Proof>,
{
app.add_capability(CapStore::with_items(LlmToolsCapability::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genai::FunctionDeclaration;
struct DummyTool(&'static str);
#[async_trait]
impl LlmTool for DummyTool {
fn name(&self) -> &str {
self.0
}
fn declaration(&self) -> FunctionDeclaration {
FunctionDeclaration {
name: self.0.into(),
description: "dummy".into(),
parameters: None,
}
}
async fn run(&self, _ctx: &ToolCtx<'_>, _args: Value) -> Result<Value, String> {
Ok(Value::Null)
}
}
#[test]
fn register_get_upsert() {
let mut cap = LlmToolsCapability::new();
cap.register(DummyTool("a")).register(DummyTool("b"));
assert_eq!(cap.all().len(), 2);
assert!(cap.get("a").is_some());
cap.register(DummyTool("a"));
assert_eq!(cap.all().len(), 2);
assert_eq!(cap.declarations().len(), 2);
}
}