mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-17 14:14:26 +01:00
Merge branch 'goose-api' into main
This commit is contained in:
361
Cargo.lock
generated
361
Cargo.lock
generated
@@ -28,6 +28,17 @@ dependencies = [
|
|||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.15",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
@@ -251,7 +262,7 @@ version = "52.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16f4a9468c882dc66862cef4e1fd8423d47e67972377d85d80e022786427768c"
|
checksum = "16f4a9468c882dc66862cef4e1fd8423d47e67972377d85d80e022786427768c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
"arrow-data",
|
"arrow-data",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -382,7 +393,7 @@ version = "52.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cd09a518c602a55bd406bcc291a967b284cfa7a63edfbf8b897ea4748aad23c"
|
checksum = "4cd09a518c602a55bd406bcc291a967b284cfa7a63edfbf8b897ea4748aad23c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
"arrow-data",
|
"arrow-data",
|
||||||
@@ -405,7 +416,7 @@ version = "52.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "600bae05d43483d216fb3494f8c32fdbefd8aa4e1de237e790dbb3d9f44690a3"
|
checksum = "600bae05d43483d216fb3494f8c32fdbefd8aa4e1de237e790dbb3d9f44690a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
"arrow-data",
|
"arrow-data",
|
||||||
@@ -676,7 +687,7 @@ dependencies = [
|
|||||||
"fastrand 2.3.0",
|
"fastrand 2.3.0",
|
||||||
"hex",
|
"hex",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -1089,7 +1100,7 @@ dependencies = [
|
|||||||
"sha1",
|
"sha1",
|
||||||
"sync_wrapper 1.0.2",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite 0.26.2",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -1855,6 +1866,25 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "config"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"json5",
|
||||||
|
"lazy_static",
|
||||||
|
"nom",
|
||||||
|
"pathdiff",
|
||||||
|
"ron 0.7.1",
|
||||||
|
"rust-ini 0.18.0",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"toml 0.5.11",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@@ -1866,8 +1896,8 @@ dependencies = [
|
|||||||
"json5",
|
"json5",
|
||||||
"nom",
|
"nom",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"ron",
|
"ron 0.8.1",
|
||||||
"rust-ini",
|
"rust-ini 0.20.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"toml 0.8.20",
|
"toml 0.8.20",
|
||||||
@@ -2236,7 +2266,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4fd4a99fc70d40ef7e52b243b4a399c3f8d353a40d5ecb200deee05e49c61bb"
|
checksum = "e4fd4a99fc70d40ef7e52b243b4a399c3f8d353a40d5ecb200deee05e49c61bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-ipc",
|
"arrow-ipc",
|
||||||
@@ -2299,7 +2329,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44fdbc877e3e40dcf88cc8f283d9f5c8851f0a3aa07fee657b1b75ac1ad49b9c"
|
checksum = "44fdbc877e3e40dcf88cc8f283d9f5c8851f0a3aa07fee657b1b75ac1ad49b9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -2350,7 +2380,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c1841c409d9518c17971d15c9bae62e629eb937e6fb6c68cd32e9186f8b30d2"
|
checksum = "1c1841c409d9518c17971d15c9bae62e629eb937e6fb6c68cd32e9186f8b30d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -2392,7 +2422,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b4ece19f73c02727e5e8654d79cd5652de371352c1df3c4ac3e419ecd6943fb"
|
checksum = "2b4ece19f73c02727e5e8654d79cd5652de371352c1df3c4ac3e419ecd6943fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
"datafusion-common",
|
"datafusion-common",
|
||||||
@@ -2452,7 +2482,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a223962b3041304a3e20ed07a21d5de3d88d7e4e71ca192135db6d24e3365a4"
|
checksum = "9a223962b3041304a3e20ed07a21d5de3d88d7e4e71ca192135db6d24e3365a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -2482,7 +2512,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db5e7d8532a1601cd916881db87a70b0a599900d23f3db2897d389032da53bc6"
|
checksum = "db5e7d8532a1601cd916881db87a70b0a599900d23f3db2897d389032da53bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"datafusion-common",
|
"datafusion-common",
|
||||||
"datafusion-expr",
|
"datafusion-expr",
|
||||||
@@ -2508,7 +2538,7 @@ version = "41.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d1116949432eb2d30f6362707e2846d942e491052a206f2ddcb42d08aea1ffe"
|
checksum = "8d1116949432eb2d30f6362707e2846d942e491052a206f2ddcb42d08aea1ffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -2701,6 +2731,12 @@ dependencies = [
|
|||||||
"syn 2.0.99",
|
"syn 2.0.99",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlv-list"
|
name = "dlv-list"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -3493,6 +3529,32 @@ dependencies = [
|
|||||||
"wiremock",
|
"wiremock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "goose-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"config 0.13.4",
|
||||||
|
"dashmap 6.1.0",
|
||||||
|
"futures",
|
||||||
|
"futures-util",
|
||||||
|
"goose",
|
||||||
|
"goose-mcp",
|
||||||
|
"jsonwebtoken 8.3.0",
|
||||||
|
"mcp-client",
|
||||||
|
"mcp-core",
|
||||||
|
"mcp-server",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
|
"warp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-bench"
|
name = "goose-bench"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -3679,7 +3741,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.5.31",
|
"clap 4.5.31",
|
||||||
"config",
|
"config 0.14.1",
|
||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -3775,6 +3837,9 @@ name = "hashbrown"
|
|||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.7.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -3782,7 +3847,7 @@ version = "0.14.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3806,6 +3871,30 @@ dependencies = [
|
|||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http 0.2.12",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
|
||||||
|
dependencies = [
|
||||||
|
"http 0.2.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -4579,6 +4668,20 @@ dependencies = [
|
|||||||
"uuid-simd",
|
"uuid-simd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "8.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"pem 1.1.1",
|
||||||
|
"ring 0.16.20",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonwebtoken"
|
name = "jsonwebtoken"
|
||||||
version = "9.3.1"
|
version = "9.3.1"
|
||||||
@@ -4587,8 +4690,8 @@ checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"pem",
|
"pem 3.0.5",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simple_asn1",
|
"simple_asn1",
|
||||||
@@ -5599,6 +5702,46 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
|
||||||
|
name = "monostate"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aafe1be9d0c75642e3e50fedc7ecadf1ef1cbce6eb66462153fc44245343fbee"
|
||||||
|
dependencies = [
|
||||||
|
"monostate-impl",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "monostate-impl"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c402a4092d5e204f32c9e155431046831fa712637043c58cb73bc6bc6c9663b5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multer"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"spin 0.9.8",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multimap"
|
name = "multimap"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -5936,7 +6079,7 @@ dependencies = [
|
|||||||
"quick-xml 0.36.2",
|
"quick-xml 0.36.2",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest 0.12.12",
|
"reqwest 0.12.12",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"rustls-pemfile 2.2.0",
|
"rustls-pemfile 2.2.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -6047,13 +6190,23 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list 0.3.0",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-multimap"
|
name = "ordered-multimap"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlv-list",
|
"dlv-list 0.5.2",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6146,6 +6299,15 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "3.0.5"
|
version = "3.0.5"
|
||||||
@@ -6660,7 +6822,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"rustls 0.23.23",
|
"rustls 0.23.23",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -7090,6 +7252,21 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.16.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"spin 0.5.2",
|
||||||
|
"untrusted 0.7.1",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -7100,7 +7277,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7148,6 +7325,17 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ron"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -7160,6 +7348,16 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap 0.4.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@@ -7167,7 +7365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
|
checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"ordered-multimap",
|
"ordered-multimap 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7254,7 +7452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"rustls-webpki 0.101.7",
|
"rustls-webpki 0.101.7",
|
||||||
"sct",
|
"sct",
|
||||||
]
|
]
|
||||||
@@ -7266,7 +7464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.102.8",
|
"rustls-webpki 0.102.8",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -7330,8 +7528,8 @@ version = "0.101.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7340,9 +7538,9 @@ version = "0.102.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7440,6 +7638,12 @@ dependencies = [
|
|||||||
"syn 2.0.99",
|
"syn 2.0.99",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -7472,8 +7676,8 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring 0.17.14",
|
||||||
"untrusted",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7883,6 +8087,31 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
|
||||||
|
name = "spin"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spm_precompiled"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser"
|
name = "sqlparser"
|
||||||
version = "0.49.0"
|
version = "0.49.0"
|
||||||
@@ -8614,6 +8843,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite 0.21.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
@@ -8623,7 +8864,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tungstenite",
|
"tungstenite 0.26.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8851,6 +9092,25 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.2.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"url",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
@@ -8897,7 +9157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "17ec15f1f191ba42ba0ed0f788999eec910c201cbbd4ae5de7cf0eb0a94b3d1a"
|
checksum = "17ec15f1f191ba42ba0ed0f788999eec910c201cbbd4ae5de7cf0eb0a94b3d1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"ahash",
|
"ahash 0.8.11",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"cbc",
|
"cbc",
|
||||||
@@ -9083,6 +9343,12 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -9249,6 +9515,35 @@ dependencies = [
|
|||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "warp"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"headers",
|
||||||
|
"http 0.2.12",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"multer",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"scoped-tls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite 0.21.0",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
|||||||
30
crates/goose-api/Cargo.toml
Normal file
30
crates/goose-api/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "goose-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
goose = { path = "../goose" }
|
||||||
|
goose-mcp = { path = "../goose-mcp" }
|
||||||
|
mcp-client = { path = "../mcp-client" }
|
||||||
|
mcp-core = { path = "../mcp-core" }
|
||||||
|
mcp-server = { path = "../mcp-server" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
warp = "0.3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] }
|
||||||
|
config = "0.13"
|
||||||
|
jsonwebtoken = "8"
|
||||||
|
futures = "0.3"
|
||||||
|
futures-util = "0.3"
|
||||||
|
# For session IDs
|
||||||
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
dashmap = "6"
|
||||||
|
# Add dynamic-library for extension loading
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
async-trait = "0.1"
|
||||||
450
crates/goose-api/README.md
Normal file
450
crates/goose-api/README.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# Goose API
|
||||||
|
|
||||||
|
An asynchronous REST API for interacting with Goose's AI agent capabilities.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The goose-api crate provides an HTTP API interface to Goose's AI capabilities, enabling integration with other services and applications. It is designed as a daemon that can be run in the background, offering the same core functionality as the Goose CLI but accessible over HTTP.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust toolchain (cargo, rustc)
|
||||||
|
- Goose dependencies
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to the goose-api directory
|
||||||
|
cd crates/goose-api
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# For a production-optimized build
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Goose API supports configuration via environment variables and configuration files.
|
||||||
|
The precedence order is:
|
||||||
|
|
||||||
|
1. Environment variables (highest priority)
|
||||||
|
2. Goose CLI configuration file (usually `~/.config/goose/config.yaml`) if it exists
|
||||||
|
3. `config` file shipped with the crate
|
||||||
|
4. Default values (lowest priority)
|
||||||
|
|
||||||
|
### Configuration File
|
||||||
|
|
||||||
|
If no CLI configuration file is found, goose-api looks for a `config` file in its
|
||||||
|
crate directory. This file has no extension and can be JSON, YAML, TOML, etc.
|
||||||
|
The `config` crate will detect the format automatically.
|
||||||
|
|
||||||
|
Example `config` file (YAML format):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# API server configuration
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8080
|
||||||
|
api_key: your_secure_api_key
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
provider: openai
|
||||||
|
model: gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
All configurations can be set using environment variables prefixed with `GOOSE_API_`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API server configuration
|
||||||
|
export GOOSE_API_HOST=0.0.0.0
|
||||||
|
export GOOSE_API_PORT=8080
|
||||||
|
export GOOSE_API_KEY=your_secure_api_key
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
export GOOSE_API_PROVIDER=openai
|
||||||
|
export GOOSE_API_MODEL=gpt-4o
|
||||||
|
|
||||||
|
# Provider-specific credentials (based on provider requirements)
|
||||||
|
export OPENAI_API_KEY=your_openai_api_key
|
||||||
|
export ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||||
|
# etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Authentication
|
||||||
|
|
||||||
|
All API endpoints require authentication using an API key. The key should be provided in the `x-api-key` header.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
x-api-key: your_secure_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the server in development mode
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Run the compiled binary directly
|
||||||
|
./target/debug/goose-api
|
||||||
|
|
||||||
|
# For production (with optimizations)
|
||||||
|
./target/release/goose-api
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the server runs on `127.0.0.1:8080`. You can modify this using configuration options.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Start a Session
|
||||||
|
|
||||||
|
**Endpoint**: `POST /session/start`
|
||||||
|
|
||||||
|
**Description**: Initiates a new session with Goose, providing an initial prompt.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- Content-Type: application/json
|
||||||
|
- x-api-key: [your-api-key]
|
||||||
|
- Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "Your instruction to Goose"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Session started with prompt: Your instruction to Goose",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Reply to a Session
|
||||||
|
|
||||||
|
**Endpoint**: `POST /session/reply`
|
||||||
|
|
||||||
|
**Description**: Sends a follow-up message to an existing session.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- Content-Type: application/json
|
||||||
|
- x-api-key: [your-api-key]
|
||||||
|
- Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "Your follow-up instruction"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Reply: Response from Goose",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. List Extensions
|
||||||
|
|
||||||
|
**Endpoint**: `GET /extensions/list`
|
||||||
|
|
||||||
|
**Description**: Returns a list of available extensions.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- x-api-key: [your-api-key]
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"extensions": ["extension1", "extension2", "extension3"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
**Description**: Returns the current provider configuration.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- x-api-key: [your-api-key]
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Summarize Session
|
||||||
|
|
||||||
|
**Endpoint**: `POST /session/summarize`
|
||||||
|
|
||||||
|
**Description**: Summarizes the full conversation for a given session.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- Content-Type: application/json
|
||||||
|
- x-api-key: [your-api-key]
|
||||||
|
- Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "<uuid>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "<summarized conversation>",
|
||||||
|
"status": "success"
|
||||||
|
=======
|
||||||
|
### 7. Metrics
|
||||||
|
|
||||||
|
**Endpoint**: `GET /metrics`
|
||||||
|
|
||||||
|
**Description**: Returns runtime metrics about stored sessions and extensions.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
- Headers:
|
||||||
|
- `x-api-key: [your-api-key]`
|
||||||
|
|
||||||
|
**Response** (example):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_messages": {
|
||||||
|
"20240605_001234": 3,
|
||||||
|
"20240605_010000": 5
|
||||||
|
},
|
||||||
|
"active_sessions": 2,
|
||||||
|
"pending_requests": {
|
||||||
|
"mcp_say": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
Sessions created via the API are stored in the same location as the CLI
|
||||||
|
(`~/.local/share/goose/sessions` on most platforms). Each session is saved to a
|
||||||
|
`<session_id>.jsonl` file.
|
||||||
|
|
||||||
|
You can resume or inspect these sessions with the CLI by providing the session ID
|
||||||
|
(which is a UUID) returned from the API. For example, if the API returns a
|
||||||
|
`session_id` of `a1b2c3d4-e5f6-7890-1234-567890abcdef`, you can resume it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
goose session --resume --name a1b2c3d4-e5f6-7890-1234-567890abcdef
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Using cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start a session
|
||||||
|
curl -X POST http://localhost:8080/session/start \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: kurac" \
|
||||||
|
-d '{"prompt": "Create a Python function to generate Fibonacci numbers"}'
|
||||||
|
|
||||||
|
# Reply to an ongoing session
|
||||||
|
curl -X POST http://localhost:8080/session/reply \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: your_secure_api_key" \
|
||||||
|
-d '{"prompt": "Add documentation to this function"}'
|
||||||
|
|
||||||
|
# List extensions
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Summarize a session
|
||||||
|
curl -X POST http://localhost:8080/session/summarize \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: your_secure_api_key" \
|
||||||
|
-d '{"session_id": "your-session-id"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_URL = "http://localhost:8080"
|
||||||
|
API_KEY = "your_secure_api_key"
|
||||||
|
HEADERS = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start a session
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/session/start",
|
||||||
|
headers=HEADERS,
|
||||||
|
json={"prompt": "Create a Python function to generate Fibonacci numbers"}
|
||||||
|
)
|
||||||
|
print(response.json())
|
||||||
|
|
||||||
|
# Reply to an ongoing session
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/session/reply",
|
||||||
|
headers=HEADERS,
|
||||||
|
json={"prompt": "Add documentation to this function"}
|
||||||
|
)
|
||||||
|
print(response.json())
|
||||||
|
|
||||||
|
# List extensions
|
||||||
|
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())
|
||||||
|
|
||||||
|
# Summarize a session
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/session/summarize",
|
||||||
|
headers=HEADERS,
|
||||||
|
json={"session_id": "your-session-id"}
|
||||||
|
)
|
||||||
|
print(response.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **API Key Authentication Failure**:
|
||||||
|
Ensure the key in your request header matches the configured API key.
|
||||||
|
|
||||||
|
2. **Provider Configuration Issues**:
|
||||||
|
Make sure you've set the necessary environment variables for your chosen provider.
|
||||||
|
|
||||||
|
3. **Missing Required Keys**:
|
||||||
|
Check the server logs for messages about missing required provider configuration keys.
|
||||||
|
|
||||||
|
## Implementation Status (vs. Implementation Plan)
|
||||||
|
|
||||||
|
The current implementation includes the following features from the implementation plan:
|
||||||
|
|
||||||
|
✅ **Step 1-2**: Created goose-api crate with necessary dependencies
|
||||||
|
✅ **Step 3-4**: Defined API endpoints with request/response structures
|
||||||
|
✅ **Step 5**: Integration with goose core functionality
|
||||||
|
✅ **Step 6**: Configuration via environment variables and config file
|
||||||
|
✅ **Step 9**: API Key authentication
|
||||||
|
|
||||||
|
🟡 **Step 7**: Extension loading mechanism (partial implementation)
|
||||||
|
🟡 **Step 8**: MCP support (partial implementation)
|
||||||
|
✅ **Step 10**: Documentation
|
||||||
|
✅ **Step 11**: Tests
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Run all unit and integration tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
This command executes the entire workspace test suite. To test a single crate, use `cargo test -p <crate>`.
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
- Extend session management capabilities
|
||||||
|
- Add more comprehensive error handling
|
||||||
|
- Expand unit and integration tests
|
||||||
|
- Complete MCP integration
|
||||||
|
- Add metrics and monitoring
|
||||||
|
- Add OpenAPI documentation generation
|
||||||
8
crates/goose-api/config
Normal file
8
crates/goose-api/config
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# API server configuration
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 8181
|
||||||
|
api_key: kurac
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
provider: ollama
|
||||||
|
model: qwen3:4b
|
||||||
53
crates/goose-api/goose-api-plan.md
Normal file
53
crates/goose-api/goose-api-plan.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Plan for `goose-api` Review and Improvements
|
||||||
|
|
||||||
|
This document outlines the plan to address the user's request regarding `goose-api`'s interaction with `goose-cli`, session sharing, and reported resource exhaustion/memory leaks. All changes will be confined to the `crates/goose-api` crate.
|
||||||
|
|
||||||
|
## Summary of Findings
|
||||||
|
|
||||||
|
### Session Sharing
|
||||||
|
* Both `goose-api` and `goose-cli` leverage the `goose` crate's session management, storing sessions as `.jsonl` files in a common directory (`~/.local/share/goose/sessions` by default).
|
||||||
|
* `goose-api` generates a `Uuid` for each new session and returns it. This UUID is used as the session name for file persistence.
|
||||||
|
* `goose-cli`'s `session resume` command can accept a session name or path. Therefore, the UUID returned by `goose-api` can be used directly with `goose-cli session --resume --name <UUID>`.
|
||||||
|
|
||||||
|
### Resource Exhaustion and Memory Leaks
|
||||||
|
* **Primary Suspect: Partial Stream Consumption in `agent.reply`:** In `crates/goose-api/src/handlers.rs`, both `start_session_handler` and `reply_session_handler` only consume the *first* item from the `BoxStream` returned by `agent.reply`. If `agent.reply` produces a stream of multiple messages (common for LLM interactions), the remaining messages and associated resources are not consumed or released, leading to memory accumulation. This is highly likely to be the root cause of single-session resource exhaustion.
|
||||||
|
* **Per-Session `Agent` Instances:** `goose-api` creates a new `Agent` instance for each session and stores it in an in-memory `DashMap` (`SESSIONS`). While this provides session isolation, it means more `Agent` instances (each with its own internal state and resources) are held in memory.
|
||||||
|
* **Session Cleanup:** `cleanup_expired_sessions()` is called to remove inactive sessions from the `DashMap` after `SESSION_TIMEOUT_SECS` (currently 1 hour). If this timeout is too long, or if `Agent` instances don't fully release resources upon being dropped, memory can accumulate.
|
||||||
|
* **LLM Calls for Summarization:** `generate_description` (in `goose::session::storage`) and `agent.summarize_context` (in `goose` crate) involve additional LLM calls, which are resource-intensive operations.
|
||||||
|
* **Extension Management:** `Stdio` extensions can spawn external processes. If these processes are not properly terminated when their associated `Agent` is dropped, they could contribute to leaks.
|
||||||
|
|
||||||
|
## Detailed Plan
|
||||||
|
|
||||||
|
### Phase 1: Address Immediate Resource Leak (Critical)
|
||||||
|
|
||||||
|
1. **Fully Consume `agent.reply` Stream in `crates/goose-api/src/handlers.rs`:**
|
||||||
|
* **Action:** Modify `start_session_handler` and `reply_session_handler` to iterate through the entire `BoxStream<anyhow::Result<Message>>` returned by `agent.reply`. All messages from the stream will be collected and concatenated to form the complete response. This ensures all resources associated with the stream are properly released.
|
||||||
|
|
||||||
|
* **Mermaid Diagram for Stream Consumption:**
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Call agent.reply()] --> B{Receive BoxStream<Message>};
|
||||||
|
B --> C{Loop: stream.try_next().await};
|
||||||
|
C -- Has Message --> D[Append Message to history];
|
||||||
|
C -- No More Messages / Error --> E[Process complete response];
|
||||||
|
D --> C;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Improve Session Sharing (Documentation within `goose-api`)
|
||||||
|
|
||||||
|
1. **Clarify Session ID Usage in `crates/goose-api/README.md`:**
|
||||||
|
* **Action:** Add a clear note or example in the "Session Management" section of `crates/goose-api/README.md` demonstrating that the `session_id` (UUID) returned by the API can be directly used with `goose-cli session --resume --name <UUID>`.
|
||||||
|
|
||||||
|
### Phase 3: Investigate and Mitigate Potential Resource Issues (within `goose-api` only)
|
||||||
|
|
||||||
|
1. **Review `ApiSession` and `cleanup_expired_sessions` in `crates/goose-api/src/api_sessions.rs`:**
|
||||||
|
* **Action:** No code change is immediately required.
|
||||||
|
* **Recommendation (for user consideration):** The `SESSION_TIMEOUT_SECS` constant (currently 1 hour) is a critical parameter. If resource issues persist after Phase 1, reducing this timeout (e.g., to 5-15 minutes) would cause inactive `Agent` instances to be dropped more quickly, freeing up their resources. This would be a configuration/tuning step.
|
||||||
|
|
||||||
|
2. **Monitor `generate_description` and `summarize_context` calls:**
|
||||||
|
* **Action:** No direct code change in `goose-api` is possible for the implementation of these functions as they reside in the `goose` crate.
|
||||||
|
* **Recommendation (for user consideration):** These LLM calls add to the overall load. If resource issues are observed, especially during summarization, it might indicate a bottleneck in the LLM provider interaction or the `goose` crate's handling of large contexts.
|
||||||
|
|
||||||
|
3. **Extension Management:**
|
||||||
|
* **Action:** No direct code change in `goose-api` is possible to fix potential leaks within the `goose` crate's `ExtensionManager`.
|
||||||
|
* **Recommendation (for user consideration):** If specific `Stdio` extensions are identified as problematic, the user might need to investigate their implementation or consider if `goose-api` could offer a way to explicitly terminate processes associated with a session's `Agent` when the session expires.
|
||||||
62
crates/goose-api/src/api_sessions.rs
Normal file
62
crates/goose-api/src/api_sessions.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use dashmap::DashMap;
|
||||||
|
use goose::agents::Agent;
|
||||||
|
use std::sync::{atomic::{AtomicU64, Ordering}, Arc, LazyLock};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use crate::handlers::shutdown_agent_extensions;
|
||||||
|
|
||||||
|
pub struct ApiSession {
|
||||||
|
pub agent: Arc<Mutex<Agent>>, // agent for this session
|
||||||
|
last_active: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiSession {
|
||||||
|
pub fn new(agent: Agent) -> Self {
|
||||||
|
Self {
|
||||||
|
agent: Arc::new(Mutex::new(agent)),
|
||||||
|
last_active: AtomicU64::new(current_timestamp()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn touch(&self) {
|
||||||
|
self.last_active.store(current_timestamp(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_expired(&self, ttl: Duration) -> bool {
|
||||||
|
current_timestamp() - self.last_active.load(Ordering::Relaxed) > ttl.as_secs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_timestamp() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static SESSIONS: LazyLock<DashMap<Uuid, ApiSession>> = LazyLock::new(DashMap::new);
|
||||||
|
|
||||||
|
pub const SESSION_TIMEOUT_SECS: u64 = 3600;
|
||||||
|
|
||||||
|
pub async fn cleanup_expired_sessions() {
|
||||||
|
let ttl = Duration::from_secs(SESSION_TIMEOUT_SECS);
|
||||||
|
let mut sessions_to_remove = Vec::new();
|
||||||
|
|
||||||
|
// Collect sessions to remove and shut down their agents
|
||||||
|
for entry in SESSIONS.iter() {
|
||||||
|
let sess = entry.value();
|
||||||
|
if sess.is_expired(ttl) {
|
||||||
|
sessions_to_remove.push(entry.key().clone());
|
||||||
|
// Acquire agent and shut down extensions
|
||||||
|
shutdown_agent_extensions(sess.agent.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sessions from the DashMap
|
||||||
|
for session_id in sessions_to_remove {
|
||||||
|
SESSIONS.remove(&session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
183
crates/goose-api/src/config.rs
Normal file
183
crates/goose-api/src/config.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
use crate::handlers::AGENT;
|
||||||
|
use goose::config::{Config, ExtensionEntry};
|
||||||
|
use goose::agents::ExtensionConfig;
|
||||||
|
use goose::providers::{create, providers};
|
||||||
|
use goose::model::ModelConfig;
|
||||||
|
use tracing::{info, warn, error};
|
||||||
|
use config::{builder::DefaultState, ConfigBuilder, Environment, File};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn load_configuration() -> std::result::Result<config::Config, config::ConfigError> {
|
||||||
|
// Determine the configuration file based on priority:
|
||||||
|
// 1. Explicit GOOSE_CONFIG env var
|
||||||
|
// 2. Goose CLI config if it exists
|
||||||
|
// 3. Fallback to config file packaged with goose-api
|
||||||
|
|
||||||
|
let config_path = if let Ok(path) = std::env::var("GOOSE_CONFIG") {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
let global = Config::global();
|
||||||
|
if global.exists() {
|
||||||
|
global.path()
|
||||||
|
} else {
|
||||||
|
// Use the config file that ships with goose-api
|
||||||
|
format!("{}/config", env!("CARGO_MANIFEST_DIR"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let builder = ConfigBuilder::<DefaultState>::default()
|
||||||
|
.add_source(File::with_name(&config_path).required(false))
|
||||||
|
.add_source(Environment::with_prefix("GOOSE_API"));
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize_provider_config() -> Result<(), anyhow::Error> {
|
||||||
|
let api_config = load_configuration()?;
|
||||||
|
|
||||||
|
let global_config = Config::global();
|
||||||
|
|
||||||
|
let provider_name = if let Ok(val) = std::env::var("GOOSE_API_PROVIDER") {
|
||||||
|
val
|
||||||
|
} else if let Ok(val) = api_config.get_string("provider") {
|
||||||
|
val
|
||||||
|
} else if global_config.exists() {
|
||||||
|
global_config
|
||||||
|
.get_param::<String>("GOOSE_PROVIDER")
|
||||||
|
.unwrap_or_else(|_| "openai".to_string())
|
||||||
|
} else {
|
||||||
|
"openai".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let model_name = if let Ok(val) = std::env::var("GOOSE_API_MODEL") {
|
||||||
|
val
|
||||||
|
} else if let Ok(val) = api_config.get_string("model") {
|
||||||
|
val
|
||||||
|
} else if global_config.exists() {
|
||||||
|
global_config
|
||||||
|
.get_param::<String>("GOOSE_MODEL")
|
||||||
|
.unwrap_or_else(|_| "gpt-4o".to_string())
|
||||||
|
} else {
|
||||||
|
"gpt-4o".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Initializing with provider: {}, model: {}", provider_name, model_name);
|
||||||
|
|
||||||
|
let config = Config::global();
|
||||||
|
config.set_param("GOOSE_PROVIDER", Value::String(provider_name.clone()))?;
|
||||||
|
config.set_param("GOOSE_MODEL", Value::String(model_name.clone()))?;
|
||||||
|
|
||||||
|
let available_providers = providers();
|
||||||
|
if let Some(provider_meta) = available_providers.iter().find(|p| p.name == provider_name) {
|
||||||
|
for key in &provider_meta.config_keys {
|
||||||
|
let env_name = key.name.clone();
|
||||||
|
if let Ok(value) = std::env::var(&env_name) {
|
||||||
|
if key.secret {
|
||||||
|
config.set_secret(&key.name, Value::String(value))?;
|
||||||
|
info!("Set secret key: {}", key.name);
|
||||||
|
} else {
|
||||||
|
config.set_param(&key.name, Value::String(value))?;
|
||||||
|
info!("Set parameter: {}", key.name);
|
||||||
|
}
|
||||||
|
} else if global_config.exists() {
|
||||||
|
// If not provided via environment, try existing CLI config
|
||||||
|
let result: Result<String, _> = if key.secret {
|
||||||
|
global_config.get_secret(&key.name)
|
||||||
|
} else {
|
||||||
|
global_config.get_param(&key.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(value) => {
|
||||||
|
if key.secret {
|
||||||
|
config.set_secret(&key.name, Value::String(value))?;
|
||||||
|
} else {
|
||||||
|
config.set_param(&key.name, Value::String(value))?;
|
||||||
|
}
|
||||||
|
info!("Loaded {} from CLI config", key.name);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if let Some(default) = &key.default {
|
||||||
|
if key.secret {
|
||||||
|
config.set_secret(&key.name, Value::String(default.clone()))?;
|
||||||
|
} else {
|
||||||
|
config.set_param(&key.name, Value::String(default.clone()))?;
|
||||||
|
}
|
||||||
|
info!("Using default for {}", key.name);
|
||||||
|
} else if key.required {
|
||||||
|
error!("Required key {} not provided", key.name);
|
||||||
|
return Err(anyhow::anyhow!("Required key {} not provided", key.name));
|
||||||
|
} else {
|
||||||
|
warn!("Environment variable not set for key: {}", key.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(default) = &key.default {
|
||||||
|
if key.secret {
|
||||||
|
config.set_secret(&key.name, Value::String(default.clone()))?;
|
||||||
|
} else {
|
||||||
|
config.set_param(&key.name, Value::String(default.clone()))?;
|
||||||
|
}
|
||||||
|
info!("Using default for {}", key.name);
|
||||||
|
|
||||||
|
} else if key.required {
|
||||||
|
error!("Required key {} not provided", key.name);
|
||||||
|
return Err(anyhow::anyhow!("Required key {} not provided", key.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let model_config = ModelConfig::new(model_name);
|
||||||
|
let provider = create(&provider_name, model_config)?;
|
||||||
|
|
||||||
|
let agent = AGENT.lock().await;
|
||||||
|
agent.update_provider(provider).await?;
|
||||||
|
|
||||||
|
info!("Provider configuration successful");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize_extensions(config: &config::Config) -> Result<(), anyhow::Error> {
|
||||||
|
let agent = AGENT.lock().await;
|
||||||
|
|
||||||
|
// First, remove any existing extensions from a previous run (if any)
|
||||||
|
let existing_extensions = agent.list_extensions().await;
|
||||||
|
drop(agent); // Release lock before async calls
|
||||||
|
|
||||||
|
for ext_name in existing_extensions {
|
||||||
|
let agent_guard = AGENT.lock().await;
|
||||||
|
if let Err(e) = agent_guard.remove_extension(&ext_name).await {
|
||||||
|
error!("Failed to remove existing extension {} during initialization cleanup: {}", ext_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, proceed with adding extensions from the config
|
||||||
|
let agent = AGENT.lock().await; // Re-acquire lock
|
||||||
|
if let Ok(ext_table) = config.get_table("extensions") {
|
||||||
|
for (name, ext_config) in ext_table {
|
||||||
|
let entry: ExtensionEntry = ext_config.clone().try_deserialize()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to deserialize extension config for {}: {}", name, e))?;
|
||||||
|
|
||||||
|
if entry.enabled {
|
||||||
|
let extension_config: ExtensionConfig = entry.config;
|
||||||
|
if let Err(e) = agent.add_extension(extension_config).await {
|
||||||
|
error!("Failed to add extension {}: {}", name, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Skipping disabled extension: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("No extensions configured in config file.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_init_tests() -> Result<(), anyhow::Error> {
|
||||||
|
info!("Running initialization tests");
|
||||||
|
{
|
||||||
|
let _agent = AGENT.lock().await;
|
||||||
|
info!("Agent initialization test passed");
|
||||||
|
}
|
||||||
|
info!("Initialization tests completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
573
crates/goose-api/src/handlers.rs
Normal file
573
crates/goose-api/src/handlers.rs
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
use warp::{http::HeaderValue, Filter, Rejection, reject::custom};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use tracing::{info, warn, error};
|
||||||
|
use mcp_core::tool::Tool;
|
||||||
|
use goose::agents::{extension::Envs, extension_manager::ExtensionManager, Agent, SessionConfig, AgentEvent};
|
||||||
|
use goose::message::{Message, MessageContent};
|
||||||
|
use goose::session::{self, Identifier};
|
||||||
|
use goose::config::Config;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
use tokio::sync::Mutex; // Explicitly add this import
|
||||||
|
use crate::api_sessions::{ApiSession, SESSIONS, cleanup_expired_sessions};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
// Custom rejection type for anyhow::Error
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AnyhowRejection(#[allow(dead_code)] anyhow::Error);
|
||||||
|
|
||||||
|
impl warp::reject::Reject for AnyhowRejection {}
|
||||||
|
|
||||||
|
pub static EXTENSION_MANAGER: LazyLock<ExtensionManager> = LazyLock::new(|| ExtensionManager::default());
|
||||||
|
pub static AGENT: LazyLock<tokio::sync::Mutex<Agent>> = LazyLock::new(|| tokio::sync::Mutex::new(Agent::new()));
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SessionRequest {
|
||||||
|
pub prompt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ApiResponse {
|
||||||
|
pub message: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct StartSessionResponse {
|
||||||
|
pub message: String,
|
||||||
|
pub status: String,
|
||||||
|
pub session_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SessionReplyRequest {
|
||||||
|
pub session_id: Uuid,
|
||||||
|
pub prompt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct EndSessionRequest {
|
||||||
|
pub session_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SummarizeSessionRequest {
|
||||||
|
pub session_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ExtensionsResponse {
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ProviderConfig {
|
||||||
|
pub provider: String,
|
||||||
|
pub model: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ExtensionResponse {
|
||||||
|
pub error: bool,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct MetricsResponse {
|
||||||
|
pub active_sessions: usize,
|
||||||
|
pub pending_requests: HashMap<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub 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>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_session_handler(
|
||||||
|
req: SessionRequest,
|
||||||
|
_api_key: String,
|
||||||
|
) -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Starting session with prompt: {}", req.prompt);
|
||||||
|
|
||||||
|
cleanup_expired_sessions().await;
|
||||||
|
|
||||||
|
// create fresh agent using provider from the template agent
|
||||||
|
let template = AGENT.lock().await;
|
||||||
|
let new_agent = Agent::new();
|
||||||
|
if let Ok(provider) = template.provider().await {
|
||||||
|
let _ = new_agent.update_provider(provider).await;
|
||||||
|
}
|
||||||
|
drop(template);
|
||||||
|
|
||||||
|
let mut messages = vec![Message::user().with_text(&req.prompt)];
|
||||||
|
let session_id = Uuid::new_v4();
|
||||||
|
let session_name = session_id.to_string();
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_name.clone()));
|
||||||
|
|
||||||
|
let session = ApiSession::new(new_agent);
|
||||||
|
let agent_ref = session.agent.clone();
|
||||||
|
SESSIONS.insert(session_id, session);
|
||||||
|
|
||||||
|
let provider = agent_ref.lock().await.provider().await.ok();
|
||||||
|
|
||||||
|
let agent_locked = agent_ref.lock().await;
|
||||||
|
let result = agent_locked
|
||||||
|
.reply(
|
||||||
|
&messages,
|
||||||
|
Some(SessionConfig {
|
||||||
|
id: Identifier::Name(session_name.clone()),
|
||||||
|
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||||
|
schedule_id: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
let mut full_response_text = String::new();
|
||||||
|
let mut final_status = "success".to_string();
|
||||||
|
|
||||||
|
while let Some(agent_event) = stream.try_next().await.map_err(|e| custom(AnyhowRejection(e)))? {
|
||||||
|
let response = match agent_event {
|
||||||
|
AgentEvent::Message(msg) => msg,
|
||||||
|
_ => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if matches!(response.content.first(), Some(MessageContent::ContextLengthExceeded(_))) {
|
||||||
|
// This block needs to be handled carefully.
|
||||||
|
// The `agent` here refers to the global AGENT, not the session-specific agent_ref.
|
||||||
|
// This might be a bug in the original code.
|
||||||
|
// For now, I'll keep the existing logic but note this potential issue.
|
||||||
|
let session_agent = agent_ref.lock().await; // Use session-specific agent
|
||||||
|
match session_agent.summarize_context(&messages).await {
|
||||||
|
Ok((summarized, _)) => {
|
||||||
|
messages = summarized;
|
||||||
|
final_status = "warning".to_string();
|
||||||
|
full_response_text = "Conversation summarized to fit context window".to_string();
|
||||||
|
// Persist summarized messages immediately
|
||||||
|
if let Err(e) = session::persist_messages(&session_path, &messages, provider.clone()).await {
|
||||||
|
warn!("Failed to persist session {}: {}", session_name, e);
|
||||||
|
}
|
||||||
|
break; // Exit loop after summarization
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to summarize context: {}", e);
|
||||||
|
final_status = "error".to_string();
|
||||||
|
full_response_text = format!("Failed to summarize context: {}", e);
|
||||||
|
break; // Exit loop on summarization error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let response_text = response.as_concat_text();
|
||||||
|
full_response_text.push_str(&response_text);
|
||||||
|
messages.push(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if full_response_text.is_empty() && final_status == "success" {
|
||||||
|
final_status = "warning".to_string();
|
||||||
|
full_response_text = "Session started but no response generated".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist all messages after the stream is fully consumed
|
||||||
|
if let Err(e) = session::persist_messages(&session_path, &messages, provider.clone()).await {
|
||||||
|
warn!("Failed to persist session {}: {}", session_name, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let api_response = StartSessionResponse {
|
||||||
|
message: full_response_text,
|
||||||
|
status: final_status,
|
||||||
|
session_id,
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&api_response),
|
||||||
|
warp::http::StatusCode::OK,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to start session: {}", e);
|
||||||
|
let response = ApiResponse {
|
||||||
|
message: format!("Failed to start session: {}", e),
|
||||||
|
status: "error".to_string(),
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&response),
|
||||||
|
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reply_session_handler(
|
||||||
|
req: SessionReplyRequest,
|
||||||
|
_api_key: String,
|
||||||
|
) -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Replying to session with prompt: {}", req.prompt);
|
||||||
|
|
||||||
|
cleanup_expired_sessions().await;
|
||||||
|
|
||||||
|
let session_name = req.session_id.to_string();
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_name.clone()));
|
||||||
|
|
||||||
|
let session_entry = match SESSIONS.get(&req.session_id) {
|
||||||
|
Some(s) => s,
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session_entry.touch();
|
||||||
|
let agent_ref = session_entry.agent.clone();
|
||||||
|
drop(session_entry);
|
||||||
|
|
||||||
|
let mut messages = match session::read_messages(&session_path) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => {
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.push(Message::user().with_text(&req.prompt));
|
||||||
|
|
||||||
|
let provider = agent_ref.lock().await.provider().await.ok();
|
||||||
|
|
||||||
|
let agent_locked = agent_ref.lock().await;
|
||||||
|
let result = agent_locked
|
||||||
|
.reply(
|
||||||
|
&messages,
|
||||||
|
Some(SessionConfig {
|
||||||
|
id: Identifier::Name(session_name.clone()),
|
||||||
|
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||||
|
schedule_id: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
let mut full_response_text = String::new();
|
||||||
|
let mut final_status = "success".to_string();
|
||||||
|
|
||||||
|
while let Some(agent_event) = stream.try_next().await.map_err(|e| custom(AnyhowRejection(e)))? {
|
||||||
|
let response = match agent_event {
|
||||||
|
AgentEvent::Message(msg) => msg,
|
||||||
|
_ => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if matches!(response.content.first(), Some(MessageContent::ContextLengthExceeded(_))) {
|
||||||
|
// This block needs to be handled carefully.
|
||||||
|
// The `agent` here refers to the global AGENT, not the session-specific agent_ref.
|
||||||
|
// This might be a bug in the original code.
|
||||||
|
// For now, I'll keep the existing logic but note this potential issue.
|
||||||
|
let session_agent = agent_ref.lock().await; // Use session-specific agent
|
||||||
|
match session_agent.summarize_context(&messages).await {
|
||||||
|
Ok((summarized, _)) => {
|
||||||
|
messages = summarized;
|
||||||
|
final_status = "warning".to_string();
|
||||||
|
full_response_text = "Conversation summarized to fit context window".to_string();
|
||||||
|
// Persist summarized messages immediately
|
||||||
|
if let Err(e) = session::persist_messages(&session_path, &messages, provider.clone()).await {
|
||||||
|
warn!("Failed to persist session {}: {}", session_name, e);
|
||||||
|
}
|
||||||
|
break; // Exit loop after summarization
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to summarize context: {}", e);
|
||||||
|
final_status = "error".to_string();
|
||||||
|
full_response_text = format!("Failed to summarize context: {}", e);
|
||||||
|
break; // Exit loop on summarization error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let response_text = response.as_concat_text();
|
||||||
|
full_response_text.push_str(&response_text);
|
||||||
|
messages.push(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if full_response_text.is_empty() && final_status == "success" {
|
||||||
|
final_status = "warning".to_string();
|
||||||
|
full_response_text = "Reply processed but no response generated".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist all messages after the stream is fully consumed
|
||||||
|
if let Err(e) = session::persist_messages(&session_path, &messages, provider.clone()).await {
|
||||||
|
warn!("Failed to persist session {}: {}", session_name, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let api_response = ApiResponse {
|
||||||
|
message: format!("Reply: {}", full_response_text),
|
||||||
|
status: final_status,
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&api_response),
|
||||||
|
warp::http::StatusCode::OK,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to reply to session: {}", e);
|
||||||
|
let response = ApiResponse {
|
||||||
|
message: format!("Failed to reply to session: {}", e),
|
||||||
|
status: "error".to_string(),
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&response),
|
||||||
|
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn end_session_handler(
|
||||||
|
req: EndSessionRequest,
|
||||||
|
_api_key: String,
|
||||||
|
) -> Result<impl warp::Reply, Rejection> {
|
||||||
|
cleanup_expired_sessions().await;
|
||||||
|
|
||||||
|
let session_name = req.session_id.to_string();
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_name.clone()));
|
||||||
|
|
||||||
|
// remove in-memory agent if present
|
||||||
|
if let Some((_, api_session)) = SESSIONS.remove(&req.session_id) {
|
||||||
|
shutdown_agent_extensions(api_session.agent).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if std::fs::remove_file(&session_path).is_ok() {
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn summarize_session_handler(
|
||||||
|
req: SummarizeSessionRequest,
|
||||||
|
_api_key: String,
|
||||||
|
) -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Summarizing session: {}", req.session_id);
|
||||||
|
|
||||||
|
let agent = AGENT.lock().await;
|
||||||
|
|
||||||
|
let session_name = req.session_id.to_string();
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_name.clone()));
|
||||||
|
|
||||||
|
let messages = match session::read_messages(&session_path) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => {
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider = agent.provider().await.ok();
|
||||||
|
|
||||||
|
match agent.summarize_context(&messages).await {
|
||||||
|
Ok((summarized_messages, _)) => {
|
||||||
|
let summary_text = summarized_messages
|
||||||
|
.first()
|
||||||
|
.map(|m| m.as_concat_text())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if let Err(e) = session::persist_messages(&session_path, &summarized_messages, provider.clone()).await {
|
||||||
|
warn!("Failed to persist session {}: {}", session_name, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = ApiResponse {
|
||||||
|
message: summary_text,
|
||||||
|
status: "success".to_string(),
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&resp),
|
||||||
|
warp::http::StatusCode::OK,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to summarize session: {}", e);
|
||||||
|
let resp = ApiResponse {
|
||||||
|
message: format!("Failed to summarize session: {}", e),
|
||||||
|
status: "error".to_string(),
|
||||||
|
};
|
||||||
|
Ok(warp::reply::with_status(
|
||||||
|
warp::reply::json(&resp),
|
||||||
|
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_extensions_handler() -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Listing extensions");
|
||||||
|
|
||||||
|
match EXTENSION_MANAGER.list_extensions().await {
|
||||||
|
Ok(exts) => {
|
||||||
|
let response = ExtensionsResponse { extensions: exts };
|
||||||
|
Ok::<warp::reply::Json, warp::Rejection>(warp::reply::json(&response))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to list extensions: {}", e);
|
||||||
|
let response = ExtensionsResponse {
|
||||||
|
extensions: vec!["Failed to list extensions".to_string()],
|
||||||
|
};
|
||||||
|
Ok::<warp::reply::Json, warp::Rejection>(warp::reply::json(&response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_provider_config_handler() -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Getting provider configuration");
|
||||||
|
|
||||||
|
let config = Config::global();
|
||||||
|
let provider = config
|
||||||
|
.get_param::<String>("GOOSE_PROVIDER")
|
||||||
|
.unwrap_or_else(|_| "Not configured".to_string());
|
||||||
|
let model = config
|
||||||
|
.get_param::<String>("GOOSE_MODEL")
|
||||||
|
.unwrap_or_else(|_| "Not configured".to_string());
|
||||||
|
|
||||||
|
let response = ProviderConfig { provider, model };
|
||||||
|
Ok::<warp::reply::Json, warp::Rejection>(warp::reply::json(&response))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn shutdown_agent_extensions(agent_ref: Arc<Mutex<Agent>>) {
|
||||||
|
let agent_guard = agent_ref.lock().await;
|
||||||
|
let extensions = agent_guard.list_extensions().await;
|
||||||
|
drop(agent_guard);
|
||||||
|
|
||||||
|
for ext_name in extensions {
|
||||||
|
let agent_guard = agent_ref.lock().await;
|
||||||
|
if let Err(e) = agent_guard.remove_extension(&ext_name).await {
|
||||||
|
error!("Failed to remove extension {} during shutdown: {}", ext_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn metrics_handler() -> Result<impl warp::Reply, Rejection> {
|
||||||
|
info!("Getting metrics");
|
||||||
|
|
||||||
|
|
||||||
|
// Gather pending request sizes for each extension
|
||||||
|
let agent_guard = AGENT.lock().await;
|
||||||
|
let pending_requests: HashMap<String, usize> = agent_guard
|
||||||
|
.get_tool_stats()
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, v as usize))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let resp = MetricsResponse {
|
||||||
|
active_sessions: SESSIONS.len(),
|
||||||
|
pending_requests,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(warp::reply::json(&resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_rejection(err: Rejection) -> Result<impl warp::Reply, Rejection> {
|
||||||
|
if let Some(e) = err.find::<AnyhowRejection>() {
|
||||||
|
let message = e.0.to_string();
|
||||||
|
let status_code = if message.contains("Unauthorized") {
|
||||||
|
warp::http::StatusCode::UNAUTHORIZED
|
||||||
|
} else if message.contains("Failed to add extension") || message.contains("Failed to remove extension") {
|
||||||
|
warp::http::StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warp::http::StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = ApiResponse {
|
||||||
|
message,
|
||||||
|
status: "error".to_string(),
|
||||||
|
};
|
||||||
|
let json = warp::reply::json(&response);
|
||||||
|
Ok(warp::reply::with_status(json, status_code))
|
||||||
|
} else {
|
||||||
|
// If it's not a custom rejection, re-reject it
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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| {
|
||||||
|
let api_key = api_key.clone();
|
||||||
|
async move {
|
||||||
|
if header_api_key == api_key {
|
||||||
|
Ok(api_key)
|
||||||
|
} else {
|
||||||
|
warn!("Unauthorized access attempt with API key: {}", header_api_key.to_str().unwrap_or("invalid_header_value"));
|
||||||
|
Err(warp::reject::custom(AnyhowRejection(anyhow::anyhow!("Unauthorized"))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
6
crates/goose-api/src/lib.rs
Normal file
6
crates/goose-api/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod handlers;
|
||||||
|
mod config;
|
||||||
|
mod routes;
|
||||||
|
mod api_sessions;
|
||||||
|
|
||||||
|
pub use routes::{build_routes, run_server};
|
||||||
80
crates/goose-api/src/main.rs
Normal file
80
crates/goose-api/src/main.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use goose_api::run_server;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
// Check if this is being called as an MCP server
|
||||||
|
if args.len() >= 3 && args[1] == "mcp" {
|
||||||
|
let extension_name = &args[2];
|
||||||
|
run_mcp_server(extension_name).await
|
||||||
|
} else {
|
||||||
|
// Run as the main API server
|
||||||
|
run_server().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_mcp_server(extension_name: &str) -> Result<(), anyhow::Error> {
|
||||||
|
use goose_mcp::*;
|
||||||
|
use mcp_server::router::RouterService;
|
||||||
|
use mcp_server::{ByteTransport, Server};
|
||||||
|
use tokio::io::{stdin, stdout};
|
||||||
|
use tracing_subscriber;
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// Route to the appropriate MCP server based on extension name
|
||||||
|
let result = match extension_name {
|
||||||
|
"computercontroller" => {
|
||||||
|
let router = RouterService(ComputerControllerRouter::new());
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
"developer" => {
|
||||||
|
let router = RouterService(DeveloperRouter::new());
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
"memory" => {
|
||||||
|
let router = RouterService(MemoryRouter::new());
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
"google_drive" => {
|
||||||
|
let router = RouterService(GoogleDriveRouter::new().await);
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
"jetbrains" => {
|
||||||
|
let router = RouterService(JetBrainsRouter::new());
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
"tutorial" => {
|
||||||
|
let router = RouterService(TutorialRouter::new());
|
||||||
|
let server = Server::new(router);
|
||||||
|
let transport = ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown MCP extension: {}", extension_name);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("MCP server error for {}: {}", extension_name, e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
150
crates/goose-api/src/routes.rs
Normal file
150
crates/goose-api/src/routes.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use warp::Filter;
|
||||||
|
use tracing::{info, warn, error};
|
||||||
|
|
||||||
|
use crate::handlers::{
|
||||||
|
end_session_handler, get_provider_config_handler, handle_rejection,
|
||||||
|
list_extensions_handler, metrics_handler, reply_session_handler,
|
||||||
|
start_session_handler, summarize_session_handler, with_api_key,
|
||||||
|
};
|
||||||
|
use crate::config::{
|
||||||
|
initialize_provider_config, load_configuration,
|
||||||
|
run_init_tests,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn build_routes(api_key: String) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||||
|
let start_session = warp::path("session")
|
||||||
|
.and(warp::path("start"))
|
||||||
|
.and(warp::post())
|
||||||
|
.and(warp::body::json())
|
||||||
|
.and(with_api_key(api_key.clone()))
|
||||||
|
.and_then(start_session_handler);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let summarize_session = warp::path("session")
|
||||||
|
.and(warp::path("summarize"))
|
||||||
|
.and(warp::post())
|
||||||
|
.and(warp::body::json())
|
||||||
|
.and(with_api_key(api_key.clone()))
|
||||||
|
.and_then(summarize_session_handler);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let list_extensions = warp::path("extensions")
|
||||||
|
.and(warp::path("list"))
|
||||||
|
.and(warp::get())
|
||||||
|
.and_then(list_extensions_handler);
|
||||||
|
|
||||||
|
|
||||||
|
let get_provider_config = warp::path("provider")
|
||||||
|
.and(warp::path("config"))
|
||||||
|
.and(warp::get())
|
||||||
|
.and_then(get_provider_config_handler);
|
||||||
|
|
||||||
|
let metrics = warp::path("metrics")
|
||||||
|
.and(warp::get())
|
||||||
|
.and_then(metrics_handler);
|
||||||
|
|
||||||
|
start_session
|
||||||
|
.or(reply_session)
|
||||||
|
.or(summarize_session)
|
||||||
|
.or(end_session)
|
||||||
|
.or(list_extensions)
|
||||||
|
.or(get_provider_config)
|
||||||
|
.or(metrics)
|
||||||
|
.recover(handle_rejection)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_server() -> Result<(), anyhow::Error> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("Starting goose-api server");
|
||||||
|
|
||||||
|
let api_config = load_configuration()?;
|
||||||
|
|
||||||
|
let api_key_source = if std::env::var("GOOSE_API_KEY").is_ok() {
|
||||||
|
"environment variable"
|
||||||
|
} else if api_config.get_string("api_key").is_ok() {
|
||||||
|
"config file"
|
||||||
|
} else {
|
||||||
|
"default"
|
||||||
|
};
|
||||||
|
info!("API key loaded from: {}", api_key_source);
|
||||||
|
|
||||||
|
let api_key: String = std::env::var("GOOSE_API_KEY")
|
||||||
|
.or_else(|_| api_config.get_string("api_key"))
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
warn!("No API key configured, using default");
|
||||||
|
"default_api_key".to_string()
|
||||||
|
});
|
||||||
|
info!("Using API key: {}", api_key);
|
||||||
|
|
||||||
|
if let Err(e) = initialize_provider_config().await {
|
||||||
|
error!("Failed to initialize provider: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if let Err(e) = run_init_tests().await {
|
||||||
|
error!("Initialization tests failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let routes = build_routes(api_key.clone());
|
||||||
|
|
||||||
|
let host = std::env::var("GOOSE_API_HOST")
|
||||||
|
.or_else(|_| api_config.get_string("host"))
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
let port = std::env::var("GOOSE_API_PORT")
|
||||||
|
.or_else(|_| api_config.get_string("port"))
|
||||||
|
.unwrap_or_else(|_| "8080".to_string())
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or(8080);
|
||||||
|
|
||||||
|
info!("Server binding to {}:{}", host, port);
|
||||||
|
|
||||||
|
let host_parts: Vec<u8> = host
|
||||||
|
.split('.')
|
||||||
|
.map(|part| part.parse::<u8>().unwrap_or(127))
|
||||||
|
.collect();
|
||||||
|
let addr = if host_parts.len() == 4 {
|
||||||
|
[host_parts[0], host_parts[1], host_parts[2], host_parts[3]]
|
||||||
|
} else {
|
||||||
|
[127, 0, 0, 1]
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_addr, server) = warp::serve(routes).bind_with_graceful_shutdown((addr, port), async {
|
||||||
|
tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
|
||||||
|
info!("Received Ctrl+C, initiating graceful shutdown...");
|
||||||
|
|
||||||
|
// Perform cleanup here
|
||||||
|
use crate::handlers::AGENT; // Import AGENT from handlers
|
||||||
|
use tracing::error; // Import error for logging
|
||||||
|
|
||||||
|
let agent_guard = AGENT.lock().await;
|
||||||
|
let extensions = agent_guard.list_extensions().await;
|
||||||
|
drop(agent_guard); // Release lock before async calls
|
||||||
|
|
||||||
|
for ext_name in extensions {
|
||||||
|
let agent_guard = AGENT.lock().await;
|
||||||
|
if let Err(e) = agent_guard.remove_extension(&ext_name).await {
|
||||||
|
error!("Failed to remove extension {} during graceful shutdown: {}", ext_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("Extensions shut down during graceful shutdown.");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.await; // Await the server
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
107
crates/goose-api/src/tests.rs
Normal file
107
crates/goose-api/src/tests.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use goose::message::{Message, MessageContent};
|
||||||
|
use goose::model::ModelConfig;
|
||||||
|
use goose::providers::{
|
||||||
|
base::{Provider, ProviderMetadata, ProviderUsage, Usage},
|
||||||
|
errors::ProviderError,
|
||||||
|
};
|
||||||
|
use mcp_core::tool::Tool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use warp::reply::Reply;
|
||||||
|
use goose::session::{self, Identifier};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use hyper::body;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ContextProvider {
|
||||||
|
model_config: ModelConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Provider for ContextProvider {
|
||||||
|
fn metadata() -> ProviderMetadata {
|
||||||
|
ProviderMetadata::empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_model_config(&self) -> ModelConfig {
|
||||||
|
self.model_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn complete(
|
||||||
|
&self,
|
||||||
|
system: &str,
|
||||||
|
_messages: &[Message],
|
||||||
|
_tools: &[Tool],
|
||||||
|
) -> Result<(Message, ProviderUsage), ProviderError> {
|
||||||
|
if system.contains("summarizing") {
|
||||||
|
Ok((
|
||||||
|
Message::user().with_text("summary"),
|
||||||
|
ProviderUsage::new("mock".to_string(), Usage::default()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(ProviderError::ContextLengthExceeded("too long".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup() -> (TempDir, Uuid) {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
std::env::set_var("HOME", tmp.path());
|
||||||
|
|
||||||
|
let provider = Arc::new(ContextProvider {
|
||||||
|
model_config: ModelConfig::new("test".to_string()),
|
||||||
|
});
|
||||||
|
let agent = AGENT.lock().await;
|
||||||
|
agent.update_provider(provider).await.unwrap();
|
||||||
|
drop(agent);
|
||||||
|
|
||||||
|
let req = SessionRequest {
|
||||||
|
prompt: "start".repeat(1000),
|
||||||
|
};
|
||||||
|
let reply = start_session_handler(req, "key".to_string()).await.unwrap();
|
||||||
|
let resp = reply.into_response();
|
||||||
|
let body = body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
|
let start: StartSessionResponse = serde_json::from_slice(&body).unwrap();
|
||||||
|
(tmp, start.session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_routes_compiles() {
|
||||||
|
let _routes = build_routes("test-key".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn summarizes_large_history_on_start() {
|
||||||
|
let (tmp, session_id) = setup().await;
|
||||||
|
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_id.to_string()));
|
||||||
|
let messages = session::read_messages(&session_path).unwrap();
|
||||||
|
assert!(messages.iter().any(|m| m.as_concat_text().contains("summary")));
|
||||||
|
drop(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn summarizes_large_history_on_reply() {
|
||||||
|
let (tmp, session_id) = setup().await;
|
||||||
|
|
||||||
|
let req = SessionReplyRequest {
|
||||||
|
session_id,
|
||||||
|
prompt: "reply".repeat(1000),
|
||||||
|
};
|
||||||
|
let reply = reply_session_handler(req, "key".to_string()).await.unwrap();
|
||||||
|
let resp = reply.into_response();
|
||||||
|
let body = body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
|
let api: ApiResponse = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(api.status, "warning");
|
||||||
|
|
||||||
|
let session_path = session::get_path(Identifier::Name(session_id.to_string()));
|
||||||
|
let messages = session::read_messages(&session_path).unwrap();
|
||||||
|
assert!(messages
|
||||||
|
.iter()
|
||||||
|
.all(|m| !matches!(m.content.first(), Some(MessageContent::ContextLengthExceeded(_)))));
|
||||||
|
drop(tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
crates/goose-api/test.py
Normal file
98
crates/goose-api/test.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:8080"
|
||||||
|
API_KEY = "default_api_key"
|
||||||
|
HEADERS = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_provider_config():
|
||||||
|
print("\n--- Testing GET /provider/config ---")
|
||||||
|
url = f"{BASE_URL}/provider/config"
|
||||||
|
response = requests.get(url, headers={"x-api-key": API_KEY})
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "provider" in response.json()
|
||||||
|
assert "model" in response.json()
|
||||||
|
|
||||||
|
def test_start_session():
|
||||||
|
print("\n--- Testing POST /session/start ---")
|
||||||
|
url = f"{BASE_URL}/session/start"
|
||||||
|
data = {"prompt": "Create a Python function to generate Fibonacci numbers"}
|
||||||
|
response = requests.post(url, headers=HEADERS, data=json.dumps(data))
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "session_id" in response.json()
|
||||||
|
return response.json().get("session_id")
|
||||||
|
|
||||||
|
def test_reply_session(session_id):
|
||||||
|
print(f"\n--- Testing POST /session/reply for session_id: {session_id} ---")
|
||||||
|
url = f"{BASE_URL}/session/reply"
|
||||||
|
data = {"session_id": session_id, "prompt": "Continue with the next Fibonacci number."}
|
||||||
|
response = requests.post(url, headers=HEADERS, data=json.dumps(data))
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "message" in response.json()
|
||||||
|
|
||||||
|
def test_summarize_session(session_id):
|
||||||
|
print(f"\n--- Testing POST /session/summarize for session_id: {session_id} ---")
|
||||||
|
url = f"{BASE_URL}/session/summarize"
|
||||||
|
data = {"session_id": session_id}
|
||||||
|
response = requests.post(url, headers=HEADERS, data=json.dumps(data))
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "summary" in response.json()
|
||||||
|
|
||||||
|
def test_end_session(session_id):
|
||||||
|
print(f"\n--- Testing POST /session/end for session_id: {session_id} ---")
|
||||||
|
url = f"{BASE_URL}/session/end"
|
||||||
|
data = {"session_id": session_id}
|
||||||
|
response = requests.post(url, headers=HEADERS, data=json.dumps(data))
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "message" in response.json()
|
||||||
|
|
||||||
|
def test_list_extensions():
|
||||||
|
print("\n--- Testing GET /extensions/list ---")
|
||||||
|
url = f"{BASE_URL}/extensions/list"
|
||||||
|
response = requests.get(url, headers=HEADERS) # API key is not enforced for this endpoint, but including for consistency
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "extensions" in response.json()
|
||||||
|
|
||||||
|
def test_get_metrics():
|
||||||
|
print("\n--- Testing GET /metrics ---")
|
||||||
|
url = f"{BASE_URL}/metrics"
|
||||||
|
response = requests.get(url) # No API key required
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "active_sessions" in response.json()
|
||||||
|
assert "pending_requests" in response.json()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Starting API endpoint tests...")
|
||||||
|
|
||||||
|
# Test endpoints that don't require a session_id first
|
||||||
|
test_get_provider_config()
|
||||||
|
test_list_extensions()
|
||||||
|
test_get_metrics()
|
||||||
|
|
||||||
|
# Test session-related endpoints
|
||||||
|
session_id = test_start_session()
|
||||||
|
if session_id:
|
||||||
|
test_reply_session(session_id)
|
||||||
|
test_summarize_session(session_id)
|
||||||
|
test_end_session(session_id)
|
||||||
|
else:
|
||||||
|
print("Skipping session tests as session_id was not obtained.")
|
||||||
|
|
||||||
|
print("\nAll tests completed.")
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
|
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
|
||||||
|
|
||||||
@@ -18,9 +17,6 @@ use crate::transport::TransportMessageRecv;
|
|||||||
|
|
||||||
use super::{serialize_and_send, Error, Transport, TransportHandle};
|
use super::{serialize_and_send, Error, Transport, TransportHandle};
|
||||||
|
|
||||||
// Global to track process groups we've created
|
|
||||||
static PROCESS_GROUP: AtomicI32 = AtomicI32::new(-1);
|
|
||||||
|
|
||||||
/// A `StdioTransport` uses a child process's stdin/stdout as a communication channel.
|
/// A `StdioTransport` uses a child process's stdin/stdout as a communication channel.
|
||||||
///
|
///
|
||||||
/// It uses channels for message passing and handles responses asynchronously through a background task.
|
/// It uses channels for message passing and handles responses asynchronously through a background task.
|
||||||
@@ -32,21 +28,21 @@ pub struct StdioActor {
|
|||||||
stdin: Option<ChildStdin>,
|
stdin: Option<ChildStdin>,
|
||||||
stdout: Option<ChildStdout>,
|
stdout: Option<ChildStdout>,
|
||||||
stderr: Option<ChildStderr>,
|
stderr: Option<ChildStderr>,
|
||||||
|
#[cfg(unix)]
|
||||||
|
pgid: Option<i32>, // Process group ID for cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for StdioActor {
|
impl Drop for StdioActor {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Get the process group ID before attempting cleanup
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(pid) = self.process.id() {
|
if let Some(pgid) = self.pgid {
|
||||||
if let Ok(pgid) = getpgid(Some(Pid::from_raw(pid as i32))) {
|
// Send SIGTERM to the entire process group
|
||||||
// Send SIGTERM to the entire process group
|
let _ = kill(Pid::from_raw(-pgid), Signal::SIGTERM);
|
||||||
let _ = kill(Pid::from_raw(-pgid.as_raw()), Signal::SIGTERM);
|
// Note: std::thread::sleep is blocking, but this is a Drop impl.
|
||||||
// Give processes a moment to cleanup
|
// For graceful async shutdown, use the `close` method on `StdioTransport`.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
// Force kill if still running
|
// Force kill if still running
|
||||||
let _ = kill(Pid::from_raw(-pgid.as_raw()), Signal::SIGKILL);
|
let _ = kill(Pid::from_raw(-pgid), Signal::SIGKILL);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +205,7 @@ impl StdioTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_process(&self) -> Result<(Child, ChildStdin, ChildStdout, ChildStderr), Error> {
|
async fn spawn_process(&self) -> Result<(Child, ChildStdin, ChildStdout, ChildStderr, Option<i32>), Error> {
|
||||||
let mut command = Command::new(&self.command);
|
let mut command = Command::new(&self.command);
|
||||||
command
|
command
|
||||||
.envs(&self.env)
|
.envs(&self.env)
|
||||||
@@ -259,16 +255,16 @@ impl StdioTransport {
|
|||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| Error::StdioProcessError("Failed to get stderr".into()))?;
|
.ok_or_else(|| Error::StdioProcessError("Failed to get stderr".into()))?;
|
||||||
|
|
||||||
|
let mut pgid = None;
|
||||||
// Store the process group ID for cleanup
|
// Store the process group ID for cleanup
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(pid) = process.id() {
|
if let Some(pid) = process.id() {
|
||||||
// Use nix instead of unsafe libc calls
|
if let Ok(id) = getpgid(Some(Pid::from_raw(pid as i32))) {
|
||||||
if let Ok(pgid) = getpgid(Some(Pid::from_raw(pid as i32))) {
|
pgid = Some(id.as_raw());
|
||||||
PROCESS_GROUP.store(pgid.as_raw(), Ordering::SeqCst);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((process, stdin, stdout, stderr))
|
Ok((process, stdin, stdout, stderr, pgid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +273,7 @@ impl Transport for StdioTransport {
|
|||||||
type Handle = StdioTransportHandle;
|
type Handle = StdioTransportHandle;
|
||||||
|
|
||||||
async fn start(&self) -> Result<Self::Handle, Error> {
|
async fn start(&self) -> Result<Self::Handle, Error> {
|
||||||
let (process, stdin, stdout, stderr) = self.spawn_process().await?;
|
let (process, stdin, stdout, stderr, pgid) = self.spawn_process().await?;
|
||||||
let (outbox_tx, outbox_rx) = mpsc::channel(32);
|
let (outbox_tx, outbox_rx) = mpsc::channel(32);
|
||||||
let (inbox_tx, inbox_rx) = mpsc::channel(32);
|
let (inbox_tx, inbox_rx) = mpsc::channel(32);
|
||||||
let (error_tx, error_rx) = mpsc::channel(1);
|
let (error_tx, error_rx) = mpsc::channel(1);
|
||||||
@@ -290,6 +286,8 @@ impl Transport for StdioTransport {
|
|||||||
stdin: Some(stdin),
|
stdin: Some(stdin),
|
||||||
stdout: Some(stdout),
|
stdout: Some(stdout),
|
||||||
stderr: Some(stderr),
|
stderr: Some(stderr),
|
||||||
|
#[cfg(unix)]
|
||||||
|
pgid, // Pass the pgid to the actor
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(actor.run());
|
tokio::spawn(actor.run());
|
||||||
@@ -303,17 +301,8 @@ impl Transport for StdioTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn close(&self) -> Result<(), Error> {
|
async fn close(&self) -> Result<(), Error> {
|
||||||
// Attempt to clean up the process group on close
|
// The StdioActor's Drop implementation handles process termination.
|
||||||
#[cfg(unix)]
|
// This method can be a no-op for now.
|
||||||
if let Some(pgid) = PROCESS_GROUP.load(Ordering::SeqCst).checked_abs() {
|
|
||||||
// Use nix instead of unsafe libc calls
|
|
||||||
// Try SIGTERM first
|
|
||||||
let _ = kill(Pid::from_raw(-pgid), Signal::SIGTERM);
|
|
||||||
// Give processes a moment to cleanup
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
||||||
// Force kill if still running
|
|
||||||
let _ = kill(Pid::from_raw(-pgid), Signal::SIGKILL);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user