use std::cell::OnceCell;
use orch_response::{OrchResponseVariants, ResponseSchemaField};
use crate::{alignment::AlignmentStrategy, lm::LanguageModel};
use super::{
generate_embedding, Executor, ExecutorBuilderError, ExecutorContext, ExecutorError,
ExecutorTextCompleteResponse, DEFAULT_PREAMBLE,
};
pub struct StructuredExecutor<'a, T> {
pub(crate) lm: &'a dyn LanguageModel,
pub(crate) preamble: Option<&'a str>,
pub(crate) variants: Box<dyn OrchResponseVariants<T>>,
pub(crate) alignment_strategy: Option<AlignmentStrategy<'a>>,
}
impl<'a, T> Executor<'a> for StructuredExecutor<'a, T> {
fn lm(&self) -> &'a dyn LanguageModel {
self.lm
}
fn system_prompt(&self) -> String {
let cell = OnceCell::new();
cell.get_or_init(|| {
let response_options = self.variants.variants();
let all_types = response_options
.iter()
.map(|option| option.type_name.clone())
.collect::<Vec<_>>();
let response_options_text = response_options
.iter()
.map(|option| {
let mut schema_text = String::new();
let mut schema_example = "{".to_string();
let type_field = ResponseSchemaField {
name: "response_type".to_string(),
description: format!(
"The type of the response (\"{}\" in this case)",
option.type_name
)
.to_string(),
typ: "string".to_string(),
example: all_types.first().unwrap().to_string(),
};
for (i, field) in option
.schema
.iter()
.chain(std::iter::once(&type_field))
.enumerate()
{
schema_text.push_str(&format!(
" - `{}` of type {} (description: {})\n\n",
field.name, field.typ, field.description
));
schema_example
.push_str(&format!("\"{}\": \"{}\"", field.name, field.example));
if i < option.schema.len() - 1 {
schema_example.push(',');
}
}
schema_example.push('}');
format!(
"SCENARIO: {}\nDESCRIPTION: {}\nSCHEMA:\n{}\nEXAMPLE RESPONSE: {}\n\n\n",
option.scenario, option.description, schema_text, schema_example
)
})
.collect::<Vec<_>>()
.join("\n");
let preamble = self.preamble.map(|pa| format!("Additional information: {}", pa)).unwrap_or("".to_owned());
let system_prompt = format!(
"
You will receive a prompt from a user, and will need to response with a JSON object that represents the response.
Response *only* with the JSON object, and nothing else. No additional preamble or explanations. Only work with the responses you can reply with.
{preamble}
You have {choices_len} choices to respond, in a JSON format:
{response_options_text}
",
preamble = preamble,
choices_len = response_options.len(),
response_options_text = response_options_text
)
.trim()
.to_string();
system_prompt
})
.clone()
}
}
impl<'a, T> StructuredExecutor<'a, T> {
pub async fn execute(
&'a self,
prompt: &'a str,
) -> Result<ExecutorTextCompleteResponse<T>, ExecutorError> {
let mut model_response = self.text_complete(prompt).await?.content;
if let Some(alignment_strategy) = &self.alignment_strategy {
model_response = alignment_strategy
.align(
self.lm,
self.preamble.unwrap_or(DEFAULT_PREAMBLE),
prompt,
&model_response,
)
.await
.map_err(ExecutorError::Alignment)?;
}
let result = self
.variants
.parse(&model_response)
.map_err(|e| ExecutorError::Parsing(format!("{e}\nResponse: {:?}", model_response)))?;
Ok(ExecutorTextCompleteResponse {
content: result,
context: ExecutorContext {},
})
}
pub async fn generate_embedding(&'a self, prompt: &'a str) -> Result<Vec<f32>, ExecutorError> {
generate_embedding(self.lm, prompt).await
}
}
#[derive(Default)]
pub struct StructuredExecutorBuilder<'a, T> {
lm: Option<&'a dyn LanguageModel>,
preamble: Option<&'a str>,
variants: Option<Box<dyn OrchResponseVariants<T>>>,
alignment_strategy: Option<AlignmentStrategy<'a>>,
}
impl<'a, T> StructuredExecutorBuilder<'a, T> {
pub fn new() -> Self {
Self {
lm: None,
preamble: None,
variants: None,
alignment_strategy: None,
}
}
pub fn with_lm(mut self, lm: &'a dyn LanguageModel) -> Self {
self.lm = Some(lm);
self
}
pub fn with_options(mut self, options: Box<dyn OrchResponseVariants<T>>) -> Self {
self.variants = Some(options);
self
}
pub fn with_preamble(mut self, preamble: &'a str) -> Self {
self.preamble = Some(preamble);
self
}
pub fn with_alignment(mut self, strategy: AlignmentStrategy<'a>) -> Self {
self.alignment_strategy = Some(strategy);
self
}
pub fn try_build(self) -> Result<StructuredExecutor<'a, T>, ExecutorBuilderError> {
let Some(lm) = self.lm else {
return Err(ExecutorBuilderError::ConfigurationNotSet(
"Language model".to_string(),
));
};
let Some(response_options) = self.variants else {
return Err(ExecutorBuilderError::InternalError(
"Response variants are not set".to_string(),
));
};
Ok(StructuredExecutor {
lm,
preamble: self.preamble,
variants: response_options,
alignment_strategy: self.alignment_strategy,
})
}
}