use super::{ChatEvent, ChatRequest, ChatStream, LlmProvider, Role, STREAM_BUFFER, ToolCall};
use crate::ctx::BoxFuture;
use serde_json::Value as Json;
use std::time::Duration;
#[derive(Clone, Debug)]
pub enum Say {
Text(String),
Tool(String, Json),
}
impl Say {
pub fn text(text: impl Into<String>) -> Self {
Say::Text(text.into())
}
pub fn tool(name: impl Into<String>, arguments: Json) -> Self {
Say::Tool(name.into(), arguments)
}
}
#[derive(Clone, Debug, Default)]
pub struct ScriptedProvider {
rules: Vec<(Vec<String>, Vec<Vec<Say>>)>,
otherwise: Vec<Vec<Say>>,
delay: Duration,
}
fn fold(text: &str) -> String {
text.to_lowercase()
.chars()
.filter(|c| *c != '\u{307}')
.map(|c| if c == 'ı' { 'i' } else { c })
.collect()
}
impl ScriptedProvider {
pub fn new() -> Self {
ScriptedProvider::default()
}
pub fn when(mut self, words: &str, rounds: Vec<Vec<Say>>) -> Self {
self.rules.push((
fold(words)
.split_whitespace()
.map(str::to_string)
.collect(),
rounds,
));
self
}
pub fn otherwise(mut self, rounds: Vec<Vec<Say>>) -> Self {
self.otherwise = rounds;
self
}
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
fn plan(&self, request: &ChatRequest) -> Vec<Say> {
let last_user = request
.messages
.iter()
.rposition(|m| m.role == Role::User);
let text = last_user
.map(|i| fold(&request.messages[i].text))
.unwrap_or_default();
let round = last_user.map_or(0, |i| {
request.messages[i..]
.iter()
.filter(|m| m.role == Role::Assistant)
.count()
});
if request
.messages
.last()
.is_some_and(|m| m.role == Role::Tool && m.text.contains("declined"))
{
return vec![Say::text("All right, I left it as it was.")];
}
let rounds = self
.rules
.iter()
.find(|(words, _)| {
words
.iter()
.all(|w| text.contains(w.as_str()))
})
.map_or(&self.otherwise, |(_, r)| r);
rounds
.get(round)
.cloned()
.unwrap_or_else(|| vec![Say::text("Done.")])
}
}
impl LlmProvider for ScriptedProvider {
fn chat(&self, request: ChatRequest) -> BoxFuture<crate::Result<ChatStream>> {
let plan = self.plan(&request);
let delay = self.delay;
let input = (request.system.len()
+ request
.messages
.iter()
.map(|m| m.text.len())
.sum::<usize>())
/ 4;
let (tx, rx) = tokio::sync::mpsc::channel(STREAM_BUFFER);
tokio::spawn(async move {
let mut output = 0;
for (i, say) in plan.into_iter().enumerate() {
match say {
Say::Text(text) => {
output += text.len() / 4;
for word in text.split_inclusive(' ') {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
if tx
.send(Ok(ChatEvent::Text(word.to_string())))
.await
.is_err()
{
return;
}
}
}
Say::Tool(name, arguments) => {
let call = ToolCall {
id: format!("script_{i}_{name}"),
name,
arguments,
};
if tx
.send(Ok(ChatEvent::ToolCall(call)))
.await
.is_err()
{
return;
}
}
}
}
let _ = tx
.send(Ok(ChatEvent::Usage {
input: input as u32,
output: output as u32,
}))
.await;
});
Box::pin(async move { Ok(rx) })
}
}