gitdataai/lib/ai/agent/subagent.rs

219 lines
6.6 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
}