204 lines
6.3 KiB
Rust
204 lines
6.3 KiB
Rust
use rig::agent::AgentBuilder;
|
|
use rig::client::CompletionClient;
|
|
use rig::completion::Prompt;
|
|
use tracing::{debug, warn};
|
|
|
|
use super::config::AgentConfig;
|
|
use super::helpers::with_retry;
|
|
use super::persistence::{
|
|
ActiveAgentRun, AgentRealtime, AgentRuntime, AgentStreamEvent,
|
|
estimate_output_tokens,
|
|
};
|
|
use super::request::{AgentExpert, AgentExpertOutput};
|
|
use crate::client::AiClient;
|
|
use crate::error::{AiError, AiResult};
|
|
|
|
pub async fn run_experts(
|
|
client: &AiClient,
|
|
config: &AgentConfig,
|
|
experts: &[AgentExpert],
|
|
realtime: Option<&AgentRealtime>,
|
|
run: &ActiveAgentRun,
|
|
) -> AiResult<Vec<AgentExpertOutput>> {
|
|
let mut outputs = Vec::with_capacity(experts.len());
|
|
let mut failed_count = 0;
|
|
|
|
for expert in experts {
|
|
match run_single(client, config, expert, realtime, run).await {
|
|
Ok(output) => {
|
|
debug!(subagent_id = %output.id, role = %output.role, "subagent completed");
|
|
outputs.push(output);
|
|
}
|
|
Err(error) => {
|
|
warn!(subagent_id = %expert.id, role = %expert.role, error = %error, "subagent failed");
|
|
let _ = publish_subagent_failed(realtime, run, expert, &error.to_string()).await;
|
|
failed_count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
debug!(total = experts.len(), ok = outputs.len(), failed = failed_count, "experts done");
|
|
Ok(outputs)
|
|
}
|
|
|
|
async fn run_single(
|
|
client: &AiClient,
|
|
config: &AgentConfig,
|
|
expert: &AgentExpert,
|
|
realtime: Option<&AgentRealtime>,
|
|
run: &ActiveAgentRun,
|
|
) -> AiResult<AgentExpertOutput> {
|
|
publish_subagent_started(realtime, run, config, expert).await?;
|
|
|
|
let rig_client = client.llm_client().clone();
|
|
let model_name = config.model.clone();
|
|
let temperature = expert.temperature.or(config.temperature);
|
|
let max_completion_tokens = expert.max_completion_tokens.or(config.max_completion_tokens);
|
|
let retry_attempts = config.retry_max_attempts;
|
|
let retry_delay_ms = config.retry_base_delay_ms;
|
|
|
|
let prompt = expert.system_prompt.clone().unwrap_or_else(|| {
|
|
format!(
|
|
"You are a specialist subagent. Role: {}. Produce a concise expert answer for the parent chat agent.",
|
|
expert.role
|
|
)
|
|
});
|
|
|
|
let task = build_expert_task(expert);
|
|
|
|
let (output, input_tokens_usage, output_tokens_usage) = with_retry(
|
|
retry_attempts,
|
|
retry_delay_ms,
|
|
|| {
|
|
let rig_client = rig_client.clone();
|
|
let model_name = model_name.clone();
|
|
let prompt = prompt.clone();
|
|
let task = task.clone();
|
|
async move {
|
|
let model = rig_client.completion_model(&model_name);
|
|
let mut builder = AgentBuilder::new(model).preamble(&prompt);
|
|
if let Some(temp) = temperature {
|
|
builder = builder.temperature(temp);
|
|
}
|
|
if let Some(mt) = max_completion_tokens {
|
|
builder = builder.max_tokens(mt);
|
|
}
|
|
let agent = builder.build();
|
|
|
|
let response = agent
|
|
.prompt(&task)
|
|
.extended_details()
|
|
.await
|
|
.map_err(|e: rig::completion::PromptError| {
|
|
AiError::Api(e.to_string())
|
|
})?;
|
|
|
|
Ok((
|
|
response.output,
|
|
response.usage.input_tokens,
|
|
response.usage.output_tokens,
|
|
))
|
|
}
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
let input_tokens = input_tokens_usage as i64;
|
|
let output_tokens = if output_tokens_usage > 0 {
|
|
output_tokens_usage as i64
|
|
} else {
|
|
estimate_output_tokens(&output)
|
|
};
|
|
|
|
let result = AgentExpertOutput {
|
|
id: expert.id.clone(),
|
|
role: expert.role.clone(),
|
|
task: expert.task.clone(),
|
|
output,
|
|
input_tokens,
|
|
output_tokens,
|
|
};
|
|
|
|
publish_subagent_completed(realtime, run, config, &result).await?;
|
|
Ok(result)
|
|
}
|
|
|
|
fn build_expert_task(expert: &AgentExpert) -> String {
|
|
let mut task = String::new();
|
|
|
|
if !expert.context.is_empty() {
|
|
task.push_str("Retrieved context for this specialist task:\n");
|
|
for (index, chunk) in expert.context.iter().enumerate() {
|
|
task.push_str(&format!(
|
|
"\n[{}] id={} source={}\n{}\n",
|
|
index + 1,
|
|
chunk.id,
|
|
chunk.source.as_deref().unwrap_or("unknown"),
|
|
chunk.content
|
|
));
|
|
}
|
|
task.push('\n');
|
|
}
|
|
|
|
task.push_str(&expert.task);
|
|
task
|
|
}
|
|
|
|
async fn publish_subagent_started(
|
|
realtime: Option<&AgentRealtime>,
|
|
run: &ActiveAgentRun,
|
|
config: &AgentConfig,
|
|
expert: &AgentExpert,
|
|
) -> AiResult<()> {
|
|
AgentRuntime::default().publish(
|
|
realtime,
|
|
&AgentStreamEvent::SubagentStarted {
|
|
conversation_id: run.conversation_id,
|
|
message_id: run.message_id,
|
|
subagent_id: expert.id.clone(),
|
|
role: expert.role.clone(),
|
|
task: expert.task.clone(),
|
|
model: config.model.clone(),
|
|
},
|
|
).await
|
|
}
|
|
|
|
async fn publish_subagent_completed(
|
|
realtime: Option<&AgentRealtime>,
|
|
run: &ActiveAgentRun,
|
|
config: &AgentConfig,
|
|
output: &AgentExpertOutput,
|
|
) -> AiResult<()> {
|
|
AgentRuntime::default().publish(
|
|
realtime,
|
|
&AgentStreamEvent::SubagentCompleted {
|
|
conversation_id: run.conversation_id,
|
|
message_id: run.message_id,
|
|
subagent_id: output.id.clone(),
|
|
role: output.role.clone(),
|
|
task: output.task.clone(),
|
|
output: output.output.clone(),
|
|
input_tokens: output.input_tokens,
|
|
output_tokens: output.output_tokens,
|
|
model: config.model.clone(),
|
|
},
|
|
).await
|
|
}
|
|
|
|
async fn publish_subagent_failed(
|
|
realtime: Option<&AgentRealtime>,
|
|
run: &ActiveAgentRun,
|
|
expert: &AgentExpert,
|
|
error: &str,
|
|
) -> AiResult<()> {
|
|
AgentRuntime::default().publish(
|
|
realtime,
|
|
&AgentStreamEvent::SubagentFailed {
|
|
conversation_id: run.conversation_id,
|
|
message_id: run.message_id,
|
|
subagent_id: expert.id.clone(),
|
|
error: error.to_string(),
|
|
},
|
|
).await
|
|
}
|