1use crate::tool::Tool;
2use candid::{CandidType, Principal};
3use serde::{Deserialize, Serialize};
4
5#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq)]
7pub enum ChatMessage {
8 #[serde(rename = "user")]
9 User { content: String },
10 #[serde(rename = "system")]
11 System { content: String },
12 #[serde(rename = "assistant")]
13 Assistant(AssistantMessage),
14 #[serde(rename = "tool")]
15 Tool {
16 content: String,
17 tool_call_id: String,
18 },
19}
20
21#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
22pub struct Response {
23 pub message: AssistantMessage,
24}
25
26#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)]
27pub struct AssistantMessage {
28 pub content: Option<String>,
29 pub tool_calls: Vec<ToolCall>,
30}
31
32#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)]
33pub struct ToolCall {
34 pub id: String,
35 pub function: FunctionCall,
36}
37
38#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)]
39pub struct FunctionCall {
40 pub name: String,
41 pub arguments: Vec<ToolCallArgument>,
42}
43
44impl FunctionCall {
45 pub fn get(&self, argument: &str) -> Option<String> {
46 self.arguments
47 .iter()
48 .find(|arg| arg.name == argument)
49 .map(|arg| arg.value.clone())
50 }
51}
52
53#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)]
55pub struct ToolCallArgument {
56 pub name: String,
57 pub value: String,
58}
59
60#[derive(CandidType, Serialize, Deserialize, Debug)]
62struct Request {
63 model: String,
64 messages: Vec<ChatMessage>,
65 tools: Option<Vec<Tool>>,
66}
67
68const CYCLES_PER_CHAT: u128 = 100_000_000_000;
78
79#[derive(Debug)]
81pub struct ChatBuilder {
82 model: String,
83 messages: Vec<ChatMessage>,
84 tools: Vec<Tool>,
85 canister: Principal,
86}
87
88impl ChatBuilder {
89 pub fn new(model: impl Into<String>) -> Self {
94 Self {
95 model: model.into(),
96 messages: Vec::new(),
97 tools: Vec::new(),
98 canister: crate::default_llm_canister(),
99 }
100 }
101
102 pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
104 self.messages = messages;
105 self
106 }
107
108 pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
110 self.tools = tools;
111 self
112 }
113
114 pub fn with_canister(mut self, canister: Principal) -> Self {
123 self.canister = canister;
124 self
125 }
126
127 pub async fn send(self) -> Response {
133 let tools_option = if self.tools.is_empty() {
134 None
135 } else {
136 Some(self.tools)
137 };
138
139 ic_cdk::call::Call::bounded_wait(self.canister, "v1_chat")
140 .change_timeout(300)
141 .with_cycles(CYCLES_PER_CHAT)
142 .with_arg(Request {
143 model: self.model,
144 messages: self.messages,
145 tools: tools_option,
146 })
147 .await
148 .unwrap_or_else(|e| ic_cdk::trap(format!("LLM call failed: {e:?}")))
149 .candid()
150 .unwrap_or_else(|e| ic_cdk::trap(format!("failed to decode LLM response: {e:?}")))
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::tool::ToolBuilder;
158
159 #[test]
160 fn create_chat_builder() {
161 let builder = ChatBuilder::new("llama3.1:8b");
162 assert!(builder.messages.is_empty());
163 assert!(builder.tools.is_empty());
164 }
165
166 #[test]
167 fn chat_builder_with_messages() {
168 let messages = vec![
169 ChatMessage::System {
170 content: "You are a helpful assistant".to_string(),
171 },
172 ChatMessage::User {
173 content: "Hello".to_string(),
174 },
175 ];
176
177 let builder = ChatBuilder::new("llama3.1:8b").with_messages(messages.clone());
178
179 assert_eq!(builder.messages, messages);
180 assert!(builder.tools.is_empty());
181 }
182
183 #[test]
184 fn chat_builder_with_tools() {
185 let tool = ToolBuilder::new("test_tool")
186 .with_description("A test tool")
187 .build();
188
189 let builder = ChatBuilder::new("llama3.1:8b").with_tools(vec![tool.clone()]);
190
191 assert!(builder.messages.is_empty());
192 assert_eq!(builder.tools.len(), 1);
193 assert_eq!(builder.tools[0], tool);
194 }
195
196 #[test]
197 fn chat_builder_defaults_to_mainnet_llm_canister() {
198 let builder = ChatBuilder::new("llama3.1:8b");
199 assert_eq!(
200 builder.canister,
201 Principal::from_text(crate::MAINNET_LLM_CANISTER).unwrap(),
202 );
203 }
204
205 #[test]
206 fn chat_builder_with_canister() {
207 let canister = Principal::from_slice(&[1, 2, 3, 4]);
208 let builder = ChatBuilder::new("llama3.1:8b").with_canister(canister);
209 assert_eq!(builder.canister, canister);
210 }
211
212 #[test]
213 fn chat_builder_with_messages_and_tools() {
214 let messages = vec![ChatMessage::User {
215 content: "Hello".to_string(),
216 }];
217
218 let tool = ToolBuilder::new("test_tool").build();
219
220 let builder = ChatBuilder::new("llama3.1:8b")
221 .with_messages(messages.clone())
222 .with_tools(vec![tool.clone()]);
223
224 assert_eq!(builder.messages, messages);
225 assert_eq!(builder.tools.len(), 1);
226 assert_eq!(builder.tools[0], tool);
227 }
228
229 #[test]
230 fn function_call_get() {
231 let function_call = FunctionCall {
232 name: "test_function".to_string(),
233 arguments: vec![
234 ToolCallArgument {
235 name: "arg1".to_string(),
236 value: "value1".to_string(),
237 },
238 ToolCallArgument {
239 name: "arg2".to_string(),
240 value: "value2".to_string(),
241 },
242 ],
243 };
244
245 assert_eq!(function_call.get("arg1"), Some("value1".to_string()));
246 assert_eq!(function_call.get("arg2"), Some("value2".to_string()));
247 assert_eq!(function_call.get("arg3"), None);
248 }
249}