use std::{collections::HashMap, sync::LazyLock};
use async_trait::async_trait;
use regex::Regex;
use serde_json::{json, Value};
use crate::{
chain::{Chain, ChainError, LLMChainBuilder, DEFAULT_RESULT_KEY},
language_models::{llm::LLM, GenerateResult, TokenUsage},
output_parsers::strip_reasoning,
prompt::{PromptArgs, PromptTemplate, TemplateFormat},
tools::{evaluate_expression, format_number},
};
pub const LLM_MATH_DEFAULT_INPUT_KEY: &str = "question";
pub const LLM_MATH_DEFAULT_OUTPUT_KEY: &str = "answer";
pub const LLM_MATH_EXPRESSION_KEY: &str = "expression";
const PROMPT: &str = r#"Translate a math problem into a single-line arithmetic expression and put it in a ```text block. A calculator evaluates it: it supports + - * / % // ^ (power), parentheses, the constants pi and e, and functions such as sqrt, cbrt, abs, exp, ln, log, log10, sin, cos, tan, floor, ceil, round, min, max, pow and factorial. Don't compute the result yourself.
Question: ${Question with math problem.}
```text
${single line expression that solves the problem}
```
Question: What is 37593 * 67?
```text
37593 * 67
```
Question: What is the fifth root of 37593?
```text
37593 ^ (1 / 5)
```
Question: {{question}}
"#;
static FENCED: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?s)```[A-Za-z0-9_-]*[ \t]*\r?\n?(.*?)(?:```|\z)").expect("valid regex")
});
#[derive(Clone, Debug, PartialEq)]
pub struct MathAnswer {
pub answer: String,
pub expression: Option<String>,
pub tokens: Option<TokenUsage>,
}
pub struct LLMMathChain {
llm_chain: Box<dyn Chain>,
input_key: String,
output_key: String,
}
impl LLMMathChain {
pub fn new<L: Into<Box<dyn LLM>>>(llm: L) -> Self {
let chain = LLMChainBuilder::new()
.prompt(PromptTemplate::new(
PROMPT.to_string(),
vec![LLM_MATH_DEFAULT_INPUT_KEY.to_string()],
TemplateFormat::Jinja2,
))
.llm(llm)
.build()
.expect("prompt and llm are set");
Self::from_chain(chain)
}
pub fn from_chain<C: Into<Box<dyn Chain>>>(chain: C) -> Self {
Self {
llm_chain: chain.into(),
input_key: LLM_MATH_DEFAULT_INPUT_KEY.into(),
output_key: LLM_MATH_DEFAULT_OUTPUT_KEY.into(),
}
}
pub fn with_input_key<S: Into<String>>(mut self, key: S) -> Self {
self.input_key = key.into();
self
}
pub fn with_output_key<S: Into<String>>(mut self, key: S) -> Self {
self.output_key = key.into();
self
}
pub async fn solve(&self, input_variables: PromptArgs) -> Result<MathAnswer, ChainError> {
let mut inputs = input_variables;
if self.input_key != LLM_MATH_DEFAULT_INPUT_KEY {
let question = inputs
.remove(&self.input_key)
.ok_or_else(|| ChainError::MissingInputVariable(self.input_key.clone()))?;
inputs.insert(LLM_MATH_DEFAULT_INPUT_KEY.to_string(), question);
}
let result = self.llm_chain.call(inputs).await?;
let (answer, expression) = parse_math_output(&result.generation)?;
Ok(MathAnswer {
answer,
expression,
tokens: result.tokens,
})
}
}
pub(crate) fn parse_math_output(output: &str) -> Result<(String, Option<String>), ChainError> {
let output = strip_reasoning(output).trim();
if let Some(caps) = FENCED.captures(output) {
let expression = caps[1].trim();
if !expression.is_empty() {
let value = evaluate_expression(expression).map_err(|e| {
ChainError::OtherError(format!("llm math: evaluating `{expression}`: {e}"))
})?;
return Ok((format_number(value), Some(expression.to_string())));
}
}
if let Some(start) = output.rfind("Answer:") {
let answer = output[start + "Answer:".len()..].trim();
if !answer.is_empty() {
return Ok((answer.to_string(), None));
}
}
Err(ChainError::OtherError(format!(
"llm math: expected an expression in a ``` block or an `Answer:` line, got: {output:?}"
)))
}
#[async_trait]
impl Chain for LLMMathChain {
async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
let answer = self.solve(input_variables).await?;
Ok(GenerateResult {
generation: answer.answer,
tokens: answer.tokens,
..Default::default()
})
}
async fn execute(
&self,
input_variables: PromptArgs,
) -> Result<HashMap<String, Value>, ChainError> {
let answer = self.solve(input_variables).await?;
let result = GenerateResult {
generation: answer.answer.clone(),
tokens: answer.tokens,
..Default::default()
};
let mut output = HashMap::new();
output.insert(self.output_key.clone(), json!(answer.answer));
output.insert(DEFAULT_RESULT_KEY.to_string(), json!(result));
if let Some(expression) = answer.expression {
output.insert(LLM_MATH_EXPRESSION_KEY.to_string(), json!(expression));
}
Ok(output)
}
fn get_input_keys(&self) -> Vec<String> {
vec![self.input_key.clone()]
}
fn get_output_keys(&self) -> Vec<String> {
vec![
self.output_key.clone(),
DEFAULT_RESULT_KEY.to_string(),
LLM_MATH_EXPRESSION_KEY.to_string(),
]
}
}
#[cfg(test)]
mod tests;