use async_trait::async_trait;
use crate::types::requests::EvaluationRequest;
use crate::types::responses::ModelVote;
use crate::{TetradError, TetradResult};
#[async_trait]
pub trait CliExecutor: Send + Sync {
fn name(&self) -> &str;
fn command(&self) -> &str;
async fn is_available(&self) -> bool {
tokio::process::Command::new(self.command())
.arg("--version")
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
}
async fn version(&self) -> TetradResult<String> {
let output = tokio::process::Command::new(self.command())
.arg("--version")
.output()
.await?;
let version = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.unwrap_or("unknown")
.to_string();
Ok(version)
}
async fn evaluate(&self, request: &EvaluationRequest) -> TetradResult<ModelVote>;
fn specialization(&self) -> &str;
fn build_prompt(&self, request: &EvaluationRequest) -> String {
let eval_type = request.evaluation_type.to_string();
let language = &request.language;
let code = &request.code;
let mut prompt = format!(
"Avalie o seguinte código {} para {}.\n\n",
language, eval_type
);
prompt.push_str("Código:\n```\n");
prompt.push_str(code);
prompt.push_str("\n```\n\n");
if let Some(context) = &request.context {
prompt.push_str("Contexto adicional:\n");
prompt.push_str(context);
prompt.push_str("\n\n");
}
prompt.push_str("Responda em JSON com o formato:\n");
prompt.push_str("{\n");
prompt.push_str(" \"vote\": \"PASS\" | \"WARN\" | \"FAIL\",\n");
prompt.push_str(" \"score\": 0-100,\n");
prompt.push_str(" \"reasoning\": \"explicação\",\n");
prompt.push_str(" \"issues\": [\"issue1\", \"issue2\"],\n");
prompt.push_str(" \"suggestions\": [\"sugestão1\", \"sugestão2\"]\n");
prompt.push_str("}\n");
prompt
}
}
#[derive(Debug, serde::Deserialize)]
pub struct ExecutorResponse {
pub vote: String,
pub score: u8,
pub reasoning: String,
#[serde(default)]
pub issues: Vec<String>,
#[serde(default)]
pub suggestions: Vec<String>,
}
impl ExecutorResponse {
pub fn parse_from_output(output: &str, executor_name: &str) -> TetradResult<Self> {
let cleaned = Self::strip_code_fences(output);
if let Some(json_str) = Self::find_balanced_json(&cleaned) {
return serde_json::from_str(json_str).map_err(|e| {
TetradError::ExecutorFailed(
executor_name.to_string(),
format!("Falha ao parsear JSON: {}", e),
)
});
}
Err(TetradError::ExecutorFailed(
executor_name.to_string(),
"Resposta não contém JSON válido".to_string(),
))
}
fn strip_code_fences(input: &str) -> String {
let mut result = input.to_string();
while let Some(start) = result.find("```") {
let end_of_fence = result[start + 3..]
.find('\n')
.map(|i| start + 3 + i + 1)
.unwrap_or(start + 3);
if let Some(close) = result[end_of_fence..].find("```") {
let close_pos = end_of_fence + close;
let content = &result[end_of_fence..close_pos];
result = format!(
"{}{}{}",
&result[..start],
content,
&result[close_pos + 3..]
);
} else {
break;
}
}
result
}
fn find_balanced_json(input: &str) -> Option<&str> {
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{' {
if let Some(end) = Self::find_closing_brace(input, i) {
let candidate = &input[i..=end];
if Self::is_valid_executor_json(candidate) {
return Some(candidate);
}
}
}
i += 1;
}
None
}
fn find_closing_brace(input: &str, start: usize) -> Option<usize> {
let bytes = input.as_bytes();
let mut depth = 0;
let mut in_string = false;
let mut escape_next = false;
for (i, &byte) in bytes.iter().enumerate().skip(start) {
if escape_next {
escape_next = false;
continue;
}
match byte {
b'\\' if in_string => escape_next = true,
b'"' => in_string = !in_string,
b'{' if !in_string => depth += 1,
b'}' if !in_string => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn is_valid_executor_json(json_str: &str) -> bool {
json_str.contains("\"vote\"") && json_str.contains("\"score\"")
}
pub fn into_vote(self, executor_name: &str) -> ModelVote {
use crate::types::responses::Vote;
let vote = match self.vote.to_uppercase().as_str() {
"PASS" => Vote::Pass,
"WARN" => Vote::Warn,
_ => Vote::Fail,
};
ModelVote::new(executor_name, vote, self.score)
.with_reasoning(self.reasoning)
.with_issues(self.issues)
.with_suggestions(self.suggestions)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockExecutor;
#[async_trait]
impl CliExecutor for MockExecutor {
fn name(&self) -> &str {
"mock"
}
fn command(&self) -> &str {
"echo"
}
async fn evaluate(&self, _request: &EvaluationRequest) -> TetradResult<ModelVote> {
use crate::types::responses::Vote;
Ok(ModelVote::new("mock", Vote::Pass, 100))
}
fn specialization(&self) -> &str {
"test"
}
}
#[test]
fn test_build_prompt() {
let executor = MockExecutor;
let request = EvaluationRequest::new("fn main() {}", "rust");
let prompt = executor.build_prompt(&request);
assert!(prompt.contains("rust"));
assert!(prompt.contains("fn main() {}"));
assert!(prompt.contains("JSON"));
}
#[test]
fn test_build_prompt_with_context() {
let executor = MockExecutor;
let request =
EvaluationRequest::new("fn main() {}", "rust").with_context("Este é um teste");
let prompt = executor.build_prompt(&request);
assert!(prompt.contains("Este é um teste"));
}
#[test]
fn test_executor_response_into_vote() {
let response = ExecutorResponse {
vote: "PASS".to_string(),
score: 85,
reasoning: "Código bom".to_string(),
issues: vec![],
suggestions: vec!["Adicionar testes".to_string()],
};
let vote = response.into_vote("test");
assert_eq!(vote.executor, "test");
assert_eq!(vote.score, 85);
assert_eq!(vote.suggestions.len(), 1);
}
#[test]
fn test_parse_json_with_code_fence() {
let output = r#"
Here is my analysis:
```json
{"vote": "PASS", "score": 90, "reasoning": "Good", "issues": [], "suggestions": []}
```
That's my response.
"#;
let response = ExecutorResponse::parse_from_output(output, "Test");
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "PASS");
assert_eq!(response.score, 90);
}
#[test]
fn test_parse_json_with_multiple_braces() {
let output = r#"
The function `fn foo() { bar() }` looks good.
{"vote": "WARN", "score": 70, "reasoning": "Minor issues", "issues": ["issue1"], "suggestions": []}
End of response.
"#;
let response = ExecutorResponse::parse_from_output(output, "Test");
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "WARN");
assert_eq!(response.score, 70);
}
#[test]
fn test_parse_json_with_nested_json() {
let output = r#"
Some text with nested object: {"other": "data"}
{"vote": "FAIL", "score": 30, "reasoning": "Bad code", "issues": ["bug"], "suggestions": ["fix"]}
"#;
let response = ExecutorResponse::parse_from_output(output, "Test");
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "FAIL");
assert_eq!(response.score, 30);
}
#[test]
fn test_parse_json_direct() {
let output = r#"{"vote": "PASS", "score": 100, "reasoning": "Perfect", "issues": [], "suggestions": []}"#;
let response = ExecutorResponse::parse_from_output(output, "Test");
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "PASS");
assert_eq!(response.score, 100);
}
#[test]
fn test_parse_json_no_valid_json() {
let output = "No JSON here, just some text with { random braces }";
let response = ExecutorResponse::parse_from_output(output, "Test");
assert!(response.is_err());
}
}