use serde_json::{Value, json};
use crate::core::Tainted;
use crate::model::CacheTtl;
#[derive(Debug, Clone, Default)]
pub struct Prompt {
system: Vec<Tainted<Value>>,
input: Vec<Tainted<Value>>,
}
impl Prompt {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn system(mut self, text: Tainted<String>) -> Self {
self.system.push(part(text, None));
self
}
#[must_use]
pub fn system_cached(mut self, text: Tainted<String>, ttl: CacheTtl) -> Self {
self.system.push(part(text, Some(ttl)));
self
}
#[must_use]
pub fn input(mut self, text: Tainted<String>) -> Self {
self.input.push(part(text, None));
self
}
#[must_use]
pub fn input_cached(mut self, text: Tainted<String>, ttl: CacheTtl) -> Self {
self.input.push(part(text, Some(ttl)));
self
}
#[must_use]
pub fn build(self) -> Tainted<Value> {
let mut fields = Vec::with_capacity(2);
if !self.system.is_empty() {
fields.push(("system", Tainted::array(self.system)));
}
fields.push(("input", Tainted::array(self.input)));
Tainted::object(fields)
}
}
fn part(text: Tainted<String>, cache: Option<CacheTtl>) -> Tainted<Value> {
text.map(|text| match cache {
Some(ttl) => json!({ "type": "text", "text": text, "cache": ttl.as_str() }),
None => json!({ "type": "text", "text": text }),
})
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Part<'a> {
pub(crate) text: std::borrow::Cow<'a, str>,
pub(crate) cache: Option<CacheTtl>,
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
impl<'a> Part<'a> {
fn plain(text: impl Into<std::borrow::Cow<'a, str>>) -> Self {
Self {
text: text.into(),
cache: None,
}
}
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
#[derive(Debug)]
pub(crate) enum Turns<'a> {
Native(&'a [Value]),
Parts(Vec<Part<'a>>),
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
#[derive(Debug)]
pub(crate) enum System<'a> {
Parts(Vec<Part<'a>>),
#[cfg_attr(
not(feature = "providers"),
expect(
dead_code,
reason = "Converse refuses a structured instruction outright"
)
)]
Native(&'a Value),
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
pub(crate) fn parts(value: &Value) -> Option<Vec<Part<'_>>> {
let items = value.as_array().filter(|items| !items.is_empty())?;
items
.iter()
.map(|item| {
let map = item.as_object()?;
if map.get("type").and_then(Value::as_str) != Some("text")
|| !map
.keys()
.all(|k| matches!(k.as_str(), "type" | "text" | "cache"))
{
return None;
}
let cache = match map.get("cache") {
None => None,
Some(ttl) => Some(serde_json::from_value::<CacheTtl>(ttl.clone()).ok()?),
};
Some(Part {
text: map.get("text")?.as_str()?.into(),
cache,
})
})
.collect()
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
pub(crate) fn turns<'a>(prompt: &'a Value, natives: &[&str]) -> Turns<'a> {
match prompt {
Value::String(text) => return Turns::Parts(vec![Part::plain(text.as_str())]),
Value::Array(items) => {
return parts(prompt).map_or(Turns::Native(items), Turns::Parts);
}
_ => {}
}
for key in natives {
if let Some(list @ Value::Array(items)) = super::prompt_envelope(prompt, key) {
return parts(list).map_or(Turns::Native(items), Turns::Parts);
}
}
if let Some(input) = super::prompt_envelope(prompt, "input") {
return match input {
Value::String(text) => Turns::Parts(vec![Part::plain(text.as_str())]),
other => parts(other).map_or_else(
|| Turns::Parts(vec![Part::plain(other.to_string())]),
Turns::Parts,
),
};
}
let mut rest = prompt.clone();
if let Some(map) = rest.as_object_mut() {
map.remove("system");
}
Turns::Parts(vec![Part::plain(rest.to_string())])
}
#[cfg(any(feature = "providers", feature = "bedrock"))]
pub(crate) fn system(prompt: &Value) -> Option<System<'_>> {
let system = prompt.get("system").filter(|s| !s.is_null())?;
Some(match system {
Value::String(text) => System::Parts(vec![Part::plain(text.as_str())]),
other => parts(other).map_or(System::Native(other), System::Parts),
})
}
#[cfg(feature = "providers")]
pub(crate) fn joined(parts: &[Part<'_>]) -> String {
parts
.iter()
.map(|part| part.text.as_ref())
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(feature = "providers")]
pub(crate) fn system_text(prompt: &Value) -> Option<String> {
system(prompt).map(|system| match system {
System::Parts(parts) => joined(&parts),
System::Native(other) => other.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::Prompt;
use crate::core::{SourceId, Tainted};
#[test]
fn each_part_keeps_its_label() {
let prompt = Prompt::new()
.system(Tainted::trusted("policy".to_owned()))
.system(Tainted::from_source(
"a page".to_owned(),
SourceId::new("web"),
))
.input(Tainted::trusted("the task".to_owned()))
.build();
let label = |path: &str| prompt.label_at(path).expect(path).is_untrusted();
assert!(!label("/system/0"), "a trusted part was tainted");
assert!(label("/system/1"), "an untrusted part was trusted");
assert!(
label("/system"),
"the slot does not carry its untrusted part"
);
assert!(!label("/input"));
}
}