Agent extension api (#5281)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -348,6 +348,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::resume_agent,
|
||||
super::routes::agent::get_tools,
|
||||
super::routes::agent::update_from_session,
|
||||
super::routes::agent::agent_add_extension,
|
||||
super::routes::agent::agent_remove_extension,
|
||||
super::routes::agent::update_agent_provider,
|
||||
super::routes::agent::update_router_tool_selector,
|
||||
super::routes::reply::confirm_permission,
|
||||
@@ -486,6 +488,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
super::routes::agent::UpdateFromSessionRequest,
|
||||
super::routes::agent::AddExtensionRequest,
|
||||
super::routes::agent::RemoveExtensionRequest,
|
||||
super::routes::setup::SetupResponse,
|
||||
))
|
||||
)]
|
||||
|
||||
@@ -11,6 +11,7 @@ use axum::{
|
||||
};
|
||||
use goose::config::PermissionManager;
|
||||
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::Config;
|
||||
use goose::model::ModelConfig;
|
||||
use goose::prompt_template::render_global_file;
|
||||
@@ -70,6 +71,18 @@ pub struct ResumeAgentRequest {
|
||||
load_model_and_extensions: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct AddExtensionRequest {
|
||||
session_id: String,
|
||||
config: ExtensionConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct RemoveExtensionRequest {
|
||||
name: String,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/start",
|
||||
@@ -451,6 +464,88 @@ async fn update_router_tool_selector(
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/add_extension",
|
||||
request_body = AddExtensionRequest,
|
||||
responses(
|
||||
(status = 200, description = "Extension added", body = String),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn agent_add_extension(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<AddExtensionRequest>,
|
||||
) -> Result<StatusCode, ErrorResponse> {
|
||||
// If this is a Stdio extension that uses npx, check for Node.js installation
|
||||
#[cfg(target_os = "windows")]
|
||||
if let ExtensionConfig::Stdio { cmd, .. } = &request.config {
|
||||
if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") {
|
||||
let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe").exists()
|
||||
|| std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists();
|
||||
|
||||
if !node_exists {
|
||||
let cmd_path = std::path::Path::new(&cmd);
|
||||
let script_dir = cmd_path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let install_script = script_dir.join("install-node.cmd");
|
||||
|
||||
if install_script.exists() {
|
||||
eprintln!("Installing Node.js...");
|
||||
let output = std::process::Command::new(&install_script)
|
||||
.arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi")
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
eprintln!("Failed to run Node.js installer: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(ErrorResponse::internal(format!(
|
||||
"Failed to install Node.js: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
eprintln!("Node.js installation completed");
|
||||
} else {
|
||||
return Err(ErrorResponse::internal(format!(
|
||||
"Node.js not detected and no installer script not found at: {}",
|
||||
install_script.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let agent = state.get_agent(request.session_id).await?;
|
||||
agent
|
||||
.add_extension(request.config)
|
||||
.await
|
||||
.map_err(|e| ErrorResponse::internal(format!("Failed to add extension: {}", e)))?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/remove_extension",
|
||||
request_body = RemoveExtensionRequest,
|
||||
responses(
|
||||
(status = 200, description = "Extension removed", body = String),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn agent_remove_extension(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<RemoveExtensionRequest>,
|
||||
) -> Result<StatusCode, ErrorResponse> {
|
||||
let agent = state.get_agent(request.session_id).await?;
|
||||
agent.remove_extension(&request.name).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/agent/start", post(start_agent))
|
||||
@@ -462,5 +557,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
post(update_router_tool_selector),
|
||||
)
|
||||
.route("/agent/update_from_session", post(update_from_session))
|
||||
.route("/agent/add_extension", post(agent_add_extension))
|
||||
.route("/agent/remove_extension", post(agent_remove_extension))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ pub struct ErrorResponse {
|
||||
pub status: StatusCode,
|
||||
}
|
||||
|
||||
impl ErrorResponse {
|
||||
pub(crate) fn internal(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ErrorResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let body = Json(serde_json::json!({
|
||||
@@ -22,3 +31,9 @@ impl IntoResponse for ErrorResponse {
|
||||
(self.status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ErrorResponse {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::internal(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::state::AppState;
|
||||
use axum::{extract::State, routing::post, Json, Router};
|
||||
use goose::agents::ExtensionConfig;
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExtensionResponse {
|
||||
error: bool,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AddExtensionRequest {
|
||||
session_id: String,
|
||||
#[serde(flatten)]
|
||||
config: ExtensionConfig,
|
||||
}
|
||||
|
||||
async fn add_extension(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<AddExtensionRequest>,
|
||||
) -> Result<Json<ExtensionResponse>, StatusCode> {
|
||||
// Log the request for debugging
|
||||
tracing::info!(
|
||||
"Received extension request for session: {}",
|
||||
request.session_id
|
||||
);
|
||||
|
||||
// If this is a Stdio extension that uses npx, check for Node.js installation
|
||||
#[cfg(target_os = "windows")]
|
||||
if let ExtensionConfig::Stdio { cmd, .. } = &request.config {
|
||||
if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") {
|
||||
// Check if Node.js is installed in standard locations
|
||||
let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe").exists()
|
||||
|| std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists();
|
||||
|
||||
if !node_exists {
|
||||
// Get the directory containing npx.cmd
|
||||
let cmd_path = std::path::Path::new(&cmd);
|
||||
let script_dir = cmd_path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// Run the Node.js installer script
|
||||
let install_script = script_dir.join("install-node.cmd");
|
||||
|
||||
if install_script.exists() {
|
||||
eprintln!("Installing Node.js...");
|
||||
let output = std::process::Command::new(&install_script)
|
||||
.arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi")
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
eprintln!("Failed to run Node.js installer: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Failed to install Node.js: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return Ok(Json(ExtensionResponse {
|
||||
error: true,
|
||||
message: Some(format!(
|
||||
"Failed to install Node.js: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)),
|
||||
}));
|
||||
}
|
||||
eprintln!("Node.js installation completed");
|
||||
} else {
|
||||
eprintln!(
|
||||
"Node.js installer script not found at: {}",
|
||||
install_script.display()
|
||||
);
|
||||
return Ok(Json(ExtensionResponse {
|
||||
error: true,
|
||||
message: Some("Node.js installer script not found".to_string()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let agent = state.get_agent_for_route(request.session_id).await?;
|
||||
let response = agent.add_extension(request.config).await;
|
||||
|
||||
// Respond with the result.
|
||||
match response {
|
||||
Ok(_) => Ok(Json(ExtensionResponse {
|
||||
error: false,
|
||||
message: None,
|
||||
})),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to add extension configuration: {:?}", e);
|
||||
Ok(Json(ExtensionResponse {
|
||||
error: true,
|
||||
message: Some(format!(
|
||||
"Failed to add extension configuration, error: {:?}",
|
||||
e
|
||||
)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RemoveExtensionRequest {
|
||||
name: String,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
/// Handler for removing an extension by name
|
||||
async fn remove_extension(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<RemoveExtensionRequest>,
|
||||
) -> Result<Json<ExtensionResponse>, StatusCode> {
|
||||
let agent = state.get_agent_for_route(request.session_id).await?;
|
||||
|
||||
match agent.remove_extension(&request.name).await {
|
||||
Ok(_) => Ok(Json(ExtensionResponse {
|
||||
error: false,
|
||||
message: None,
|
||||
})),
|
||||
Err(e) => Ok(Json(ExtensionResponse {
|
||||
error: true,
|
||||
message: Some(format!("Failed to remove extension: {:?}", e)),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/extensions/add", post(add_extension))
|
||||
.route("/extensions/remove", post(remove_extension))
|
||||
.with_state(state)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub mod agent;
|
||||
pub mod audio;
|
||||
pub mod config_management;
|
||||
pub mod errors;
|
||||
pub mod extension;
|
||||
pub mod recipe;
|
||||
pub mod recipe_utils;
|
||||
pub mod reply;
|
||||
@@ -11,6 +10,7 @@ pub mod session;
|
||||
pub mod setup;
|
||||
pub mod status;
|
||||
pub mod utils;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
@@ -22,7 +22,6 @@ pub fn configure(state: Arc<crate::state::AppState>) -> Router {
|
||||
.merge(reply::routes(state.clone()))
|
||||
.merge(agent::routes(state.clone()))
|
||||
.merge(audio::routes(state.clone()))
|
||||
.merge(extension::routes(state.clone()))
|
||||
.merge(config_management::routes(state.clone()))
|
||||
.merge(recipe::routes(state.clone()))
|
||||
.merge(session::routes(state.clone()))
|
||||
|
||||
@@ -49,7 +49,6 @@ impl AppState {
|
||||
self.agent_manager.get_or_create_agent(session_id).await
|
||||
}
|
||||
|
||||
/// Get agent for route handlers - always uses Interactive mode and converts any error to 500
|
||||
pub async fn get_agent_for_route(
|
||||
&self,
|
||||
session_id: String,
|
||||
|
||||
Reference in New Issue
Block a user