use std::sync::Arc;
use parking_lot::Mutex;
use wabot_core::injection::Container;
use wabot_feature_chat_bot::{
ChatAdapter, ChatBot, ChatBotError, ChatItem, ChatMemory, ChatMessage, FunctionCall,
};
use wabot_feature_mindset::{Mindset, MindsetOperator, MindsetTool, ToolDefinition, ToolError};
use crate::memory::TestChatMemory;
use crate::mock_adapter::MockChatAdapter;
#[derive(Debug, Clone, Default)]
pub struct ChatTurn {
pub replies: Vec<ChatMessage>,
pub tool_calls: Vec<FunctionCall>,
pub items: Vec<ChatItem>,
}
impl ChatTurn {
pub fn texts(&self) -> Vec<String> {
self.replies.iter().filter_map(|m| m.text.clone()).collect()
}
pub fn text(&self) -> String {
let texts = self.texts();
assert_eq!(texts.len(), 1, "expected exactly one reply, got {texts:?}");
texts.into_iter().next().unwrap()
}
pub fn called(&self, name: &str) -> bool {
self.tool_calls.iter().any(|c| c.name == name)
}
}
pub struct ChatBotHarness {
adapter: Arc<MockChatAdapter>,
memory: Arc<TestChatMemory>,
operator: Arc<MindsetOperator>,
bot: ChatBot,
}
impl ChatBotHarness {
pub fn new(mindset: Arc<dyn Mindset>) -> Self {
Self::builder(mindset).build()
}
pub fn builder(mindset: Arc<dyn Mindset>) -> ChatBotHarnessBuilder {
ChatBotHarnessBuilder {
mindset,
tools: Vec::new(),
container: None,
adapter: None,
}
}
pub fn adapter(&self) -> &Arc<MockChatAdapter> {
&self.adapter
}
pub fn operator(&self) -> &Arc<MindsetOperator> {
&self.operator
}
pub async fn send(&self, message: impl IntoChatMessage) -> Result<ChatTurn, ChatBotError> {
let before = self.memory.len();
let replies: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(Vec::new()));
let sink = replies.clone();
let reply: wabot_feature_chat_bot::BotReplyFn = Arc::new(move |message| {
let sink = sink.clone();
Box::pin(async move {
sink.lock().push(message);
})
});
self.bot
.send_message(message.into_chat_message(), reply)
.await?;
let replies = std::mem::take(&mut *replies.lock());
let items = self.memory.items_from(before);
let tool_calls = items
.iter()
.filter_map(|item| match item {
ChatItem::FunctionCall { function_call } => Some(function_call.clone()),
_ => None,
})
.collect();
Ok(ChatTurn {
replies,
tool_calls,
items,
})
}
pub async fn call_tool(
&self,
name: &str,
arguments: impl crate::mock_adapter::ToArguments,
) -> Result<String, ToolError> {
self.operator
.call_function(name, &arguments.to_arguments())
.await
}
pub async fn system_prompt(&self) -> String {
self.operator.system_prompt().await
}
pub fn tools(&self) -> Result<Vec<MindsetTool>, ToolError> {
self.operator.tools()
}
pub fn history(&self) -> Vec<ChatItem> {
self.memory.all()
}
pub fn memory(&self) -> &Arc<TestChatMemory> {
&self.memory
}
}
pub struct ChatBotHarnessBuilder {
mindset: Arc<dyn Mindset>,
tools: Vec<ToolDefinition>,
container: Option<Container>,
adapter: Option<Arc<MockChatAdapter>>,
}
impl ChatBotHarnessBuilder {
pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools.extend(tools);
self
}
pub fn container(mut self, container: Container) -> Self {
self.container = Some(container);
self
}
pub fn adapter(mut self, adapter: Arc<MockChatAdapter>) -> Self {
self.adapter = Some(adapter);
self
}
pub fn build(self) -> ChatBotHarness {
let container = self.container.unwrap_or_default();
let adapter = self.adapter.unwrap_or_else(MockChatAdapter::arc);
let memory = Arc::new(TestChatMemory::new());
let operator =
Arc::new(MindsetOperator::new(container, self.mindset).with_module_tools(self.tools));
let bot = ChatBot::new(
memory.clone() as Arc<dyn ChatMemory>,
adapter.clone() as Arc<dyn ChatAdapter>,
operator.clone(),
);
ChatBotHarness {
adapter,
memory,
operator,
bot,
}
}
}
pub trait IntoChatMessage {
fn into_chat_message(self) -> ChatMessage;
}
impl IntoChatMessage for ChatMessage {
fn into_chat_message(self) -> ChatMessage {
self
}
}
impl IntoChatMessage for &str {
fn into_chat_message(self) -> ChatMessage {
ChatMessage::text(self)
}
}
impl IntoChatMessage for String {
fn into_chat_message(self) -> ChatMessage {
ChatMessage::text(self)
}
}