use crate::observe::{Observer, detail};
use crate::{Error, Result};
pub use crate::lua::LuaProgram;
mod build;
mod fence;
mod list;
pub use build::{Frontmatter, MAX_TOOL_ITERATIONS, MaxToolIterations, promptforge_version};
use build::{Heading, build_sections, collect_headings, line_add, split_frontmatter};
use fence::{exact_shared_openings, split_h1};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ParseErrorKind {
Frontmatter,
Structure,
Fence,
List,
Lua,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ParseError {
kind: ParseErrorKind,
span: Option<(usize, usize)>,
inner: Box<Error>,
}
fn classify_parse_error(inner: &Error) -> (ParseErrorKind, Option<(usize, usize)>) {
match inner {
Error::ParseStructured { kind, span, .. } => (*kind, *span),
Error::ParseFrontmatter { .. } => (ParseErrorKind::Frontmatter, None),
Error::LuaCompile { .. } => (ParseErrorKind::Lua, None),
Error::Parse(message) => {
let kind = if message.contains("frontmatter") {
ParseErrorKind::Frontmatter
} else if message.contains("fence") {
ParseErrorKind::Fence
} else if message.contains("list section") || message.contains("bullet item") {
ParseErrorKind::List
} else {
ParseErrorKind::Structure
};
(kind, None)
}
_ => (ParseErrorKind::Structure, None),
}
}
impl ParseError {
#[must_use]
pub fn kind(&self) -> ParseErrorKind {
self.kind
}
#[must_use]
pub fn span(&self) -> Option<(usize, usize)> {
self.span
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(&self.inner)
}
}
impl From<Error> for ParseError {
fn from(inner: Error) -> Self {
let (kind, span) = classify_parse_error(&inner);
ParseError {
kind,
span,
inner: Box::new(inner),
}
}
}
impl From<ParseError> for Error {
fn from(error: ParseError) -> Self {
*error.inner
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Block {
Lua(LuaProgram),
#[non_exhaustive]
Prose {
text: String,
loop_capable: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Section {
pub(crate) name: String,
pub(crate) level: u8,
pub(crate) blocks: Vec<Block>,
pub(crate) children: Vec<Section>,
pub(crate) items: Vec<String>,
}
impl Section {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn level(&self) -> u8 {
self.level
}
#[must_use]
pub fn blocks(&self) -> &[Block] {
&self.blocks
}
#[must_use]
pub fn children(&self) -> &[Section] {
&self.children
}
#[must_use]
pub fn items(&self) -> &[String] {
&self.items
}
#[must_use]
pub fn prologue(&self) -> Option<&LuaProgram> {
match self.blocks.first() {
Some(Block::Lua(program)) => Some(program),
_ => None,
}
}
#[must_use]
pub fn prose(&self) -> &str {
self.blocks
.iter()
.rev()
.find_map(|block| match block {
Block::Prose {
text,
loop_capable: true,
} => Some(text.as_str()),
_ => None,
})
.unwrap_or("")
}
#[must_use]
pub fn epilog(&self) -> Option<&LuaProgram> {
match self.blocks.as_slice() {
[Block::Lua(_)] => None,
[.., Block::Lua(program)] => Some(program),
_ => None,
}
}
#[must_use]
pub fn is_list_only(&self) -> bool {
!self.items.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Prompt {
pub(crate) frontmatter: Frontmatter,
pub(crate) title: String,
pub(crate) replay: Option<LuaProgram>,
pub(crate) h1_blocks: Vec<Block>,
pub(crate) description_text: String,
pub(crate) sections: Vec<Section>,
}
impl Prompt {
#[must_use]
pub fn frontmatter(&self) -> &Frontmatter {
&self.frontmatter
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn replay(&self) -> Option<&LuaProgram> {
self.replay.as_ref()
}
#[must_use]
pub fn h1_blocks(&self) -> &[Block] {
&self.h1_blocks
}
#[must_use]
pub fn sections(&self) -> &[Section] {
&self.sections
}
pub fn strip_h1_prose(&mut self) {
self.h1_blocks
.retain(|block| matches!(block, Block::Lua(_)));
self.description_text.clear();
}
}
impl Prompt {
pub fn parse(
input: &str,
execution: &str,
observer: &dyn Observer,
) -> std::result::Result<Prompt, ParseError> {
observer.observe(execution, "Prompt", detail::PARSE_STARTED);
let result = Self::parse_inner(input, execution, observer);
observer.observe(
execution,
"Prompt",
if result.is_ok() {
detail::PARSE_SUCCEEDED
} else {
detail::PARSE_FAILED
},
);
result.map_err(ParseError::from)
}
fn parse_inner(input: &str, execution: &str, observer: &dyn Observer) -> Result<Prompt> {
let (yaml, body, frontmatter_lines) = split_frontmatter(input)?;
let frontmatter: Frontmatter = serde_yaml_ng::from_str(&yaml).map_err(|e| {
Error::ParseFrontmatter {
message: e.to_string(),
source: Box::new(e),
}
})?;
let headings = collect_headings(&body)?;
let h1_positions: Vec<usize> = headings
.iter()
.enumerate()
.filter_map(|(index, heading)| (heading.level == 1).then_some(index))
.collect();
let [h1_index] = h1_positions.as_slice() else {
return Err(Error::Parse(if h1_positions.is_empty() {
"prompt requires an H1 title".into()
} else {
"prompt must contain exactly one H1 title".into()
}));
};
let h1 = &headings[*h1_index];
if h1.title.trim().is_empty() {
return Err(Error::Parse("prompt H1 title must not be empty".into()));
}
let title = h1.title.clone();
let h1_content_abs_line = line_add(frontmatter_lines, h1.content_start_line)?;
let shared_fences = exact_shared_openings(&body);
let h1_shared_fences = exact_shared_openings(&h1.content);
if shared_fences.len() > 1 {
return Err(Error::Parse(
"prompt allows at most one `lua shared` fence".into(),
));
}
if shared_fences.len() != h1_shared_fences.len() {
return Err(Error::Parse(
"`lua shared` fence is allowed only in H1".into(),
));
}
let (replay, h1_blocks, description_text) = split_h1(
&h1.content,
&title,
h1_content_abs_line,
execution,
observer,
)?;
let section_headings: Vec<Heading> = headings
.into_iter()
.skip(*h1_index + 1)
.filter(|h| h.level >= 2)
.collect();
if section_headings.is_empty() {
return Err(Error::Parse("prompt has no ## sections".into()));
}
let mut pos = 0;
let sections = build_sections(
§ion_headings,
&mut pos,
1,
frontmatter_lines,
execution,
observer,
)?;
Ok(Prompt {
frontmatter,
title,
replay,
h1_blocks,
description_text,
sections,
})
}
#[must_use]
pub fn entry(&self) -> &Section {
&self.sections[0]
}
}
#[cfg(test)]
mod tests;