|
5 | 5 | //! 2. The `AgentLoop` correctly routes natural language to the LLM. |
6 | 6 | //! 3. Memory recall is injected and persisted across turns. |
7 | 7 | //! 4. Skill composer integration works end-to-end. |
8 | | -//! 5. Channel runner processes mock messages correctly. |
| 8 | +//! 5. ChatBot runner processes mock adapters correctly. |
9 | 9 | //! 6. Full context with all components is accessible. |
10 | 10 |
|
11 | 11 | use std::{collections::VecDeque, path::PathBuf, sync::Arc}; |
12 | 12 |
|
13 | 13 | use alice_adapters::memory::sqlite_store::SqliteMemoryStore; |
14 | 14 | use alice_core::memory::{domain::HybridWeights, service::MemoryService}; |
15 | 15 | use alice_runtime::{ |
16 | | - channel_runner::run_channels, |
| 16 | + chatbot_runner::run_chatbot, |
17 | 17 | config::{SkillSourceEntry, SkillsConfig}, |
18 | 18 | context::AliceRuntimeContext, |
19 | 19 | handle_input::handle_input_with_skills, |
20 | 20 | skill_wiring::build_skill_composer, |
21 | 21 | }; |
22 | 22 | use async_trait::async_trait; |
23 | 23 | use bob_adapters::tape_memory::InMemoryTapeStore; |
24 | | -use bob_core::{ |
25 | | - channel::{Channel, ChannelError, ChannelMessage, ChannelOutput}, |
26 | | - error::AgentError, |
27 | | - ports::TapeStorePort, |
28 | | - types::*, |
| 24 | +use bob_chat::{ |
| 25 | + adapter::ChatAdapter, |
| 26 | + card::CardElement, |
| 27 | + error::ChatError, |
| 28 | + event::ChatEvent, |
| 29 | + message::{AdapterPostableMessage, Author, IncomingMessage, SentMessage}, |
29 | 30 | }; |
| 31 | +use bob_core::{error::AgentError, ports::TapeStorePort, types::*}; |
30 | 32 | use bob_runtime::{ |
31 | 33 | AgentRuntime, NoOpToolPort, |
32 | 34 | agent_loop::{AgentLoop, AgentLoopOutput}, |
@@ -64,31 +66,90 @@ impl AgentRuntime for StubRuntime { |
64 | 66 | } |
65 | 67 |
|
66 | 68 | // --------------------------------------------------------------------------- |
67 | | -// Mock channel |
| 69 | +// Mock chat adapter |
68 | 70 | // --------------------------------------------------------------------------- |
69 | 71 |
|
70 | | -/// In-memory channel that feeds predetermined messages and collects responses. |
71 | | -#[derive(Debug)] |
72 | | -struct MockChannel { |
73 | | - messages: VecDeque<ChannelMessage>, |
74 | | - outputs: Arc<Mutex<Vec<ChannelOutput>>>, |
| 72 | +/// In-memory chat adapter that feeds predetermined events and collects posted messages. |
| 73 | +struct MockChatAdapter { |
| 74 | + events: Mutex<VecDeque<ChatEvent>>, |
| 75 | + posted: Arc<Mutex<Vec<String>>>, |
| 76 | +} |
| 77 | + |
| 78 | +impl std::fmt::Debug for MockChatAdapter { |
| 79 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 80 | + f.debug_struct("MockChatAdapter").finish_non_exhaustive() |
| 81 | + } |
75 | 82 | } |
76 | 83 |
|
77 | 84 | #[async_trait] |
78 | | -impl Channel for MockChannel { |
79 | | - async fn recv(&mut self) -> Option<ChannelMessage> { |
80 | | - self.messages.pop_front() |
| 85 | +impl ChatAdapter for MockChatAdapter { |
| 86 | + #[expect(clippy::unnecessary_literal_bound)] |
| 87 | + fn name(&self) -> &str { |
| 88 | + "mock" |
| 89 | + } |
| 90 | + |
| 91 | + async fn recv_event(&mut self) -> Option<ChatEvent> { |
| 92 | + self.events.lock().pop_front() |
| 93 | + } |
| 94 | + |
| 95 | + async fn post_message( |
| 96 | + &self, |
| 97 | + _thread_id: &str, |
| 98 | + message: &AdapterPostableMessage, |
| 99 | + ) -> Result<SentMessage, ChatError> { |
| 100 | + let text = self.render_message(message); |
| 101 | + self.posted.lock().push(text); |
| 102 | + Ok(SentMessage { |
| 103 | + id: "mock-sent".into(), |
| 104 | + thread_id: "mock-thread".into(), |
| 105 | + adapter_name: "mock".into(), |
| 106 | + raw: None, |
| 107 | + }) |
81 | 108 | } |
82 | 109 |
|
83 | | - async fn send(&self, output: ChannelOutput) -> Result<(), ChannelError> { |
84 | | - self.outputs.lock().push(output); |
85 | | - Ok(()) |
| 110 | + async fn edit_message( |
| 111 | + &self, |
| 112 | + _thread_id: &str, |
| 113 | + _message_id: &str, |
| 114 | + _message: &AdapterPostableMessage, |
| 115 | + ) -> Result<SentMessage, ChatError> { |
| 116 | + Err(ChatError::NotSupported("edit".into())) |
| 117 | + } |
| 118 | + |
| 119 | + async fn delete_message(&self, _thread_id: &str, _message_id: &str) -> Result<(), ChatError> { |
| 120 | + Err(ChatError::NotSupported("delete".into())) |
| 121 | + } |
| 122 | + |
| 123 | + fn render_card(&self, _card: &CardElement) -> String { |
| 124 | + String::new() |
| 125 | + } |
| 126 | + |
| 127 | + fn render_message(&self, message: &AdapterPostableMessage) -> String { |
| 128 | + match message { |
| 129 | + AdapterPostableMessage::Text(t) | AdapterPostableMessage::Markdown(t) => t.clone(), |
| 130 | + } |
86 | 131 | } |
87 | 132 | } |
88 | 133 |
|
89 | | -/// Create a `ChannelMessage` with default session and no sender. |
90 | | -fn make_message(text: &str) -> ChannelMessage { |
91 | | - ChannelMessage { text: text.to_string(), session_id: "test-session".to_string(), sender: None } |
| 134 | +/// Create a `ChatEvent::Message` with default session and no sender. |
| 135 | +fn make_event(text: &str) -> ChatEvent { |
| 136 | + ChatEvent::Message { |
| 137 | + thread_id: "test-session".into(), |
| 138 | + message: IncomingMessage { |
| 139 | + id: "m1".into(), |
| 140 | + text: text.to_string(), |
| 141 | + author: Author { |
| 142 | + user_id: "test-user".into(), |
| 143 | + user_name: "tester".into(), |
| 144 | + full_name: "Test User".into(), |
| 145 | + is_bot: false, |
| 146 | + }, |
| 147 | + attachments: vec![], |
| 148 | + is_mention: false, |
| 149 | + thread_id: "test-session".into(), |
| 150 | + timestamp: None, |
| 151 | + }, |
| 152 | + } |
92 | 153 | } |
93 | 154 |
|
94 | 155 | // --------------------------------------------------------------------------- |
@@ -226,27 +287,30 @@ async fn cmd_run_with_skill_composer() { |
226 | 287 | assert!(result.is_ok(), "cmd_run with skill composer should succeed"); |
227 | 288 | } |
228 | 289 |
|
229 | | -/// Exercise `run_channels` with a `MockChannel` that provides two messages |
| 290 | +/// Exercise `run_chatbot` with a `MockChatAdapter` that provides two messages |
230 | 291 | /// then returns `None` (EOF). Both messages should be processed and |
231 | 292 | /// responses collected. |
232 | 293 | #[tokio::test] |
233 | | -async fn channel_runner_with_mock_channel() { |
| 294 | +async fn chatbot_runner_with_mock_adapter() { |
234 | 295 | let Some(memory_service) = make_memory_service() else { return }; |
235 | 296 | let context = Arc::new(build_test_context(memory_service)); |
236 | 297 |
|
237 | | - let outputs: Arc<Mutex<Vec<ChannelOutput>>> = Arc::new(Mutex::new(Vec::new())); |
238 | | - let channel = MockChannel { |
239 | | - messages: VecDeque::from(vec![make_message("hello agent"), make_message("second message")]), |
240 | | - outputs: Arc::clone(&outputs), |
| 298 | + let posted: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new())); |
| 299 | + let adapter = MockChatAdapter { |
| 300 | + events: Mutex::new(VecDeque::from(vec![ |
| 301 | + make_event("hello agent"), |
| 302 | + make_event("second message"), |
| 303 | + ])), |
| 304 | + posted: Arc::clone(&posted), |
241 | 305 | }; |
242 | 306 |
|
243 | | - let channels: Vec<Box<dyn Channel>> = vec![Box::new(channel)]; |
244 | | - let result = run_channels(context, channels).await; |
245 | | - assert!(result.is_ok(), "channel runner should complete without error"); |
| 307 | + let adapters: Vec<Box<dyn ChatAdapter>> = vec![Box::new(adapter)]; |
| 308 | + let result = run_chatbot(context, adapters).await; |
| 309 | + assert!(result.is_ok(), "chatbot runner should complete without error"); |
246 | 310 |
|
247 | | - let collected = outputs.lock(); |
| 311 | + let collected = posted.lock(); |
248 | 312 | assert_eq!(collected.len(), 2, "both messages should produce a response"); |
249 | | - assert!(collected.iter().all(|o| !o.is_error), "no response should be an error"); |
| 313 | + assert!(collected.iter().all(|o| !o.is_empty()), "no response should be empty"); |
250 | 314 | } |
251 | 315 |
|
252 | 316 | /// Build a context with all optional components populated and verify every |
|
0 commit comments