Skip to main content

rig_agent/integrations/
cli_chatbot.rs

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