use perspective_js::utils::ApiError;
use serde_json::json;
use super::protocol::{ChatMessage, ChatRequest};
use super::transport::{AgentTransport, OnDelta};
use crate::agent::config::SystemRole;
use crate::agent::tools::{ToolCtx, tool_definitions, tool_dispatch};
use crate::custom_elements::viewer::PerspectiveViewerElement;
pub enum TurnError {
Budget { max_turns: usize },
Api(ApiError),
}
impl From<ApiError> for TurnError {
fn from(err: ApiError) -> Self {
Self::Api(err)
}
}
impl From<TurnError> for ApiError {
fn from(err: TurnError) -> Self {
match err {
TurnError::Budget { max_turns } => ApiError::from(format!(
"Turn budget exhausted after {max_turns} model requests (see `maxTurns`)"
)),
TurnError::Api(err) => err,
}
}
}
const BUDGET_NUDGE: &str = "This is the final model request of this turn's budget. Do not call \
more tools - answer now with a short summary of what was done and \
what, if anything, remains.";
#[allow(clippy::too_many_arguments)]
pub async fn run_turn(
transport: &AgentTransport,
model: &str,
max_turns: usize,
elem: &PerspectiveViewerElement,
ctx: &ToolCtx,
messages: &mut Vec<ChatMessage>,
system_role: SystemRole,
on_delta: OnDelta<'_>,
) -> Result<String, TurnError> {
let defs = tool_definitions(ctx);
for turn in 0..max_turns {
on_delta("", "");
let nudged;
let request_messages: &[ChatMessage] = if turn + 1 == max_turns && max_turns > 1 {
let mut with_nudge = messages.clone();
with_nudge.push(match system_role {
SystemRole::System => ChatMessage::System {
content: BUDGET_NUDGE.to_owned(),
},
SystemRole::User => ChatMessage::User {
content: BUDGET_NUDGE.to_owned(),
},
});
nudged = with_nudge;
&nudged
} else {
messages
};
let request = ChatRequest {
model,
messages: request_messages,
tools: &defs,
stream: true,
};
let message = transport.create(&request, on_delta).await?;
let ChatMessage::Assistant {
content,
tool_calls,
reasoning_content,
} = message
else {
return Err(
ApiError::from("Completion response message role is not `assistant`").into(),
);
};
if tool_calls.is_empty() {
let text = content.unwrap_or_default();
on_delta(&text, reasoning_content.as_deref().unwrap_or_default());
messages.push(ChatMessage::Assistant {
content: Some(text.clone()),
tool_calls: vec![],
reasoning_content,
});
return Ok(text);
}
messages.push(ChatMessage::Assistant {
content,
tool_calls: tool_calls.clone(),
reasoning_content,
});
for call in tool_calls {
let content =
match tool_dispatch(elem, ctx, &call.function.name, &call.function.arguments).await
{
Ok(value) => value.to_string(),
Err(err) => json!({ "error": format!("{err}") }).to_string(),
};
messages.push(ChatMessage::Tool {
tool_call_id: call.id,
content,
});
}
}
Err(TurnError::Budget { max_turns })
}