1use core::fmt;
2use serde::{
3 de::{self, Visitor},
4 Deserialize, Deserializer, Serialize,
5};
6use std::collections::BTreeMap;
7use std::{fmt::Display, marker::PhantomData};
8
9#[serde_with::skip_serializing_none]
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
11pub struct Author {
12 pub role: Role,
13 pub name: Option<String>,
14}
15
16impl Author {
17 pub fn new(role: Role, name: impl Into<String>) -> Self {
18 Self {
19 role,
20 name: Some(name.into()),
21 }
22 }
23}
24
25impl From<Role> for Author {
26 fn from(role: Role) -> Self {
27 Self { role, name: None }
28 }
29}
30
31#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
32#[serde(rename_all = "snake_case")]
33pub enum Role {
34 User,
35 Assistant,
36 System,
37 Developer,
38 Tool,
39}
40
41impl TryFrom<&str> for Role {
42 type Error = &'static str;
43 fn try_from(value: &str) -> Result<Self, Self::Error> {
44 match value {
45 "user" => Ok(Role::User),
46 "assistant" => Ok(Role::Assistant),
47 "system" => Ok(Role::System),
48 "developer" => Ok(Role::Developer),
49 "tool" => Ok(Role::Tool),
50 _ => Err("Unknown role"),
51 }
52 }
53}
54
55impl Role {
56 pub fn as_str(&self) -> &str {
57 match self {
58 Role::User => "user",
59 Role::Assistant => "assistant",
60 Role::System => "system",
61 Role::Developer => "developer",
62 Role::Tool => "tool",
63 }
64 }
65}
66
67impl Display for Role {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{}", self.as_str())
70 }
71}
72
73#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
74#[serde(rename_all = "snake_case", tag = "type")]
75pub enum Content {
76 Text(TextContent),
77 SystemContent(SystemContent),
79 DeveloperContent(DeveloperContent),
81}
82
83impl<T> From<T> for Content
84where
85 T: Into<String>,
86{
87 fn from(text: T) -> Self {
88 Self::Text(TextContent { text: text.into() })
89 }
90}
91
92impl From<SystemContent> for Content {
93 fn from(sys: SystemContent) -> Self {
94 Self::SystemContent(sys)
95 }
96}
97
98impl From<DeveloperContent> for Content {
99 fn from(dev: DeveloperContent) -> Self {
100 Self::DeveloperContent(dev)
101 }
102}
103
104#[serde_with::skip_serializing_none]
105#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
106pub struct Message {
107 #[serde(flatten)]
110 pub author: Author,
111
112 pub recipient: Option<String>,
117
118 #[serde(
121 deserialize_with = "de_string_or_content_vec",
122 serialize_with = "se_string_or_content_vec"
123 )]
124 pub content: Vec<Content>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
134 pub channel: Option<String>,
135
136 pub content_type: Option<String>,
138}
139
140impl Message {
141 pub fn from_author_and_content<C>(author: Author, content: C) -> Self
142 where
143 C: Into<Content>,
144 {
145 Message {
146 author,
147 content: vec![content.into()],
148 channel: None,
149 recipient: None,
150 content_type: None,
151 }
152 }
153
154 pub fn from_role_and_content<C>(role: Role, content: C) -> Self
155 where
156 C: Into<Content>,
157 {
158 Self::from_author_and_content(Author { role, name: None }, content)
159 }
160
161 pub fn from_role_and_contents<I>(role: Role, content: I) -> Self
162 where
163 I: IntoIterator<Item = Content>,
164 {
165 Message {
166 author: Author { role, name: None },
167 content: content.into_iter().collect(),
168 channel: None,
169 recipient: None,
170 content_type: None,
171 }
172 }
173 pub fn adding_content<C>(mut self, content: C) -> Self
174 where
175 C: Into<Content>,
176 {
177 self.content.push(content.into());
178 self
179 }
180 pub fn with_channel<S>(mut self, channel: S) -> Self
181 where
182 S: Into<String>,
183 {
184 self.channel = Some(channel.into());
185 self
186 }
187 pub fn with_recipient<S>(mut self, recipient: S) -> Self
188 where
189 S: Into<String>,
190 {
191 self.recipient = Some(recipient.into());
192 self
193 }
194 pub fn with_content_type<S>(mut self, content_type: S) -> Self
195 where
196 S: Into<String>,
197 {
198 self.content_type = Some(content_type.into());
199 self
200 }
201}
202
203#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
204pub struct TextContent {
205 pub text: String,
206}
207
208#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
209pub enum ReasoningEffort {
210 Low,
211 Medium,
212 High,
213}
214
215#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
216pub struct ChannelConfig {
217 pub valid_channels: Vec<String>,
221
222 pub channel_required: bool,
224}
225
226impl ChannelConfig {
227 pub fn require_channels<I, T>(channels: I) -> Self
228 where
229 I: IntoIterator<Item = T>,
230 T: Into<String>,
231 {
232 Self {
233 valid_channels: channels.into_iter().map(|c| c.into()).collect(),
234 channel_required: true,
235 }
236 }
237}
238
239#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
240pub struct ToolNamespaceConfig {
241 pub name: String,
242 pub description: Option<String>,
243 pub tools: Vec<ToolDescription>,
244}
245
246impl ToolNamespaceConfig {
247 pub fn new(
248 name: impl Into<String>,
249 description: Option<String>,
250 tools: Vec<ToolDescription>,
251 ) -> Self {
252 Self {
253 name: name.into(),
254 description,
255 tools,
256 }
257 }
258
259 pub fn browser() -> Self {
260 ToolNamespaceConfig::new(
261 "browser",
262 Some("Tool for browsing.\nThe `cursor` appears in brackets before each browsing display: `[{cursor}]`.\nCite information from the tool using the following format:\n`【{cursor}†L{line_start}(-L{line_end})?】`, for example: `【6†L9-L11】` or `【8†L3】`.\nDo not quote more than 10 words directly from the tool output.\nsources=web (default: web)".to_string()),
263 vec![
264 ToolDescription::new(
265 "search",
266 "Searches for information related to `query` and displays `topn` results.",
267 Some(serde_json::json!({
268 "type": "object",
269 "properties": {
270 "query": {"type": "string"},
271 "topn": {"type": "number", "default": 10},
272 "source": {"type": "string"}
273 },
274 "required": ["query"]
275 })),
276 ),
277 ToolDescription::new(
278 "open",
279 "Opens the link `id` from the page indicated by `cursor` starting at line number `loc`, showing `num_lines` lines.\nValid link ids are displayed with the formatting: `【{id}†.*】`.\nIf `cursor` is not provided, the most recent page is implied.\nIf `id` is a string, it is treated as a fully qualified URL associated with `source`.\nIf `loc` is not provided, the viewport will be positioned at the beginning of the document or centered on the most relevant passage, if available.\nUse this function without `id` to scroll to a new location of an opened page.",
280 Some(serde_json::json!({
281 "type": "object",
282 "properties": {
283 "id": {
284 "type": ["number", "string"],
285 "default": -1
286 },
287 "cursor": {"type": "number", "default": -1},
288 "loc": {"type": "number", "default": -1},
289 "num_lines": {"type": "number", "default": -1},
290 "view_source": {"type": "boolean", "default": false},
291 "source": {"type": "string"}
292 }
293 })),
294 ),
295 ToolDescription::new(
296 "find",
297 "Finds exact matches of `pattern` in the current page, or the page given by `cursor`.",
298 Some(serde_json::json!({
299 "type": "object",
300 "properties": {
301 "pattern": {"type": "string"},
302 "cursor": {"type": "number", "default": -1}
303 },
304 "required": ["pattern"]
305 })),
306 ),
307 ],
308 )
309 }
310
311 pub fn python() -> Self {
312 ToolNamespaceConfig::new(
313 "python",
314 Some("Use this tool to execute Python code in your chain of thought. The code will not be shown to the user. This tool should be used for internal reasoning, but not for code that is intended to be visible to the user (e.g. when creating plots, tables, or files).\n\nWhen you send a message containing Python code to python, it will be executed in a stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 120.0 seconds. The drive at '/mnt/data' can be used to save and persist user files. Internet access for this session is UNKNOWN. Depends on the cluster.".to_string()),
315 vec![],
316 )
317 }
318}
319
320#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
322pub struct SystemContent {
323 pub model_identity: Option<String>,
324 pub reasoning_effort: Option<ReasoningEffort>,
325 pub tools: Option<BTreeMap<String, ToolNamespaceConfig>>,
326 pub conversation_start_date: Option<String>,
329
330 pub knowledge_cutoff: Option<String>,
332
333 pub channel_config: Option<ChannelConfig>,
335}
336
337impl Default for SystemContent {
338 fn default() -> Self {
339 Self {
340 model_identity: Some(
341 "You are ChatGPT, a large language model trained by OpenAI.".to_string(),
342 ),
343 reasoning_effort: Some(ReasoningEffort::Medium),
344 tools: None,
345 conversation_start_date: None,
346 knowledge_cutoff: Some("2024-06".to_string()),
347 channel_config: Some(ChannelConfig::require_channels([
348 "analysis",
349 "commentary",
350 "final",
351 ])),
352 }
353 }
354}
355
356impl SystemContent {
357 pub fn new() -> Self {
358 Default::default()
359 }
360 pub fn with_model_identity(mut self, model_identity: impl Into<String>) -> Self {
361 self.model_identity = Some(model_identity.into());
362 self
363 }
364 pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
365 self.reasoning_effort = Some(effort);
366 self
367 }
368 pub fn with_tools(mut self, ns_config: ToolNamespaceConfig) -> Self {
369 let ns = ns_config.name.clone();
370 if let Some(ref mut map) = self.tools {
371 map.insert(ns, ns_config);
372 } else {
373 let mut map = BTreeMap::new();
374 map.insert(ns, ns_config);
375 self.tools = Some(map);
376 }
377 self
378 }
379 pub fn with_conversation_start_date(
380 mut self,
381 conversation_start_date: impl Into<String>,
382 ) -> Self {
383 self.conversation_start_date = Some(conversation_start_date.into());
384 self
385 }
386 pub fn with_knowledge_cutoff(mut self, knowledge_cutoff: impl Into<String>) -> Self {
387 self.knowledge_cutoff = Some(knowledge_cutoff.into());
388 self
389 }
390 pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self {
391 self.channel_config = Some(channel_config);
392 self
393 }
394 pub fn with_required_channels<I, T>(mut self, channels: I) -> Self
395 where
396 I: IntoIterator<Item = T>,
397 T: Into<String>,
398 {
399 self.channel_config = Some(ChannelConfig::require_channels(channels));
400 self
401 }
402
403 pub fn with_browser_tool(mut self) -> Self {
404 self = self.with_tools(ToolNamespaceConfig::browser());
405 self
406 }
407
408 pub fn with_python_tool(mut self) -> Self {
409 self = self.with_tools(ToolNamespaceConfig::python());
410 self
411 }
412}
413
414#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
415pub struct ToolDescription {
416 pub name: String,
417 pub description: String,
418 pub parameters: Option<serde_json::Value>,
419}
420
421impl ToolDescription {
422 pub fn new(
423 name: impl Into<String>,
424 description: impl Into<String>,
425 parameters: Option<serde_json::Value>,
426 ) -> Self {
427 Self {
428 name: name.into(),
429 description: description.into(),
430 parameters,
431 }
432 }
433}
434
435#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
436pub struct Conversation {
437 pub messages: Vec<Message>,
438}
439
440impl Conversation {
441 pub fn from_messages<I>(messages: I) -> Self
442 where
443 I: IntoIterator<Item = Message>,
444 {
445 Self {
446 messages: messages.into_iter().collect(),
447 }
448 }
449}
450
451impl<'a> IntoIterator for &'a Conversation {
452 type Item = &'a Message;
453 type IntoIter = std::slice::Iter<'a, Message>;
454
455 fn into_iter(self) -> Self::IntoIter {
456 self.messages.iter()
457 }
458}
459
460fn de_string_or_content_vec<'de, D>(deserializer: D) -> Result<Vec<Content>, D::Error>
461where
462 D: Deserializer<'de>,
463{
464 struct StringOrContentVec(PhantomData<fn() -> Vec<Content>>);
465
466 impl<'de> Visitor<'de> for StringOrContentVec {
467 type Value = Vec<Content>;
468
469 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
470 formatter.write_str("string or list of content")
471 }
472
473 fn visit_str<E>(self, value: &str) -> Result<Vec<Content>, E>
474 where
475 E: de::Error,
476 {
477 Ok(vec![Content::Text(TextContent {
478 text: value.to_owned(),
479 })])
480 }
481
482 fn visit_seq<A>(self, seq: A) -> std::result::Result<Self::Value, A::Error>
483 where
484 A: de::SeqAccess<'de>,
485 {
486 Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
487 }
488 }
489
490 deserializer.deserialize_any(StringOrContentVec(PhantomData))
491}
492
493fn se_string_or_content_vec<S>(value: &Vec<Content>, serializer: S) -> Result<S::Ok, S::Error>
494where
495 S: serde::Serializer,
496{
497 if value.len() == 1 {
498 if let Content::Text(TextContent { text }) = &value[0] {
499 return serializer.serialize_str(text);
500 }
501 }
502 value.serialize(serializer)
503}
504
505#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
507pub struct DeveloperContent {
508 pub instructions: Option<String>,
509 pub tools: Option<BTreeMap<String, ToolNamespaceConfig>>,
510}
511
512impl DeveloperContent {
513 pub fn new() -> Self {
514 Self::default()
515 }
516 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
517 self.instructions = Some(instructions.into());
518 self
519 }
520 pub fn with_tools(mut self, ns_config: ToolNamespaceConfig) -> Self {
521 let ns = ns_config.name.clone();
522 if let Some(ref mut map) = self.tools {
523 map.insert(ns, ns_config);
524 } else {
525 let mut map = BTreeMap::new();
526 map.insert(ns, ns_config);
527 self.tools = Some(map);
528 }
529 self
530 }
531 pub fn with_function_tools(mut self, tools: Vec<ToolDescription>) -> Self {
532 self = self.with_tools(ToolNamespaceConfig::new("functions", None, tools));
533 self
534 }
535}