wabot_testing/
chat_bot.rs1use std::sync::Arc;
5
6use parking_lot::Mutex;
7use wabot_core::injection::Container;
8use wabot_feature_chat_bot::{
9 ChatAdapter, ChatBot, ChatBotError, ChatItem, ChatMemory, ChatMessage, FunctionCall,
10};
11use wabot_feature_mindset::{Mindset, MindsetOperator, MindsetTool, ToolDefinition, ToolError};
12
13use crate::memory::TestChatMemory;
14use crate::mock_adapter::MockChatAdapter;
15
16#[derive(Debug, Clone, Default)]
18pub struct ChatTurn {
19 pub replies: Vec<ChatMessage>,
22 pub tool_calls: Vec<FunctionCall>,
24 pub items: Vec<ChatItem>,
26}
27
28impl ChatTurn {
29 pub fn texts(&self) -> Vec<String> {
31 self.replies.iter().filter_map(|m| m.text.clone()).collect()
32 }
33
34 pub fn text(&self) -> String {
42 let texts = self.texts();
43 assert_eq!(texts.len(), 1, "expected exactly one reply, got {texts:?}");
44 texts.into_iter().next().unwrap()
45 }
46
47 pub fn called(&self, name: &str) -> bool {
48 self.tool_calls.iter().any(|c| c.name == name)
49 }
50}
51
52pub struct ChatBotHarness {
74 adapter: Arc<MockChatAdapter>,
75 memory: Arc<TestChatMemory>,
76 operator: Arc<MindsetOperator>,
77 bot: ChatBot,
78}
79
80impl ChatBotHarness {
81 pub fn new(mindset: Arc<dyn Mindset>) -> Self {
83 Self::builder(mindset).build()
84 }
85
86 pub fn builder(mindset: Arc<dyn Mindset>) -> ChatBotHarnessBuilder {
87 ChatBotHarnessBuilder {
88 mindset,
89 tools: Vec::new(),
90 container: None,
91 adapter: None,
92 }
93 }
94
95 pub fn adapter(&self) -> &Arc<MockChatAdapter> {
97 &self.adapter
98 }
99
100 pub fn operator(&self) -> &Arc<MindsetOperator> {
103 &self.operator
104 }
105
106 pub async fn send(&self, message: impl IntoChatMessage) -> Result<ChatTurn, ChatBotError> {
108 let before = self.memory.len();
109 let replies: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(Vec::new()));
110
111 let sink = replies.clone();
112 let reply: wabot_feature_chat_bot::BotReplyFn = Arc::new(move |message| {
113 let sink = sink.clone();
114 Box::pin(async move {
115 sink.lock().push(message);
116 })
117 });
118
119 self.bot
120 .send_message(message.into_chat_message(), reply)
121 .await?;
122
123 let replies = std::mem::take(&mut *replies.lock());
124 let items = self.memory.items_from(before);
125 let tool_calls = items
126 .iter()
127 .filter_map(|item| match item {
128 ChatItem::FunctionCall { function_call } => Some(function_call.clone()),
129 _ => None,
130 })
131 .collect();
132
133 Ok(ChatTurn {
134 replies,
135 tool_calls,
136 items,
137 })
138 }
139
140 pub async fn call_tool(
144 &self,
145 name: &str,
146 arguments: impl crate::mock_adapter::ToArguments,
147 ) -> Result<String, ToolError> {
148 self.operator
149 .call_function(name, &arguments.to_arguments())
150 .await
151 }
152
153 pub async fn system_prompt(&self) -> String {
155 self.operator.system_prompt().await
156 }
157
158 pub fn tools(&self) -> Result<Vec<MindsetTool>, ToolError> {
160 self.operator.tools()
161 }
162
163 pub fn history(&self) -> Vec<ChatItem> {
165 self.memory.all()
166 }
167
168 pub fn memory(&self) -> &Arc<TestChatMemory> {
169 &self.memory
170 }
171}
172
173pub struct ChatBotHarnessBuilder {
174 mindset: Arc<dyn Mindset>,
175 tools: Vec<ToolDefinition>,
176 container: Option<Container>,
177 adapter: Option<Arc<MockChatAdapter>>,
178}
179
180impl ChatBotHarnessBuilder {
181 pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
184 self.tools.extend(tools);
185 self
186 }
187
188 pub fn container(mut self, container: Container) -> Self {
191 self.container = Some(container);
192 self
193 }
194
195 pub fn adapter(mut self, adapter: Arc<MockChatAdapter>) -> Self {
198 self.adapter = Some(adapter);
199 self
200 }
201
202 pub fn build(self) -> ChatBotHarness {
203 let container = self.container.unwrap_or_default();
204 let adapter = self.adapter.unwrap_or_else(MockChatAdapter::arc);
205 let memory = Arc::new(TestChatMemory::new());
206
207 let operator =
208 Arc::new(MindsetOperator::new(container, self.mindset).with_module_tools(self.tools));
209 let bot = ChatBot::new(
210 memory.clone() as Arc<dyn ChatMemory>,
211 adapter.clone() as Arc<dyn ChatAdapter>,
212 operator.clone(),
213 );
214
215 ChatBotHarness {
216 adapter,
217 memory,
218 operator,
219 bot,
220 }
221 }
222}
223
224pub trait IntoChatMessage {
226 fn into_chat_message(self) -> ChatMessage;
227}
228
229impl IntoChatMessage for ChatMessage {
230 fn into_chat_message(self) -> ChatMessage {
231 self
232 }
233}
234
235impl IntoChatMessage for &str {
236 fn into_chat_message(self) -> ChatMessage {
237 ChatMessage::text(self)
238 }
239}
240
241impl IntoChatMessage for String {
242 fn into_chat_message(self) -> ChatMessage {
243 ChatMessage::text(self)
244 }
245}