use std::sync::OnceLock;
use regex::Regex;
use serde_json::Value;
use super::OutputParserError;
pub trait TypedOutputParser: Send + Sync {
type Output;
fn format_instructions(&self) -> String;
fn parse_output(&self, text: &str) -> Result<Self::Output, OutputParserError>;
}
pub(crate) fn strip_reasoning(text: &str) -> &str {
let mut rest = text.trim_start();
while let Some(after_open) = rest.strip_prefix("<think>") {
match after_open.find("</think>") {
Some(end) => rest = after_open[end + "</think>".len()..].trim_start(),
None => break,
}
}
rest
}
fn fenced_block() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"```[A-Za-z0-9_-]*[ \t]*\r?\n?([\s\S]*?)```").unwrap())
}
pub(crate) fn extract_json(text: &str) -> Result<Value, OutputParserError> {
let text = strip_reasoning(text);
let mut candidates: Vec<&str> = fenced_block()
.captures_iter(text)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.collect();
candidates.push(text);
for (open, close) in [('{', '}'), ('[', ']')] {
if let (Some(start), Some(end)) = (text.find(open), text.rfind(close)) {
if start < end {
candidates.push(&text[start..=end]);
}
}
}
let mut first_err = None;
for candidate in candidates {
match serde_json::from_str::<Value>(candidate.trim()) {
Ok(value) => return Ok(value),
Err(e) => {
first_err.get_or_insert(e);
}
}
}
Err(OutputParserError::ParsingError(format!(
"no JSON found in output ({}): {}",
first_err.map(|e| e.to_string()).unwrap_or_default(),
preview(text)
)))
}
pub(crate) fn preview(text: &str) -> String {
const MAX: usize = 200;
match text.char_indices().nth(MAX) {
Some((cut, _)) => format!("{:?}...", &text[..cut]),
None => format!("{text:?}"),
}
}
macro_rules! string_output_parser {
($ty:ty, |$out:ident| $render:expr) => {
#[async_trait::async_trait]
impl $crate::output_parsers::OutputParser for $ty {
async fn parse(
&self,
output: &str,
) -> Result<String, $crate::output_parsers::OutputParserError> {
let $out = $crate::output_parsers::TypedOutputParser::parse_output(self, output)?;
Ok($render)
}
}
};
}
pub(crate) use string_output_parser;