use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::ir::cache::CacheControl;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SystemBlock {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
impl SystemBlock {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
cache_control: None,
}
}
#[must_use]
pub fn cached(text: impl Into<String>, cache: CacheControl) -> Self {
Self {
text: text.into(),
cache_control: Some(cache),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SystemPrompt {
blocks: Arc<[SystemBlock]>,
}
impl Default for SystemPrompt {
fn default() -> Self {
Self::empty()
}
}
impl SystemPrompt {
#[must_use]
pub fn empty() -> Self {
Self {
blocks: Arc::from([]),
}
}
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
blocks: Arc::from([SystemBlock::text(text)]),
}
}
#[must_use]
pub fn cached(text: impl Into<String>, cache: CacheControl) -> Self {
Self {
blocks: Arc::from([SystemBlock::cached(text, cache)]),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[must_use]
pub fn blocks(&self) -> &[SystemBlock] {
&self.blocks
}
#[must_use]
pub fn any_cached(&self) -> bool {
self.blocks.iter().any(|b| b.cache_control.is_some())
}
#[must_use]
pub fn map_blocks<F>(&self, mut f: F) -> Self
where
F: FnMut(&mut SystemBlock),
{
let blocks: Vec<SystemBlock> = self
.blocks
.iter()
.map(|b| {
let mut clone = b.clone();
f(&mut clone);
clone
})
.collect();
Self {
blocks: Arc::from(blocks),
}
}
#[must_use]
pub fn concat_text(&self) -> String {
self.blocks
.iter()
.map(|b| b.text.as_str())
.collect::<Vec<_>>()
.join("\n\n")
}
}
impl From<&str> for SystemPrompt {
fn from(s: &str) -> Self {
Self::text(s)
}
}
impl From<String> for SystemPrompt {
fn from(s: String) -> Self {
Self::text(s)
}
}
impl From<SystemBlock> for SystemPrompt {
fn from(block: SystemBlock) -> Self {
Self {
blocks: Arc::from([block]),
}
}
}
impl From<Vec<SystemBlock>> for SystemPrompt {
fn from(blocks: Vec<SystemBlock>) -> Self {
Self {
blocks: Arc::from(blocks),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn empty_prompt_has_no_blocks_and_renders_to_empty_string() {
let p = SystemPrompt::empty();
assert!(p.is_empty());
assert_eq!(p.len(), 0);
assert_eq!(p.concat_text(), "");
assert!(!p.any_cached());
}
#[test]
fn from_str_produces_single_uncached_block() {
let p: SystemPrompt = "Be terse.".into();
assert_eq!(p.len(), 1);
assert_eq!(p.blocks()[0].text, "Be terse.");
assert!(p.blocks()[0].cache_control.is_none());
}
#[test]
fn cached_constructor_attaches_cache_control() {
let p = SystemPrompt::cached("stable instructions", CacheControl::one_hour());
assert!(p.any_cached());
assert_eq!(
p.blocks()[0].cache_control.unwrap().ttl,
crate::ir::cache::CacheTtl::OneHour
);
}
#[test]
fn concat_text_joins_blocks_with_double_newline() {
let p = SystemPrompt::from(vec![
SystemBlock::text("first"),
SystemBlock::text("second"),
]);
assert_eq!(p.concat_text(), "first\n\nsecond");
}
#[test]
fn map_blocks_lets_redactor_rebuild_with_transformed_text() {
let p = SystemPrompt::from(vec![SystemBlock::text("alpha"), SystemBlock::text("beta")]);
let upper = p.map_blocks(|block| {
block.text = block.text.to_uppercase();
});
assert_eq!(upper.concat_text(), "ALPHA\n\nBETA");
assert_eq!(p.concat_text(), "alpha\n\nbeta");
}
#[test]
fn clone_is_atomic_refcount_bump() {
let p = SystemPrompt::cached("long stable preamble".repeat(100), CacheControl::one_hour());
let cloned = p.clone();
assert!(Arc::ptr_eq(&p.blocks, &cloned.blocks));
}
#[test]
fn round_trips_via_serde_when_cached() {
let p = SystemPrompt::cached("x", CacheControl::five_minutes());
let json = serde_json::to_string(&p).unwrap();
let back: SystemPrompt = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
}