rig_agent/integrations/
cli_chatbot.rs1use 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#[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 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}