mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 14:44:21 +01:00
Merge branch 'goose-api' into codex/parse-extensions-and-update-initialization
This commit is contained in:
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
goose = { path = "../goose" }
|
||||
goose-mcp = { path = "../goose-mcp" }
|
||||
mcp-client = { path = "../mcp-client" }
|
||||
mcp-core = { path = "../mcp-core" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
warp = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -18,4 +19,6 @@ config = "0.13"
|
||||
jsonwebtoken = "8"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
# For session IDs
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
# Add dynamic-library for extension loading
|
||||
|
||||
@@ -165,7 +165,56 @@ By default, the server runs on `127.0.0.1:8080`. You can modify this using confi
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get Provider Configuration
|
||||
### 4. Add Extension
|
||||
|
||||
**Endpoint**: `POST /extensions/add`
|
||||
|
||||
**Description**: Installs or enables an extension.
|
||||
|
||||
**Request**:
|
||||
- Headers:
|
||||
- Content-Type: application/json
|
||||
- x-api-key: [your-api-key]
|
||||
- Body (example):
|
||||
```json
|
||||
{
|
||||
"type": "builtin",
|
||||
"name": "mcp_say"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"error": false,
|
||||
"message": null
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Remove Extension
|
||||
|
||||
**Endpoint**: `POST /extensions/remove`
|
||||
|
||||
**Description**: Removes or disables an extension by name.
|
||||
|
||||
**Request**:
|
||||
- Headers:
|
||||
- Content-Type: application/json
|
||||
- x-api-key: [your-api-key]
|
||||
- Body:
|
||||
```json
|
||||
"mcp_say"
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"error": false,
|
||||
"message": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Get Provider Configuration
|
||||
|
||||
**Endpoint**: `GET /provider/config`
|
||||
|
||||
@@ -204,6 +253,18 @@ curl -X POST http://localhost:8080/session/reply \
|
||||
curl -X GET http://localhost:8080/extensions/list \
|
||||
-H "x-api-key: your_secure_api_key"
|
||||
|
||||
# Add an extension
|
||||
curl -X POST http://localhost:8080/extensions/add \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: your_secure_api_key" \
|
||||
-d '{"type": "builtin", "name": "mcp_say"}'
|
||||
|
||||
# Remove an extension
|
||||
curl -X POST http://localhost:8080/extensions/remove \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: your_secure_api_key" \
|
||||
-d '"mcp_say"'
|
||||
|
||||
# Get provider configuration
|
||||
curl -X GET http://localhost:8080/provider/config \
|
||||
-H "x-api-key: your_secure_api_key"
|
||||
@@ -241,6 +302,22 @@ print(response.json())
|
||||
response = requests.get(f"{API_URL}/extensions/list", headers=HEADERS)
|
||||
print(response.json())
|
||||
|
||||
# Add an extension
|
||||
response = requests.post(
|
||||
f"{API_URL}/extensions/add",
|
||||
headers=HEADERS,
|
||||
json={"type": "builtin", "name": "mcp_say"}
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
# Remove an extension
|
||||
response = requests.post(
|
||||
f"{API_URL}/extensions/remove",
|
||||
headers=HEADERS,
|
||||
json="mcp_say"
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
# Get provider configuration
|
||||
response = requests.get(f"{API_URL}/provider/config", headers=HEADERS)
|
||||
print(response.json())
|
||||
|
||||
@@ -2,8 +2,17 @@ use warp::{Filter, Rejection};
|
||||
use warp::http::HeaderValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::LazyLock;
|
||||
use goose::agents::{Agent, ExtensionConfig, extension_manager::ExtensionManager};
|
||||
use goose::config::{Config, ExtensionEntry};
|
||||
use goose::agents::{
|
||||
extension::Envs,
|
||||
Agent,
|
||||
extension_manager::ExtensionManager,
|
||||
ExtensionConfig,
|
||||
};
|
||||
use mcp_core::tool::Tool;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use goose::providers::{create, providers};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::message::Message;
|
||||
@@ -20,6 +29,11 @@ static AGENT: LazyLock<tokio::sync::Mutex<Agent>> = LazyLock::new(|| {
|
||||
tokio::sync::Mutex::new(Agent::new())
|
||||
});
|
||||
|
||||
// Global store for session histories
|
||||
static SESSION_HISTORY: LazyLock<tokio::sync::Mutex<HashMap<Uuid, Vec<Message>>>> = LazyLock::new(|| {
|
||||
tokio::sync::Mutex::new(HashMap::new())
|
||||
});
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SessionRequest {
|
||||
prompt: String,
|
||||
@@ -31,6 +45,24 @@ struct ApiResponse {
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StartSessionResponse {
|
||||
message: String,
|
||||
status: String,
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SessionReplyRequest {
|
||||
session_id: Uuid,
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct EndSessionRequest {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ExtensionsResponse {
|
||||
extensions: Vec<String>,
|
||||
@@ -42,37 +74,93 @@ struct ProviderConfig {
|
||||
model: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ExtensionResponse {
|
||||
error: bool,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum ExtensionConfigRequest {
|
||||
#[serde(rename = "sse")]
|
||||
Sse {
|
||||
name: String,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
envs: Envs,
|
||||
#[serde(default)]
|
||||
env_keys: Vec<String>,
|
||||
timeout: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "stdio")]
|
||||
Stdio {
|
||||
name: String,
|
||||
cmd: String,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
envs: Envs,
|
||||
#[serde(default)]
|
||||
env_keys: Vec<String>,
|
||||
timeout: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "builtin")]
|
||||
Builtin {
|
||||
name: String,
|
||||
display_name: Option<String>,
|
||||
timeout: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "frontend")]
|
||||
Frontend {
|
||||
name: String,
|
||||
tools: Vec<Tool>,
|
||||
instructions: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
async fn start_session_handler(
|
||||
req: SessionRequest,
|
||||
_api_key: String,
|
||||
) -> Result<impl warp::Reply, Rejection> {
|
||||
info!("Starting session with prompt: {}", req.prompt);
|
||||
|
||||
let agent = AGENT.lock().await;
|
||||
|
||||
|
||||
let mut agent = AGENT.lock().await;
|
||||
|
||||
// Create a user message with the prompt
|
||||
let messages = vec![Message::user().with_text(&req.prompt)];
|
||||
|
||||
// Process the messages through the agent
|
||||
let mut messages = vec![Message::user().with_text(&req.prompt)];
|
||||
|
||||
// Generate a new session ID and process the messages
|
||||
let session_id = Uuid::new_v4();
|
||||
|
||||
let result = agent.reply(&messages, None).await;
|
||||
|
||||
|
||||
match result {
|
||||
Ok(mut stream) => {
|
||||
// Process the stream to get the first response
|
||||
if let Ok(Some(response)) = stream.try_next().await {
|
||||
let response_text = response.as_concat_text();
|
||||
let api_response = ApiResponse {
|
||||
message: format!("Session started with prompt: {}. Response: {}", req.prompt, response_text),
|
||||
messages.push(response);
|
||||
let mut history = SESSION_HISTORY.lock().await;
|
||||
history.insert(session_id, messages);
|
||||
|
||||
let api_response = StartSessionResponse {
|
||||
message: response_text,
|
||||
status: "success".to_string(),
|
||||
session_id,
|
||||
};
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&api_response),
|
||||
warp::http::StatusCode::OK,
|
||||
))
|
||||
} else {
|
||||
let api_response = ApiResponse {
|
||||
message: format!("Session started but no response generated"),
|
||||
let mut history = SESSION_HISTORY.lock().await;
|
||||
history.insert(session_id, messages);
|
||||
|
||||
let api_response = StartSessionResponse {
|
||||
message: "Session started but no response generated".to_string(),
|
||||
status: "warning".to_string(),
|
||||
session_id,
|
||||
};
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&api_response),
|
||||
@@ -95,16 +183,33 @@ async fn start_session_handler(
|
||||
}
|
||||
|
||||
async fn reply_session_handler(
|
||||
req: SessionRequest,
|
||||
req: SessionReplyRequest,
|
||||
_api_key: String,
|
||||
) -> Result<impl warp::Reply, Rejection> {
|
||||
info!("Replying to session with prompt: {}", req.prompt);
|
||||
|
||||
let agent = AGENT.lock().await;
|
||||
|
||||
// Create a user message with the prompt
|
||||
let messages = vec![Message::user().with_text(&req.prompt)];
|
||||
|
||||
|
||||
let mut agent = AGENT.lock().await;
|
||||
|
||||
// Retrieve existing session history
|
||||
let mut history = SESSION_HISTORY.lock().await;
|
||||
let entry = match history.get_mut(&req.session_id) {
|
||||
Some(messages) => messages,
|
||||
None => {
|
||||
let response = ApiResponse {
|
||||
message: "Session not found".to_string(),
|
||||
status: "error".to_string(),
|
||||
};
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&response),
|
||||
warp::http::StatusCode::NOT_FOUND,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Append the new user message
|
||||
entry.push(Message::user().with_text(&req.prompt));
|
||||
let messages = entry.clone();
|
||||
|
||||
// Process the messages through the agent
|
||||
let result = agent.reply(&messages, None).await;
|
||||
|
||||
@@ -113,6 +218,8 @@ async fn reply_session_handler(
|
||||
// Process the stream to get the first response
|
||||
if let Ok(Some(response)) = stream.try_next().await {
|
||||
let response_text = response.as_concat_text();
|
||||
// store assistant response in history
|
||||
entry.push(response);
|
||||
let api_response = ApiResponse {
|
||||
message: format!("Reply: {}", response_text),
|
||||
status: "success".to_string(),
|
||||
@@ -123,7 +230,7 @@ async fn reply_session_handler(
|
||||
))
|
||||
} else {
|
||||
let api_response = ApiResponse {
|
||||
message: format!("Reply processed but no response generated"),
|
||||
message: "Reply processed but no response generated".to_string(),
|
||||
status: "warning".to_string(),
|
||||
};
|
||||
Ok(warp::reply::with_status(
|
||||
@@ -146,6 +253,32 @@ async fn reply_session_handler(
|
||||
}
|
||||
}
|
||||
|
||||
async fn end_session_handler(
|
||||
req: EndSessionRequest,
|
||||
_api_key: String,
|
||||
) -> Result<impl warp::Reply, Rejection> {
|
||||
let mut history = SESSION_HISTORY.lock().await;
|
||||
if history.remove(&req.session_id).is_some() {
|
||||
let response = ApiResponse {
|
||||
message: "Session ended".to_string(),
|
||||
status: "success".to_string(),
|
||||
};
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&response),
|
||||
warp::http::StatusCode::OK,
|
||||
))
|
||||
} else {
|
||||
let response = ApiResponse {
|
||||
message: "Session not found".to_string(),
|
||||
status: "error".to_string(),
|
||||
};
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&response),
|
||||
warp::http::StatusCode::NOT_FOUND,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_extensions_handler() -> Result<impl warp::Reply, Rejection> {
|
||||
info!("Listing extensions");
|
||||
|
||||
@@ -177,6 +310,128 @@ async fn get_provider_config_handler() -> Result<impl warp::Reply, Rejection> {
|
||||
Ok::<warp::reply::Json, warp::Rejection>(warp::reply::json(&response))
|
||||
}
|
||||
|
||||
async fn add_extension_handler(
|
||||
req: ExtensionConfigRequest,
|
||||
_api_key: String,
|
||||
) -> Result<impl warp::Reply, Rejection> {
|
||||
info!("Adding extension: {:?}", req);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let ExtensionConfigRequest::Stdio { cmd, .. } = &req {
|
||||
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_else(|| warp::reject())?;
|
||||
|
||||
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(|_| warp::reject())?;
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!(
|
||||
"Failed to install Node.js: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let resp = ExtensionResponse {
|
||||
error: true,
|
||||
message: Some(format!(
|
||||
"Failed to install Node.js: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)),
|
||||
};
|
||||
return Ok(warp::reply::json(&resp));
|
||||
}
|
||||
eprintln!("Node.js installation completed");
|
||||
} else {
|
||||
eprintln!(
|
||||
"Node.js installer script not found at: {}",
|
||||
install_script.display()
|
||||
);
|
||||
let resp = ExtensionResponse {
|
||||
error: true,
|
||||
message: Some("Node.js installer script not found".to_string()),
|
||||
};
|
||||
return Ok(warp::reply::json(&resp));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let extension = match req {
|
||||
ExtensionConfigRequest::Sse { name, uri, envs, env_keys, timeout } => {
|
||||
ExtensionConfig::Sse {
|
||||
name,
|
||||
uri,
|
||||
envs,
|
||||
env_keys,
|
||||
description: None,
|
||||
timeout,
|
||||
bundled: None,
|
||||
}
|
||||
}
|
||||
ExtensionConfigRequest::Stdio { name, cmd, args, envs, env_keys, timeout } => {
|
||||
ExtensionConfig::Stdio {
|
||||
name,
|
||||
cmd,
|
||||
args,
|
||||
envs,
|
||||
env_keys,
|
||||
timeout,
|
||||
description: None,
|
||||
bundled: None,
|
||||
}
|
||||
}
|
||||
ExtensionConfigRequest::Builtin { name, display_name, timeout } => {
|
||||
ExtensionConfig::Builtin {
|
||||
name,
|
||||
display_name,
|
||||
timeout,
|
||||
bundled: None,
|
||||
}
|
||||
}
|
||||
ExtensionConfigRequest::Frontend { name, tools, instructions } => {
|
||||
ExtensionConfig::Frontend {
|
||||
name,
|
||||
tools,
|
||||
instructions,
|
||||
bundled: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let agent = AGENT.lock().await;
|
||||
let result = agent.add_extension(extension).await;
|
||||
|
||||
let resp = match result {
|
||||
Ok(_) => ExtensionResponse { error: false, message: None },
|
||||
Err(e) => ExtensionResponse {
|
||||
error: true,
|
||||
message: Some(format!("Failed to add extension configuration, error: {:?}", e)),
|
||||
},
|
||||
};
|
||||
Ok(warp::reply::json(&resp))
|
||||
}
|
||||
|
||||
async fn remove_extension_handler(
|
||||
name: String,
|
||||
_api_key: String,
|
||||
) -> Result<impl warp::Reply, Rejection> {
|
||||
info!("Removing extension: {}", name);
|
||||
let agent = AGENT.lock().await;
|
||||
agent.remove_extension(&name).await;
|
||||
|
||||
let resp = ExtensionResponse { error: false, message: None };
|
||||
Ok(warp::reply::json(&resp))
|
||||
}
|
||||
|
||||
fn with_api_key(api_key: String) -> impl Filter<Extract = (String,), Error = Rejection> + Clone {
|
||||
warp::header::value("x-api-key")
|
||||
.and_then(move |header_api_key: HeaderValue| {
|
||||
@@ -334,19 +589,43 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
.and(with_api_key(api_key.clone()))
|
||||
.and_then(start_session_handler);
|
||||
|
||||
// Session reply endpoint
|
||||
// Session reply endpoint
|
||||
let reply_session = warp::path("session")
|
||||
.and(warp::path("reply"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_api_key(api_key.clone()))
|
||||
.and_then(reply_session_handler);
|
||||
|
||||
// Session end endpoint
|
||||
let end_session = warp::path("session")
|
||||
.and(warp::path("end"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_api_key(api_key.clone()))
|
||||
.and_then(end_session_handler);
|
||||
|
||||
// List extensions endpoint
|
||||
let list_extensions = warp::path("extensions")
|
||||
.and(warp::path("list"))
|
||||
.and(warp::get())
|
||||
.and_then(list_extensions_handler);
|
||||
|
||||
// Add extension endpoint
|
||||
let add_extension = warp::path("extensions")
|
||||
.and(warp::path("add"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_api_key(api_key.clone()))
|
||||
.and_then(add_extension_handler);
|
||||
|
||||
// Remove extension endpoint
|
||||
let remove_extension = warp::path("extensions")
|
||||
.and(warp::path("remove"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_api_key(api_key.clone()))
|
||||
.and_then(remove_extension_handler);
|
||||
|
||||
// Get provider configuration endpoint
|
||||
let get_provider_config = warp::path("provider")
|
||||
@@ -357,7 +636,10 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
// Combine all routes
|
||||
let routes = start_session
|
||||
.or(reply_session)
|
||||
.or(end_session)
|
||||
.or(list_extensions)
|
||||
.or(add_extension)
|
||||
.or(remove_extension)
|
||||
.or(get_provider_config);
|
||||
|
||||
// Get bind address from configuration or use default
|
||||
|
||||
Reference in New Issue
Block a user