use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use super::{Orchestrator, RunContext};
use crate::task::AgentTask;
use crate::AgentError;
#[derive(Debug, Clone, PartialEq)]
pub struct ReviewVerdict {
pub passed: bool,
pub feedback: String,
}
impl ReviewVerdict {
pub fn pass() -> Self {
Self {
passed: true,
feedback: String::new(),
}
}
pub fn fail(feedback: impl Into<String>) -> Self {
Self {
passed: false,
feedback: feedback.into(),
}
}
}
pub fn review_envelope(task: &AgentTask, output: &str) -> String {
serde_json::json!({
"objective": task.objective,
"expected_output": task.expected_output,
"output": output,
})
.to_string()
}
pub fn parse_review_verdict(text: &str) -> Option<ReviewVerdict> {
let text = text.trim();
if let Ok(value) = serde_json::from_str::<Value>(text) {
if let Some(passed) = value.get("passed").and_then(Value::as_bool) {
let feedback = value
.get("feedback")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
return Some(ReviewVerdict { passed, feedback });
}
}
if let Some(inner) = between(text, "<<<VERDICT>>>", "<<<END_VERDICT>>>") {
if inner.eq_ignore_ascii_case("PASS") || inner.eq_ignore_ascii_case("FAIL") {
let passed = inner.eq_ignore_ascii_case("PASS");
let feedback = between(text, "<<<FEEDBACK>>>", "<<<END_FEEDBACK>>>")
.unwrap_or("")
.to_string();
return Some(ReviewVerdict { passed, feedback });
}
}
let upper = text.to_uppercase();
if upper.starts_with("PASS") {
return Some(ReviewVerdict::pass());
}
if upper.starts_with("FAIL") {
let feedback = text
.trim_start_matches("FAIL")
.trim()
.trim_start_matches(':')
.trim()
.to_string();
return Some(ReviewVerdict::fail(feedback));
}
None
}
fn between<'a>(text: &'a str, start: &str, end: &str) -> Option<&'a str> {
let s = text.find(start)?;
let rest = &text[s + start.len()..];
let e = rest.find(end)?;
Some(rest[..e].trim())
}
pub struct ReviewOrchestrator {
worker: Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
reviewer: Arc<dyn Orchestrator<Input = String, Output = String>>,
max_attempts: usize,
fail_on_unresolved: bool,
}
impl ReviewOrchestrator {
pub fn new(
worker: Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
reviewer: Arc<dyn Orchestrator<Input = String, Output = String>>,
max_attempts: usize,
) -> Self {
Self {
worker,
reviewer,
max_attempts: max_attempts.max(1),
fail_on_unresolved: true,
}
}
pub fn with_max_attempts(mut self, max_attempts: usize) -> Self {
self.max_attempts = max_attempts.max(1);
self
}
pub fn keep_last_output(mut self) -> Self {
self.fail_on_unresolved = false;
self
}
pub fn max_attempts(&self) -> usize {
self.max_attempts
}
}
#[async_trait]
impl Orchestrator for ReviewOrchestrator {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
let mut task = input;
let mut last_output = String::new();
let mut last_feedback = String::new();
for attempt in 0..self.max_attempts {
log::debug!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator attempt {}/{} trace_id = {}",
attempt + 1,
self.max_attempts,
ctx.trace_id
);
last_output = self
.worker
.run_with_context(task.clone(), ctx)
.await
.map_err(|e| {
AgentError::Other(format!(
"ReviewOrchestrator worker (attempt {}): {e}",
attempt + 1
))
})?;
let review_text = self
.reviewer
.run_with_context(review_envelope(&task, &last_output), ctx)
.await
.map_err(|e| {
AgentError::Other(format!(
"ReviewOrchestrator reviewer (attempt {}): {e}",
attempt + 1
))
})?;
let verdict = parse_review_verdict(&review_text).ok_or_else(|| {
AgentError::Other(format!(
"ReviewOrchestrator: failed to parse review verdict: {review_text}"
))
})?;
if verdict.passed {
log::debug!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator passed on attempt {}",
attempt + 1
);
return Ok(last_output);
}
last_feedback = verdict.feedback.clone();
if attempt + 1 >= self.max_attempts {
log::warn!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator unresolved after {} attempts",
self.max_attempts
);
break;
}
let feedback_suffix = if verdict.feedback.trim().is_empty() {
"[评审未通过,请修订输出质量]".to_string()
} else {
format!("[评审未通过,请根据反馈修订: {}]", verdict.feedback.trim())
};
let mut next = AgentTask::new(format!("{}\n{}", task.objective, feedback_suffix));
if let Some(expected) = task.expected_output.clone() {
next = next.with_expected_output(expected);
}
next = next.with_allowed_tools(task.allowed_tools.clone());
task = next;
}
if self.fail_on_unresolved {
let detail = if last_feedback.trim().is_empty() {
"(none)".to_string()
} else {
last_feedback.trim().to_string()
};
Err(AgentError::Other(format!(
"ReviewOrchestrator: did not pass after {} attempts, latest feedback: {}",
self.max_attempts, detail
)))
} else {
Ok(last_output)
}
}
}