use crate::recursive::defaults::Defaults;
use crate::recursive::llm::Llm;
use crate::recursive::shared;
use crate::recursive::skill::Skill;
use crate::recursive::validate::{NoValidation, Score, Validate};
pub fn reason<'a, L: Llm>(llm: &'a L, prompt: &'a str) -> Reason<'a, L, NoValidation> {
Reason::new(llm, prompt)
}
#[derive(Clone)]
pub struct ReasonConfig {
pub reasoning_field: &'static str,
pub include_reasoning: bool,
pub max_iter: u32,
pub target: f64,
pub instruction: Option<&'static str>,
pub extract_lang: Option<String>,
pub defaults: Option<Defaults>,
pub skill_text: Option<String>,
}
impl Default for ReasonConfig {
fn default() -> Self {
Self {
reasoning_field: "reasoning",
include_reasoning: true,
max_iter: 5,
target: 1.0,
instruction: None,
extract_lang: None,
defaults: None,
skill_text: None,
}
}
}
pub struct Reason<'a, L: Llm, V: Validate> {
llm: &'a L,
prompt: &'a str,
validator: V,
pub config: ReasonConfig,
}
impl<'a, L: Llm> Reason<'a, L, NoValidation> {
pub fn new(llm: &'a L, prompt: &'a str) -> Self {
Self {
llm,
prompt,
validator: NoValidation,
config: ReasonConfig::default(),
}
}
}
impl<'a, L: Llm, V: Validate> Reason<'a, L, V> {
pub fn validate<V2: Validate>(self, validator: V2) -> Reason<'a, L, V2> {
Reason {
llm: self.llm,
prompt: self.prompt,
validator,
config: self.config,
}
}
pub fn skill(mut self, skill: &Skill<'_>) -> Self {
let rendered = skill.render();
if rendered.is_empty() {
self.config.skill_text = None;
} else {
self.config.skill_text = Some(rendered);
}
self
}
pub fn reasoning_field(mut self, name: &'static str) -> Self {
self.config.reasoning_field = name;
self
}
pub fn max_iter(mut self, n: u32) -> Self {
self.config.max_iter = n;
self
}
pub fn target(mut self, score: f64) -> Self {
self.config.target = score;
self
}
pub fn instruction(mut self, inst: &'static str) -> Self {
self.config.instruction = Some(inst);
self
}
pub fn extract(mut self, lang: impl Into<String>) -> Self {
self.config.extract_lang = Some(lang.into());
self
}
pub fn defaults(mut self, defaults: Defaults) -> Self {
self.config.defaults = Some(defaults);
self
}
pub fn no_reasoning(mut self) -> Self {
self.config.include_reasoning = false;
self
}
pub fn go(self) -> ReasonResult {
shared::block_on(self.run())
}
pub async fn run(self) -> ReasonResult {
let mut iterations = 0u32;
let mut total_tokens = 0u32;
let mut last_score = Score::pass();
let mut last_reasoning: Option<String> = None;
let mut last_output = String::new();
for iter in 0..self.config.max_iter {
iterations = iter + 1;
let cot_prompt = self.build_prompt(if iter == 0 {
None
} else {
last_score.feedback_str()
});
let output = match self.llm.generate(&cot_prompt, "", None).await {
Ok(out) => out,
Err(e) => {
return ReasonResult {
output: String::new(),
reasoning: None,
score: 0.0,
iterations,
tokens: total_tokens,
error: Some(e.to_string()),
};
}
};
total_tokens += output.prompt_tokens + output.completion_tokens;
let (reasoning, answer) = self.parse_response(&output.text);
last_reasoning = if self.config.include_reasoning {
reasoning.map(|s| s.to_string())
} else {
None
};
last_output = if let Some(ref lang) = self.config.extract_lang {
use crate::recursive::rewrite::extract_code;
let extracted = extract_code(&answer, lang)
.or_else(|| extract_code(&output.text, lang))
.map(|s| s.to_string())
.unwrap_or(answer.clone());
shared::transform_output(&extracted, None, self.config.defaults.as_ref())
} else {
shared::transform_output(&answer, None, self.config.defaults.as_ref())
};
last_score = self.validator.validate(&last_output);
if last_score.value >= self.config.target {
break;
}
}
ReasonResult {
output: last_output,
reasoning: last_reasoning,
score: last_score.value,
iterations,
tokens: total_tokens,
error: None,
}
}
fn build_prompt(&self, feedback: Option<&str>) -> String {
let mut prompt = String::new();
if let Some(ref skill_text) = self.config.skill_text {
prompt.push_str(skill_text);
prompt.push('\n');
}
prompt.push_str(self.prompt);
if self.config.include_reasoning {
let instruction = self
.config
.instruction
.unwrap_or("Let's think step by step.");
prompt.push_str(&format!("\n\n{}", instruction));
}
if let Some(fb) = feedback {
prompt.push_str(&format!(
"\n\nPrevious attempt was incorrect. Feedback: {}\n\nPlease try again.",
fb
));
}
if self.config.include_reasoning {
prompt.push_str("\n\nAfter your reasoning, provide the final answer after \"Therefore:\" or \"Answer:\".");
}
prompt
}
fn parse_response<'b>(&self, response: &'b str) -> (Option<&'b str>, String) {
if !self.config.include_reasoning {
return (None, response.trim().to_string());
}
let answer_markers = [
"Therefore:",
"Answer:",
"Final Answer:",
"So the answer is:",
"Result:",
];
for marker in &answer_markers {
if let Some(idx) = response.find(marker) {
let reasoning = response[..idx].trim();
let answer_start = idx + marker.len();
let answer = response[answer_start..].trim().to_string();
return (
if reasoning.is_empty() {
None
} else {
Some(reasoning)
},
answer,
);
}
}
(None, response.trim().to_string())
}
}
#[derive(Debug, Clone)]
pub struct ReasonResult {
pub output: String,
pub reasoning: Option<String>,
pub score: f64,
pub iterations: u32,
pub tokens: u32,
pub error: Option<String>,
}
impl ReasonResult {
pub fn reasoning(&self) -> &str {
self.reasoning.as_deref().unwrap_or("")
}
pub fn success(&self) -> bool {
self.error.is_none() && self.score >= 1.0
}
pub fn is_err(&self) -> bool {
self.error.is_some()
}
}
impl std::fmt::Display for ReasonResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Reason({} iters, score={:.2}, tokens={})",
self.iterations, self.score, self.tokens
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recursive::checks::checks;
use crate::recursive::defaults::Defaults;
use crate::recursive::llm::MockLlm;
use crate::recursive::skill::Skill;
#[test]
fn test_reason_basic() {
let llm = MockLlm::new(|_, _| {
"Step 1: Calculate 25 * 30 = 750\n\
Step 2: Calculate 25 * 7 = 175\n\
Step 3: Add them: 750 + 175 = 925\n\n\
Therefore: 925"
.to_string()
});
let result = reason(&llm, "What is 25 * 37?").go();
assert!(result.output.contains("925"));
assert!(result.reasoning().contains("Step 1"));
assert_eq!(result.iterations, 1);
}
#[test]
fn test_reason_with_validation() {
let llm = MockLlm::new(|_, _| "Let me think...\n\nAnswer: 42".to_string());
let result = reason(&llm, "Calculate something")
.validate(checks().regex(r"\d+"))
.go();
assert!(result.score >= 1.0);
assert_eq!(result.output, "42");
}
#[test]
fn test_reason_parse_response() {
let llm = MockLlm::new(|_, _| String::new());
let builder = reason(&llm, "test");
let (reasoning, answer) = builder.parse_response(
"First, I'll analyze the problem.\nThen, I'll solve it.\nTherefore: 42",
);
assert!(reasoning.is_some());
assert!(reasoning.unwrap().contains("analyze"));
assert_eq!(answer, "42");
let (reasoning, answer) =
builder.parse_response("Step 1: Do X\nStep 2: Do Y\nAnswer: The result is Z");
assert!(reasoning.is_some());
assert_eq!(answer, "The result is Z");
}
#[test]
fn test_reason_custom_instruction() {
let llm = MockLlm::new(|prompt, _| {
if prompt.contains("Break this down") {
"Breakdown: ... Answer: correct".to_string()
} else {
"Wrong instruction".to_string()
}
});
let result = reason(&llm, "Solve X")
.instruction("Break this down into parts.")
.go();
assert!(result.output.contains("correct"));
}
#[test]
fn test_reason_no_reasoning() {
let llm = MockLlm::new(|_, _| "42".to_string());
let result = reason(&llm, "Test").no_reasoning().go();
assert!(result.reasoning.is_none());
assert_eq!(result.output, "42");
}
#[test]
fn test_reason_no_reasoning_multiline() {
let llm =
MockLlm::new(|_, _| "name: test\nruntime: yaml\nresources:\n foo: bar".to_string());
let result = reason(&llm, "Generate YAML").no_reasoning().go();
assert!(result.reasoning.is_none());
assert!(result.output.contains("name: test"));
assert!(result.output.contains("resources:"));
assert!(result.output.contains("foo: bar"));
}
#[test]
fn test_reason_config() {
let llm = MockLlm::new(|_, _| String::new());
let builder = reason(&llm, "test")
.reasoning_field("thought")
.max_iter(10)
.target(0.8);
assert_eq!(builder.config.reasoning_field, "thought");
assert_eq!(builder.config.max_iter, 10);
assert!((builder.config.target - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_reason_extract_applies_to_output() {
let llm = MockLlm::new(|_, _| {
"I need to write hello world.\n\
Here is the code:\n\
```python\n\
print(\"hello\")\n\
```\n\n\
Therefore: The code above prints hello"
.to_string()
});
let result = reason(&llm, "Write hello world in Python")
.extract("python")
.go();
assert_eq!(result.output.trim(), "print(\"hello\")");
assert!(!result.output.contains("```"));
assert!(!result.output.contains("The code above"));
}
#[test]
fn test_reason_multiline_no_marker_preserves_full_output() {
let llm = MockLlm::new(|_, _| "Line 1\nLine 2\nLine 3\nLine 4".to_string());
let result = reason(&llm, "Generate multi-line content").go();
assert_eq!(result.output, "Line 1\nLine 2\nLine 3\nLine 4");
assert_eq!(result.output.lines().count(), 4);
assert!(result.output.contains("Line 1"));
assert!(result.output.contains("Line 4"));
assert!(result.reasoning.is_none());
}
#[test]
fn test_reason_with_marker_still_extracts() {
let llm = MockLlm::new(|_, _| "Step 1\nStep 2\nStep 3\nTherefore: 42".to_string());
let result = reason(&llm, "Solve problem").go();
assert_eq!(result.output, "42");
assert!(result.reasoning().contains("Step 1"));
assert!(result.reasoning().contains("Step 3"));
}
#[test]
fn test_reason_multiline_yaml_no_marker() {
let llm =
MockLlm::new(|_, _| "name: template\ntype: yaml\nconfig:\n key: value".to_string());
let result = reason(&llm, "Generate YAML")
.validate(checks().require("name:").min_len(20))
.go();
assert!(result.output.contains("type: yaml"));
assert!(result.output.contains("config:"));
assert!(result.output.contains("key: value"));
assert_eq!(result.score, 1.0);
}
#[test]
fn test_reason_yaml_template_validation() {
let llm = MockLlm::new(|_, _| {
"name: test\nruntime: yaml\n\nresources:\n bucket:\n type: storage.v1.bucket\n properties:\n name: test-bucket".to_string()
});
let result = reason(&llm, "Generate deployment YAML")
.validate(checks().min_len(50))
.go();
assert!(result.output.contains("resources:"));
assert!(result.output.contains("bucket:"));
assert!(result.output.lines().count() > 5);
assert_eq!(result.score, 1.0);
}
#[test]
fn test_reason_single_line_no_marker_unchanged() {
let llm = MockLlm::new(|_, _| "Simple answer".to_string());
let result = reason(&llm, "Question").go();
assert_eq!(result.output, "Simple answer");
assert!(result.reasoning.is_none());
}
#[test]
fn test_reason_with_defaults() {
let llm = MockLlm::new(|_, _| "user:admin@example.com".to_string());
let defaults = Defaults::new().set("email", r"admin@example\.com", "real@company.com");
let result = reason(&llm, "Generate IAM")
.defaults(defaults)
.validate(checks().require("real@company.com"))
.go();
assert!(result.success());
assert!(result.output.contains("real@company.com"));
assert!(!result.output.contains("admin@example.com"));
}
#[test]
fn test_reason_with_defaults_multiple_patterns() {
let llm = MockLlm::new(|_, _| "user:admin@example.com in my-gcp-project".to_string());
let defaults = Defaults::new()
.set("email", r"admin@example\.com", "real@company.com")
.set("project", r"my-gcp-project", "prod-123");
let result = reason(&llm, "Generate config")
.defaults(defaults)
.validate(checks().require("real@company.com").require("prod-123"))
.go();
assert!(result.success());
assert!(result.output.contains("real@company.com"));
assert!(result.output.contains("prod-123"));
}
#[test]
fn test_reason_with_defaults_no_match() {
let llm = MockLlm::new(|_, _| "no placeholders here".to_string());
let defaults = Defaults::new().set("email", r"admin@example\.com", "real@company.com");
let result = reason(&llm, "Generate text").defaults(defaults).go();
assert_eq!(result.output, "no placeholders here");
}
#[test]
fn test_reason_with_skill() {
let llm = MockLlm::new(|prompt, _| {
if prompt.contains("deletionProtection") {
"Answer: skill applied".to_string()
} else {
"Answer: no skill".to_string()
}
});
let skill = Skill::new().instruct(
"deletionProtection",
"Always set deletionProtection: false.",
);
let result = reason(&llm, "Generate config").skill(&skill).go();
assert_eq!(result.output, "skill applied");
}
#[test]
fn test_reason_with_skill_empty_noop() {
let llm = MockLlm::new(|prompt, _| {
if prompt.starts_with("Generate") {
"Answer: ok".to_string()
} else {
"Answer: unexpected prefix".to_string()
}
});
let skill = Skill::new();
let result = reason(&llm, "Generate config").skill(&skill).go();
assert_eq!(result.output, "ok");
}
}