Skip to main content

rig_agent/integrations/
cli_chatbot.rs

1use rig_core::{
2    markers::{Missing, Provided},
3    message::Message,
4    wasm_compat::WasmCompatSend,
5};
6
7use crate::{
8    agent::{Agent, MultiTurnStreamItem, Text},
9    completion::{Chat, CompletionError, CompletionModel, PromptError, Usage},
10    streaming::{StreamedAssistantContent, StreamingPrompt},
11};
12use futures::StreamExt;
13use std::io::{self, Write};
14
15pub struct ChatImpl<T>(T)
16where
17    T: Chat;
18
19pub struct AgentImpl<M>
20where
21    M: CompletionModel + 'static,
22{
23    agent: Agent<M>,
24    max_turns: usize,
25    show_usage: bool,
26    usage: Usage,
27}
28
29pub struct ChatBotBuilder<T = Missing>(T);
30
31pub struct ChatBot<T>(T);
32
33/// Trait to abstract message behavior away from cli_chat/`run` loop
34#[allow(private_interfaces)]
35trait CliChat {
36    async fn request(
37        &mut self,
38        prompt: &str,
39        history: &mut Vec<Message>,
40    ) -> Result<String, PromptError>;
41
42    fn show_usage(&self) -> bool {
43        false
44    }
45
46    fn usage(&self) -> Option<Usage> {
47        None
48    }
49}
50
51impl<T> CliChat for ChatImpl<T>
52where
53    T: Chat,
54{
55    async fn request(
56        &mut self,
57        prompt: &str,
58        history: &mut Vec<Message>,
59    ) -> Result<String, PromptError> {
60        let res = self.0.chat(prompt, history).await?;
61        println!("{res}");
62
63        Ok(res)
64    }
65}
66
67impl<M> CliChat for AgentImpl<M>
68where
69    M: CompletionModel + WasmCompatSend + 'static,
70{
71    async fn request(
72        &mut self,
73        prompt: &str,
74        history: &mut Vec<Message>,
75    ) -> Result<String, PromptError> {
76        let mut response_stream = self
77            .agent
78            .stream_prompt(prompt)
79            .history(history.clone())
80            .max_turns(self.max_turns)
81            .await;
82
83        let mut acc = String::new();
84        let mut messages = None;
85
86        let result = loop {
87            let Some(chunk) = response_stream.next().await else {
88                println!();
89                break Ok(acc);
90            };
91
92            match chunk {
93                Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Text(
94                    Text { text, .. },
95                ))) => {
96                    print!("{}", text);
97                    acc.push_str(&text);
98                }
99                Ok(MultiTurnStreamItem::FinalResponse(final_response)) => {
100                    self.usage = final_response.usage();
101                    messages = final_response.messages().map(|history| history.to_vec());
102                }
103                Err(e) => {
104                    break Err(PromptError::CompletionError(
105                        CompletionError::ResponseError(e.to_string()),
106                    ));
107                }
108                _ => continue,
109            }
110        };
111
112        if let Ok(response) = &result {
113            if let Some(messages) = messages {
114                history.extend(messages);
115            } else {
116                history.push(Message::user(prompt));
117                history.push(Message::assistant(response.as_str()));
118            }
119        }
120
121        result
122    }
123
124    fn show_usage(&self) -> bool {
125        self.show_usage
126    }
127
128    fn usage(&self) -> Option<Usage> {
129        Some(self.usage)
130    }
131}
132
133impl Default for ChatBotBuilder<Missing> {
134    fn default() -> Self {
135        Self(Missing)
136    }
137}
138
139impl ChatBotBuilder<Missing> {
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    pub fn agent<M: CompletionModel + 'static>(
145        self,
146        agent: Agent<M>,
147    ) -> ChatBotBuilder<Provided<AgentImpl<M>>> {
148        ChatBotBuilder(Provided(AgentImpl {
149            agent,
150            max_turns: 1,
151            show_usage: false,
152            usage: Usage::default(),
153        }))
154    }
155
156    pub fn chat<T: Chat>(self, chatbot: T) -> ChatBotBuilder<Provided<ChatImpl<T>>> {
157        ChatBotBuilder(Provided(ChatImpl(chatbot)))
158    }
159}
160
161impl<T> ChatBotBuilder<Provided<ChatImpl<T>>>
162where
163    T: Chat,
164{
165    pub fn build(self) -> ChatBot<ChatImpl<T>> {
166        ChatBot(self.0.0)
167    }
168}
169
170impl<M> ChatBotBuilder<Provided<AgentImpl<M>>>
171where
172    M: CompletionModel + 'static,
173{
174    /// Set the total model-call budget for each prompt, including the initial
175    /// call and every retry or continuation. Zero emits no model calls.
176    pub fn max_turns(self, max_turns: usize) -> Self {
177        ChatBotBuilder(Provided(AgentImpl {
178            max_turns,
179            ..self.0.0
180        }))
181    }
182
183    pub fn show_usage(self) -> Self {
184        ChatBotBuilder(Provided(AgentImpl {
185            show_usage: true,
186            ..self.0.0
187        }))
188    }
189
190    pub fn build(self) -> ChatBot<AgentImpl<M>> {
191        ChatBot(self.0.0)
192    }
193}
194
195#[allow(private_bounds)]
196impl<T> ChatBot<T>
197where
198    T: CliChat,
199{
200    pub async fn run(mut self) -> Result<(), PromptError> {
201        let stdin = io::stdin();
202        let mut stdout = io::stdout();
203        let mut history = vec![];
204
205        loop {
206            print!("> ");
207            stdout.flush().map_err(|e| {
208                PromptError::CompletionError(CompletionError::ResponseError(format!(
209                    "failed to flush stdout: {e}"
210                )))
211            })?;
212
213            let mut input = String::new();
214            match stdin.read_line(&mut input) {
215                Ok(_) => {
216                    let input = input.trim();
217                    if input == "exit" {
218                        break;
219                    }
220
221                    tracing::info!("Prompt:\n{input}\n");
222
223                    println!();
224                    println!("========================== Response ============================");
225
226                    self.0.request(input, &mut history).await?;
227
228                    println!("================================================================");
229                    println!();
230
231                    if self.0.show_usage()
232                        && let Some(Usage {
233                            input_tokens,
234                            output_tokens,
235                            ..
236                        }) = self.0.usage()
237                    {
238                        println!("Input {input_tokens} tokens\nOutput {output_tokens} tokens");
239                    }
240                }
241                Err(e) => println!("Error reading request: {e}"),
242            }
243        }
244
245        Ok(())
246    }
247}