use std::pin::Pin;
use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt};
use crate::cli::command::{AgentArguments, CommandExecutor, CommandRequest, CommandResponse};
pub struct SseCommandExecutor {
url: String,
signature: Option<String>,
}
impl SseCommandExecutor {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
signature: None,
}
}
pub fn signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.command_executor.AuthEnvelope")]
pub struct AuthEnvelope {
pub signature: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("connect daemon execute: {0}")]
Connect(reqwest::Error),
#[error("daemon execute http {0}: {1}")]
Http(reqwest::StatusCode, String),
#[error("daemon execute sse stream: {0}")]
Sse(String),
#[error("decode daemon execute message: {0}")]
Json(serde_json::Error),
#[error("{0}")]
Cli(crate::cli::Error),
#[error("daemon execute stream produced no items")]
Empty,
}
fn error_chain(e: &dyn std::error::Error) -> String {
let mut message = e.to_string();
let mut source = e.source();
while let Some(cause) = source {
message.push_str(": ");
message.push_str(&cause.to_string());
source = cause.source();
}
message
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Line<T> {
Err(crate::cli::Error),
Ok(T),
}
impl<T> From<Line<T>> for Result<T, Error> {
fn from(line: Line<T>) -> Self {
match line {
Line::Err(e) => Err(Error::Cli(e)),
Line::Ok(t) => Ok(t),
}
}
}
impl CommandExecutor for SseCommandExecutor {
type Error = Error;
type Stream<T>
= Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>
where
T: Send + 'static;
async fn execute<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<Self::Stream<T>, Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let mut req = reqwest::Client::new()
.post(&self.url)
.header("Accept", "text/event-stream")
.json(&request);
if let Some(signature) = &self.signature {
req = req.header("X-OBJECTIVEAI-SIGNATURE", signature);
}
if let Some(args) = agent_arguments {
if let Some(v) = &args.agent_instance_hierarchy {
req = req.header("X-OBJECTIVEAI-AGENT-INSTANCE-HIERARCHY", v);
}
if let Some(v) = &args.agent_id {
req = req.header("X-OBJECTIVEAI-AGENT-ID", v);
}
if let Some(v) = &args.agent_full_id {
req = req.header("X-OBJECTIVEAI-AGENT-FULL-ID", v);
}
if let Some(v) = &args.agent_remote {
req = req.header("X-OBJECTIVEAI-AGENT-REMOTE", v);
}
if let Some(v) = &args.response_id {
req = req.header("X-OBJECTIVEAI-RESPONSE-ID", v);
}
if let Some(v) = &args.response_ids {
req = req.header("X-OBJECTIVEAI-RESPONSE-IDS", v);
}
}
let response = req.send().await.map_err(Error::Connect)?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::Http(status, body.trim().to_string()));
}
let events = response.bytes_stream().eventsource();
let stream = events.map(|event| match event {
Ok(event) => match serde_json::from_str::<Line<T>>(&event.data) {
Ok(line) => line.into(),
Err(e) => Err(Error::Json(e)),
},
Err(eventsource_stream::EventStreamError::Transport(e)) => {
Err(Error::Sse(format!("transport: {}", error_chain(&e))))
}
Err(e) => Err(Error::Sse(error_chain(&e))),
});
Ok(Box::pin(stream))
}
async fn execute_one<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<T, Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let mut stream = self.execute::<R, T>(request, agent_arguments).await?;
stream.next().await.ok_or(Error::Empty)?
}
}