use serde::{Deserialize, Serialize};
use crate::canon::CanonicalEncoder;
use super::dialect::DialectRef;
use super::route::PromptRoute;
use super::section::{PromptMessageRole, PromptSectionId, SectionProvenance, PROMPT_DIGEST_TAG};
use super::stage::PromptStage;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompiledPromptMessage {
pub role: PromptMessageRole,
pub content: String,
pub sections: Vec<SectionProvenance>,
pub rendered_hash: String,
}
impl CompiledPromptMessage {
pub fn hash_content(content: &str) -> String {
let mut encoder = CanonicalEncoder::new(PROMPT_DIGEST_TAG);
encoder.text("rendered-message").text(content);
encoder.digest()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompiledPromptProgram {
pub route: PromptRoute,
pub stage: PromptStage,
pub dialect: DialectRef,
pub messages: Vec<CompiledPromptMessage>,
pub dropped_sections: Vec<PromptSectionId>,
pub tool_spec_hash: String,
pub program_digest: String,
}
impl CompiledPromptProgram {
#[allow(clippy::too_many_arguments)]
pub fn compute_digest(
route: &PromptRoute,
stage: PromptStage,
dialect: &DialectRef,
accountant_id: &str,
token_budget: u64,
messages: &[CompiledPromptMessage],
dropped: &[PromptSectionId],
tool_spec_hash: &str,
) -> String {
let mut encoder = CanonicalEncoder::new(PROMPT_DIGEST_TAG);
encoder
.text("program")
.text(&route.adapter)
.text(&format!("{:?}", route.family))
.text(route.exact_model.as_deref().unwrap_or(""))
.text(stage.dir_name())
.text(&dialect.id)
.u64(u64::from(dialect.version))
.text(accountant_id)
.u64(token_budget);
for message in messages {
encoder.text(match message.role {
PromptMessageRole::System => "system",
PromptMessageRole::User => "user",
});
encoder.text(&message.rendered_hash);
for section in &message.sections {
encoder
.text(§ion.id.0)
.u64(u64::from(section.version.0))
.text(§ion.content_hash)
.text(&format!("{:?}", section.origin));
}
}
encoder.list(dropped.iter().map(|id| id.0.as_str()));
encoder.text(tool_spec_hash);
encoder.digest()
}
}
impl CompiledPromptProgram {
pub fn system_text(&self) -> String {
self.messages
.iter()
.filter(|message| message.role == PromptMessageRole::System)
.map(|message| message.content.as_str())
.collect::<Vec<_>>()
.join("\n\n")
}
}
pub fn tool_surface_hash(specs: &[crate::model::ToolSpec]) -> String {
let mut encoder = CanonicalEncoder::new(PROMPT_DIGEST_TAG);
encoder.text("tool-surface");
for spec in specs {
encoder
.text(&spec.name)
.text(&spec.description)
.text(&spec.schema.to_string())
.bool(spec.strict);
}
encoder.digest()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompiledPromptInvocation {
pub platform: CompiledPromptProgram,
pub domain: CompiledPromptProgram,
pub invocation_digest: String,
}
impl CompiledPromptInvocation {
pub fn new(platform: CompiledPromptProgram, domain: CompiledPromptProgram) -> Self {
let mut encoder = CanonicalEncoder::new(PROMPT_DIGEST_TAG);
encoder
.text("invocation")
.text(&platform.program_digest)
.text(&domain.program_digest);
let invocation_digest = encoder.digest();
Self {
platform,
domain,
invocation_digest,
}
}
}