use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::{
chain::{Chain, ChainError, LLMChain, DEFAULT_OUTPUT_KEY, DEFAULT_RESULT_KEY},
language_models::{llm::LLM, GenerateResult, TokenUsage},
output_parsers::strip_reasoning,
prompt::{PromptArgs, PromptTemplate, TemplateFormat},
prompt_args,
};
use super::LLMChainBuilder;
mod principles;
mod prompts;
pub const CONSTITUTIONAL_INITIAL_OUTPUT_KEY: &str = "initial_output";
pub const CONSTITUTIONAL_STEPS_KEY: &str = "critiques_and_revisions";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ConstitutionalPrinciple {
pub name: String,
pub critique_request: String,
pub revision_request: String,
}
impl ConstitutionalPrinciple {
pub fn new<C: Into<String>, R: Into<String>>(critique_request: C, revision_request: R) -> Self {
Self {
name: "Constitutional Principle".into(),
critique_request: critique_request.into(),
revision_request: revision_request.into(),
}
}
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
self.name = name.into();
self
}
pub fn get(name: &str) -> Option<Self> {
principles::PRINCIPLES
.iter()
.find(|(n, _, _)| *n == name)
.map(|(name, critique, revision)| Self::new(*critique, *revision).with_name(*name))
}
pub fn builtin_names() -> impl Iterator<Item = &'static str> {
principles::PRINCIPLES.iter().map(|(name, _, _)| *name)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CritiqueRevision {
pub principle: String,
pub critique: String,
pub revision: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConstitutionalOutput {
pub initial_output: String,
pub output: String,
pub critiques_and_revisions: Vec<CritiqueRevision>,
pub tokens: Option<TokenUsage>,
}
pub struct ConstitutionalChain {
chain: LLMChain,
critique_chain: Box<dyn Chain>,
revision_chain: Box<dyn Chain>,
principles: Vec<ConstitutionalPrinciple>,
output_key: String,
return_intermediate_steps: bool,
}
const CRITIQUE_VARIABLES: [&str; 3] = ["input_prompt", "output_from_model", "critique_request"];
const REVISION_VARIABLES: [&str; 5] = [
"input_prompt",
"output_from_model",
"critique_request",
"critique",
"revision_request",
];
impl ConstitutionalChain {
pub fn new<L: Into<Box<dyn LLM>>>(
chain: LLMChain,
llm: L,
principles: Vec<ConstitutionalPrinciple>,
) -> Self {
let llm = llm.into();
Self {
chain,
critique_chain: Box::new(llm_chain(
llm.clone_box(),
critique_template(),
&CRITIQUE_VARIABLES,
)),
revision_chain: Box::new(llm_chain(llm, revision_template(), &REVISION_VARIABLES)),
principles,
output_key: DEFAULT_OUTPUT_KEY.into(),
return_intermediate_steps: false,
}
}
pub fn with_critique_chain<C: Into<Box<dyn Chain>>>(mut self, chain: C) -> Self {
self.critique_chain = chain.into();
self
}
pub fn with_revision_chain<C: Into<Box<dyn Chain>>>(mut self, chain: C) -> Self {
self.revision_chain = chain.into();
self
}
pub fn with_output_key<S: Into<String>>(mut self, key: S) -> Self {
self.output_key = key.into();
self
}
pub fn with_return_intermediate_steps(mut self, enabled: bool) -> Self {
self.return_intermediate_steps = enabled;
self
}
pub async fn critique_and_revise(
&self,
input_variables: PromptArgs,
) -> Result<ConstitutionalOutput, ChainError> {
let input_prompt = self.chain.prompt_text(input_variables.clone()).await?;
let first = self.chain.call(input_variables).await?;
let mut tokens = first.tokens;
let initial_output = strip_reasoning(&first.generation).trim().to_string();
let mut response = initial_output.clone();
let mut steps = Vec::with_capacity(self.principles.len());
for principle in &self.principles {
let critique = self
.critique_chain
.call(prompt_args! {
"input_prompt" => input_prompt,
"output_from_model" => response,
"critique_request" => principle.critique_request,
})
.await?;
add_usage(&mut tokens, &critique.tokens);
let critique = parse_critique(&critique.generation);
if critique.to_lowercase().contains("no critique needed") {
steps.push(CritiqueRevision {
principle: principle.name.clone(),
critique,
revision: None,
});
continue;
}
let revision = self
.revision_chain
.call(prompt_args! {
"input_prompt" => input_prompt,
"output_from_model" => response,
"critique_request" => principle.critique_request,
"critique" => critique,
"revision_request" => principle.revision_request,
})
.await?;
add_usage(&mut tokens, &revision.tokens);
let revision = parse_revision(&revision.generation);
let revision = if revision.to_lowercase().starts_with("no revisions needed") {
None
} else {
response = revision.clone();
Some(revision)
};
steps.push(CritiqueRevision {
principle: principle.name.clone(),
critique,
revision,
});
}
Ok(ConstitutionalOutput {
initial_output,
output: response,
critiques_and_revisions: steps,
tokens,
})
}
}
#[async_trait::async_trait]
impl Chain for ConstitutionalChain {
async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
let out = self.critique_and_revise(input_variables).await?;
Ok(GenerateResult {
generation: out.output,
tokens: out.tokens,
..Default::default()
})
}
async fn execute(
&self,
input_variables: PromptArgs,
) -> Result<HashMap<String, Value>, ChainError> {
let out = self.critique_and_revise(input_variables).await?;
let result = GenerateResult {
generation: out.output.clone(),
tokens: out.tokens,
..Default::default()
};
let mut output = HashMap::new();
output.insert(self.output_key.clone(), json!(out.output));
output.insert(DEFAULT_RESULT_KEY.to_string(), json!(result));
if self.return_intermediate_steps {
output.insert(
CONSTITUTIONAL_INITIAL_OUTPUT_KEY.to_string(),
json!(out.initial_output),
);
output.insert(
CONSTITUTIONAL_STEPS_KEY.to_string(),
json!(out.critiques_and_revisions),
);
}
Ok(output)
}
fn get_input_keys(&self) -> Vec<String> {
self.chain.get_input_keys()
}
fn get_output_keys(&self) -> Vec<String> {
let mut keys = vec![self.output_key.clone(), DEFAULT_RESULT_KEY.to_string()];
if self.return_intermediate_steps {
keys.push(CONSTITUTIONAL_INITIAL_OUTPUT_KEY.to_string());
keys.push(CONSTITUTIONAL_STEPS_KEY.to_string());
}
keys
}
}
fn add_usage(total: &mut Option<TokenUsage>, usage: &Option<TokenUsage>) {
if let Some(usage) = usage {
total.get_or_insert_with(TokenUsage::default).add(usage);
}
}
fn parse_critique(raw: &str) -> String {
let critique = strip_reasoning(raw);
let lower = critique.to_ascii_lowercase();
let end = lower.find("revision request:").unwrap_or(critique.len());
strip_label(&critique[..end], "critique:").to_string()
}
fn parse_revision(raw: &str) -> String {
strip_label(strip_reasoning(raw), "revision:").to_string()
}
fn strip_label<'a>(text: &'a str, label: &str) -> &'a str {
let text = text.trim();
match text.get(..label.len()) {
Some(start) if start.eq_ignore_ascii_case(label) => text[label.len()..].trim(),
_ => text,
}
}
fn llm_chain(llm: Box<dyn LLM>, template: String, variables: &[&str]) -> LLMChain {
LLMChainBuilder::new()
.prompt(PromptTemplate::new(
template,
variables.iter().map(|v| v.to_string()).collect(),
TemplateFormat::Jinja2,
))
.llm(llm)
.build()
.expect("prompt and llm are set")
}
const EXAMPLE_SEPARATOR: &str = "\n === \n";
fn critique_example(e: &prompts::Example) -> String {
format!(
"Human: {}\n\nModel: {}\n\nCritique Request: {}\n\nCritique: {}",
e.input_prompt, e.output_from_model, e.critique_request, e.critique
)
}
pub(super) fn critique_template() -> String {
let mut parts = vec!["Below is a conversation between a human and an AI model. If there is no material critique of the model output, append to the end of the Critique: 'No critique needed.' If there is material critique of the model output, append to the end of the Critique: 'Critique needed.'".to_string()];
parts.extend(prompts::EXAMPLES.iter().map(critique_example));
parts.push(
"Human: {{input_prompt}}\n\nModel: {{output_from_model}}\n\nCritique Request: {{critique_request}}\n\nCritique:"
.to_string(),
);
parts.join(EXAMPLE_SEPARATOR)
}
pub(super) fn revision_template() -> String {
let mut parts = vec!["Below is a conversation between a human and an AI model.".to_string()];
parts.extend(prompts::EXAMPLES.iter().map(|e| {
format!(
"{}\n\nRevision Request: {}\n\nRevision: {}",
critique_example(e),
e.revision_request,
e.revision
)
}));
parts.push(
"Human: {{input_prompt}}\n\nModel: {{output_from_model}}\n\nCritique Request: {{critique_request}}\n\nCritique: {{critique}}\n\nIf the critique does not identify anything worth changing, ignore the Revision Request and do not make any revisions. Instead, return \"No revisions needed\".\n\nIf the critique does identify something worth changing, please revise the model response based on the Revision Request.\n\nRevision Request: {{revision_request}}\n\nRevision:"
.to_string(),
);
parts.join(EXAMPLE_SEPARATOR)
}
#[cfg(test)]
mod tests;