use super::reminders::*;
use super::*;
#[derive(Clone, Copy)]
pub(super) enum SystemPromptPosition {
Before,
After,
}
pub(super) fn system_prompt_error(message: impl Into<String>) -> VmError {
VmError::Thrown(VmValue::String(std::sync::Arc::from(message.into())))
}
pub(super) fn system_prompt_position(
value: Option<&VmValue>,
source: &str,
fallback: SystemPromptPosition,
) -> Result<SystemPromptPosition, VmError> {
let Some(value) = value else {
return Ok(fallback);
};
match value {
VmValue::Nil => Ok(fallback),
VmValue::String(raw) => match raw.as_ref() {
"before" | "prepend" | "prefix" | "start" => Ok(SystemPromptPosition::Before),
"after" | "append" | "suffix" | "end" => Ok(SystemPromptPosition::After),
other => Err(system_prompt_error(format!(
"{source}.position: expected \"before\" or \"after\", got \"{other}\""
))),
},
other => Err(system_prompt_error(format!(
"{source}.position: expected a string, got {}",
other.type_name()
))),
}
}
pub(super) fn enabled_system_prompt_part(part: &BTreeMap<String, VmValue>) -> bool {
!matches!(
part.get("enabled"),
Some(VmValue::Bool(false) | VmValue::Nil)
)
}
pub(super) fn system_prompt_part_content(part: &BTreeMap<String, VmValue>) -> Option<String> {
part.get("content")
.or_else(|| part.get("text"))
.or_else(|| part.get("prompt"))
.map(VmValue::display)
}
pub(super) fn render_system_prompt_part(
content: String,
part: &BTreeMap<String, VmValue>,
) -> String {
let title = part
.get("label")
.or_else(|| part.get("title"))
.or_else(|| part.get("name"))
.map(VmValue::display)
.unwrap_or_default();
let title = title.trim();
let content = content.trim();
if title.is_empty() {
content.to_string()
} else {
format!("## {title}\n{content}")
}
}
pub(super) fn append_host_fragments(
out: &mut Vec<crate::llm::prompt::PromptFragment>,
value: Option<&VmValue>,
source: &str,
forced_position: SystemPromptPosition,
) -> Result<(), VmError> {
use crate::llm::prompt::PromptFragment;
let Some(value) = value else {
return Ok(());
};
match value {
VmValue::Nil | VmValue::Bool(false) => Ok(()),
VmValue::String(text) => {
out.push(PromptFragment::new(
format!("host:{source}"),
format!("host:{source}"),
fragment_bucket(forced_position),
text.to_string(),
));
Ok(())
}
VmValue::List(items) => {
for (index, item) in items.iter().enumerate() {
append_host_fragments(
out,
Some(item),
&format!("{source}[{index}]"),
forced_position,
)?;
}
Ok(())
}
VmValue::Dict(part) => {
if !enabled_system_prompt_part(part) {
return Ok(());
}
let position = system_prompt_position(part.get("position"), source, forced_position)?;
if let Some(parts) = part.get("parts") {
return append_host_fragments(out, Some(parts), source, position);
}
let content = system_prompt_part_content(part).ok_or_else(|| {
system_prompt_error(format!(
"{source}: system prompt part must include `content`, `text`, `prompt`, or `parts`"
))
})?;
let rendered = render_system_prompt_part(content, part);
out.push(PromptFragment::new(
format!("host:{source}"),
format!("host:{source}"),
fragment_bucket(position),
rendered,
));
Ok(())
}
other => Err(system_prompt_error(format!(
"{source}: expected a string, dict, list, nil, or false; got {}",
other.type_name()
))),
}
}
pub(super) fn fragment_bucket(
position: SystemPromptPosition,
) -> crate::llm::prompt::FragmentBucket {
match position {
SystemPromptPosition::Before => crate::llm::prompt::FragmentBucket::Before,
SystemPromptPosition::After => crate::llm::prompt::FragmentBucket::After,
}
}
pub(super) fn system_prompt_fingerprint(system: &str) -> String {
use sha2::Digest as _;
let digest = sha2::Sha256::digest(system.as_bytes());
format!("sha256:{}", hex::encode(digest))
}
pub(crate) fn system_prompt_metadata(system: &str) -> serde_json::Value {
let fingerprint = system_prompt_fingerprint(system);
serde_json::json!({
"content": system,
"hash": fingerprint,
"sha256": fingerprint,
"bytes": system.len(),
})
}
pub(crate) fn system_prompt_event_metadata(system: &str) -> serde_json::Value {
let fingerprint = system_prompt_fingerprint(system);
serde_json::json!({
"hash": fingerprint,
"sha256": fingerprint,
"bytes": system.len(),
})
}
pub(crate) fn compose_system_prompt(
system: Option<String>,
options: Option<&BTreeMap<String, VmValue>>,
) -> Result<Option<String>, VmError> {
compose_system_prompt_with_reminders(system, options, &[])
}
pub(super) fn compose_system_prompt_with_reminders(
system: Option<String>,
options: Option<&BTreeMap<String, VmValue>>,
rendered_reminders: &[RenderedReminder],
) -> Result<Option<String>, VmError> {
Ok(assemble_system_prompt(system, options, rendered_reminders)?.system)
}
pub(crate) fn assemble_system_prompt(
system: Option<String>,
options: Option<&BTreeMap<String, VmValue>>,
rendered_reminders: &[RenderedReminder],
) -> Result<crate::llm::prompt::AssembledPrompt, VmError> {
use crate::llm::prompt::{assemble, FragmentBucket, PromptFragment};
let mut fragments: Vec<PromptFragment> = Vec::new();
if let Some(options) = options {
append_host_fragments(
&mut fragments,
options.get("system_preamble"),
"system_preamble",
SystemPromptPosition::Before,
)?;
append_host_fragments(
&mut fragments,
options.get("system_prefix"),
"system_prefix",
SystemPromptPosition::Before,
)?;
append_host_fragments(
&mut fragments,
options.get("system_context"),
"system_context",
SystemPromptPosition::Before,
)?;
append_host_fragments(
&mut fragments,
options.get("system_prompt_parts"),
"system_prompt_parts",
SystemPromptPosition::Before,
)?;
append_host_fragments(
&mut fragments,
options.get("system_appendix"),
"system_appendix",
SystemPromptPosition::After,
)?;
append_host_fragments(
&mut fragments,
options.get("system_suffix"),
"system_suffix",
SystemPromptPosition::After,
)?;
}
let decomposed = append_decomposed_primary_fragments(&mut fragments, options)?;
if !decomposed {
let primary_system = system
.filter(|system| !system.trim().is_empty())
.or_else(|| {
options
.and_then(|options| options.get("system"))
.filter(|value| !matches!(value, VmValue::Nil | VmValue::Bool(false)))
.map(VmValue::display)
.filter(|system| !system.trim().is_empty())
});
if let Some(system) = primary_system {
fragments.push(PromptFragment::new(
"primary",
"primary",
FragmentBucket::Before,
system,
));
}
append_context_profile_fragments(&mut fragments, options);
}
append_tool_guidance_fragments(&mut fragments, options);
let _ = rendered_reminders;
let ctx = assemble_ctx(options);
Ok(assemble(&fragments, &ctx))
}
pub(super) fn tool_names_from_options(
options: Option<&BTreeMap<String, VmValue>>,
) -> std::collections::BTreeSet<String> {
let mut names = std::collections::BTreeSet::new();
let Some(list) = options.and_then(|options| tool_entry_list(options.get("tools"))) else {
return names;
};
for entry in list.iter() {
if let Some(name) = entry
.as_dict()
.and_then(|dict| dict.get("name"))
.map(VmValue::display)
.filter(|name| !name.is_empty())
{
names.insert(name);
}
}
names
}
pub(super) fn tool_entry_list(value: Option<&VmValue>) -> Option<Vec<VmValue>> {
match value? {
VmValue::List(items) => Some((**items).clone()),
VmValue::Dict(dict) => match dict.get("tools") {
Some(VmValue::List(items)) => Some((**items).clone()),
_ => None,
},
_ => None,
}
}
pub(super) fn append_tool_guidance_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&BTreeMap<String, VmValue>>,
) {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(list) = options.and_then(|options| tool_entry_list(options.get("tools"))) else {
return;
};
for entry in list.iter() {
let Some(dict) = entry.as_dict() else {
continue;
};
let Some(name) = dict
.get("name")
.map(VmValue::display)
.filter(|name| !name.is_empty())
else {
continue;
};
let guidance = dict
.get("guidance")
.or_else(|| dict.get("system_guidance"))
.map(VmValue::display)
.map(|text| text.trim().to_string())
.filter(|text| !text.is_empty());
let Some(guidance) = guidance else {
continue;
};
fragments.push(
PromptFragment::new(
format!("tool:{name}.guidance"),
format!("tool:{name}"),
FragmentBucket::Before,
guidance,
)
.requiring_tools(vec![name]),
);
}
}
pub(super) fn append_decomposed_primary_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&BTreeMap<String, VmValue>>,
) -> Result<bool, VmError> {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(VmValue::List(items)) = options.and_then(|options| options.get("_system_fragments"))
else {
return Ok(false);
};
for (index, item) in items.iter().enumerate() {
let Some(dict) = item.as_dict() else {
continue;
};
let Some(body) = dict.get("body").map(VmValue::display) else {
continue;
};
let id = dict
.get("id")
.map(VmValue::display)
.filter(|id| !id.is_empty())
.unwrap_or_else(|| format!("primary[{index}]"));
let source = dict
.get("source")
.map(VmValue::display)
.filter(|source| !source.is_empty())
.unwrap_or_else(|| "primary".to_string());
let requires_tools = match dict.get("requires_tools") {
Some(VmValue::List(tools)) => tools.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
let bucket = match dict
.get("bucket")
.map(VmValue::display)
.map(|bucket| bucket.trim().to_ascii_lowercase())
.as_deref()
{
None | Some("") | Some("before") => FragmentBucket::Before,
Some("after") => FragmentBucket::After,
Some(other) => {
return Err(VmError::Runtime(format!(
"_system_fragments[{index}].bucket must be \"before\" or \"after\"; got {other:?}"
)));
}
};
let requires_caps = match dict.get("requires_caps") {
Some(VmValue::List(caps)) => caps.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
fragments.push(
PromptFragment::new(id, source, bucket, body)
.requiring_tools(requires_tools)
.requiring_caps(requires_caps),
);
}
Ok(true)
}
pub(super) fn append_context_profile_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&BTreeMap<String, VmValue>>,
) {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(profile) = options
.and_then(|options| {
options
.get("context_profile")
.or_else(|| options.get("project_context_profile"))
})
.and_then(VmValue::as_dict)
else {
return;
};
let Some(VmValue::List(items)) = profile.get("prompt_fragments") else {
return;
};
for (index, item) in items.iter().enumerate() {
let Some(dict) = item.as_dict() else {
continue;
};
let Some(body) = dict
.get("body")
.or_else(|| dict.get("content"))
.map(VmValue::display)
.map(|body| body.trim().to_string())
.filter(|body| !body.is_empty())
else {
continue;
};
let id = dict
.get("id")
.map(VmValue::display)
.filter(|id| !id.is_empty())
.unwrap_or_else(|| format!("profile[{index}]"));
let source = dict
.get("source")
.map(VmValue::display)
.filter(|source| !source.is_empty())
.unwrap_or_else(|| "profile".to_string());
let requires_tools = match dict.get("requires_tools") {
Some(VmValue::List(tools)) => tools.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
let requires_caps = match dict.get("requires_caps") {
Some(VmValue::List(caps)) => caps.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
fragments.push(
PromptFragment::new(id, source, FragmentBucket::Before, body)
.requiring_tools(requires_tools)
.requiring_caps(requires_caps),
);
}
}
pub(super) fn assemble_ctx(
options: Option<&BTreeMap<String, VmValue>>,
) -> crate::llm::prompt::AssembleCtx {
crate::llm::prompt::AssembleCtx {
tool_names: tool_names_from_options(options),
caps: caps_from_options(options),
}
}
pub(super) fn caps_from_options(
options: Option<&BTreeMap<String, VmValue>>,
) -> std::collections::BTreeSet<String> {
let mut caps = std::collections::BTreeSet::new();
let Some(options) = options else {
return caps;
};
collect_caps(options.get("caps"), &mut caps);
collect_caps(options.get("capabilities"), &mut caps);
if let Some(profile) = options
.get("context_profile")
.or_else(|| options.get("project_context_profile"))
.and_then(VmValue::as_dict)
{
collect_caps(profile.get("caps"), &mut caps);
}
caps
}
pub(super) fn collect_caps(value: Option<&VmValue>, out: &mut std::collections::BTreeSet<String>) {
match value {
Some(VmValue::List(items)) => {
for item in items.iter() {
let cap = item.display();
if !cap.is_empty() {
out.insert(cap);
}
}
}
Some(VmValue::Dict(dict)) => {
for (key, value) in dict.iter() {
if !matches!(value, VmValue::Bool(false) | VmValue::Nil) {
out.insert(key.clone());
}
}
}
Some(value) => {
let cap = value.display();
if !cap.is_empty() {
out.insert(cap);
}
}
None => {}
}
}