Skip to main content

skilltest_core/
conversation.rs

1//! The conversation model: the transcript that flows between the runner and the
2//! provider, and is ultimately handed to evals.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// Who produced a message.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum Role {
11    /// The (real or simulated) user driving the skill.
12    User,
13    /// The skill / assistant under test.
14    Assistant,
15    /// System-level framing, if a provider chooses to surface it.
16    System,
17}
18
19/// A single turn in the conversation.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
21pub struct Message {
22    pub role: Role,
23    pub content: String,
24}
25
26impl Message {
27    /// Build a user message.
28    pub fn user(content: impl Into<String>) -> Self {
29        Self {
30            role: Role::User,
31            content: content.into(),
32        }
33    }
34
35    /// Build an assistant message.
36    pub fn assistant(content: impl Into<String>) -> Self {
37        Self {
38            role: Role::Assistant,
39            content: content.into(),
40        }
41    }
42}
43
44/// An ordered list of messages. Thin wrapper so the type reads clearly at call
45/// sites and so we can grow conversation-level helpers without churn.
46#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47pub struct Transcript {
48    pub messages: Vec<Message>,
49}
50
51impl Transcript {
52    /// Start a transcript from the initial user input given to the skill.
53    pub fn from_input(input: impl Into<String>) -> Self {
54        Self {
55            messages: vec![Message::user(input)],
56        }
57    }
58
59    /// Append a message.
60    pub fn push(&mut self, message: Message) {
61        self.messages.push(message);
62    }
63
64    /// Number of assistant turns produced so far.
65    #[must_use]
66    pub fn assistant_turns(&self) -> usize {
67        self.messages
68            .iter()
69            .filter(|m| m.role == Role::Assistant)
70            .count()
71    }
72}