use async_trait::async_trait;
#[derive(Clone, Debug)]
pub struct ThoughtState {
pub steps: Vec<String>,
pub problem: String,
}
impl ThoughtState {
pub fn new(problem: impl Into<String>) -> Self {
Self { steps: vec![], problem: problem.into() }
}
pub fn extend(&self, thought: impl Into<String>) -> Self {
let mut steps = self.steps.clone();
steps.push(thought.into());
Self { steps, problem: self.problem.clone() }
}
pub fn chain_text(&self) -> String {
self.steps.join("\n")
}
pub fn depth(&self) -> usize {
self.steps.len()
}
}
#[derive(Clone, Debug)]
pub enum SelectionStrategy {
Greedy(usize),
Sample(usize),
}
impl SelectionStrategy {
pub fn beam_width(&self) -> usize {
match self {
SelectionStrategy::Greedy(k) => *k,
SelectionStrategy::Sample(k) => *k,
}
}
}
#[async_trait]
pub trait ThoughtGenerator: Send + Sync {
async fn generate(&self, state: &ThoughtState, n: usize) -> Vec<String>;
}
#[async_trait]
pub trait ThoughtEvaluator: Send + Sync {
async fn evaluate(&self, state: &ThoughtState, thought: &str) -> f64;
}
#[derive(Debug)]
pub struct BeamSearchResult {
pub states: Vec<ThoughtState>,
pub best_score: f64,
}
pub async fn beam_search(
generator: &dyn ThoughtGenerator,
evaluator: &dyn ThoughtEvaluator,
initial: ThoughtState,
steps: usize,
strategy: SelectionStrategy,
branch_factor: usize,
) -> BeamSearchResult {
let beam_width = strategy.beam_width();
let mut beam: Vec<(ThoughtState, f64)> = vec![(initial, 0.0)];
for _step in 0..steps {
let mut candidates: Vec<(ThoughtState, f64)> = vec![];
for (state, _prev_score) in &beam {
let thoughts = generator.generate(state, branch_factor).await;
for thought in thoughts {
let score = evaluator.evaluate(state, &thought).await;
let next_state = state.extend(&thought);
candidates.push((next_state, score));
}
}
if candidates.is_empty() {
break;
}
candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
beam = match &strategy {
SelectionStrategy::Greedy(_) => {
candidates.into_iter().take(beam_width).collect()
}
SelectionStrategy::Sample(k) => {
sample_weighted(candidates, *k)
}
};
}
let best_score = beam.first().map(|(_, s)| *s).unwrap_or(0.0);
let states = beam.into_iter().map(|(s, _)| s).collect();
BeamSearchResult { states, best_score }
}
fn sample_weighted(candidates: Vec<(ThoughtState, f64)>, k: usize) -> Vec<(ThoughtState, f64)> {
let n = candidates.len().min(k);
let total_score: f64 = candidates.iter().map(|(_, s)| s.max(0.0)).sum();
if total_score <= 0.0 {
return candidates.into_iter().take(n).collect();
}
let step = total_score / n as f64;
let mut cumulative = 0.0f64;
let mut pick_at = step * 0.5;
let mut result: Vec<(ThoughtState, f64)> = Vec::with_capacity(n);
for item in &candidates {
if result.len() >= n { break; }
cumulative += item.1.max(0.0);
if cumulative >= pick_at {
result.push(item.clone());
pick_at += step;
}
}
if result.len() < n {
for item in candidates.into_iter().take(n) {
if result.len() >= n { break; }
result.push(item);
}
}
result.truncate(n);
result
}
pub struct StaticGenerator {
pub thoughts: Vec<String>,
}
#[async_trait]
impl ThoughtGenerator for StaticGenerator {
async fn generate(&self, _state: &ThoughtState, n: usize) -> Vec<String> {
self.thoughts.iter().take(n).cloned().collect()
}
}
pub struct LengthEvaluator;
#[async_trait]
impl ThoughtEvaluator for LengthEvaluator {
async fn evaluate(&self, _state: &ThoughtState, thought: &str) -> f64 {
(thought.len() as f64 / 200.0).min(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_beam_search_basic() {
let gen = StaticGenerator {
thoughts: vec![
"Step A: analyze the problem".into(),
"Step B: gather data".into(),
"Step C: form hypothesis".into(),
],
};
let eval = LengthEvaluator;
let initial = ThoughtState::new("Solve 2+2");
let result = beam_search(&gen, &eval, initial, 2, SelectionStrategy::Greedy(2), 3).await;
assert!(!result.states.is_empty());
assert!(result.states[0].depth() <= 2);
}
#[tokio::test]
async fn test_thought_state_extend() {
let state = ThoughtState::new("problem");
let next = state.extend("thought 1");
assert_eq!(next.depth(), 1);
let next2 = next.extend("thought 2");
assert_eq!(next2.depth(), 2);
assert!(next2.chain_text().contains("thought 1"));
}
}