mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
add provider bindings MVP to goose-sdk, and add python wheel publishing (#10208)
This commit is contained in:
parent
782f69f175
commit
140158c756
23 changed files with 727 additions and 113 deletions
118
.github/workflows/python-sdk-wheels.yml
vendored
Normal file
118
.github/workflows/python-sdk-wheels.yml
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
name: Python SDK Wheels
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
publish:
|
||||||
|
description: "Publish wheels to PyPI after building"
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-wheels:
|
||||||
|
name: Build wheel (${{ matrix.name }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
container: ${{ matrix.container || null }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: macos-arm64
|
||||||
|
os: macos-14
|
||||||
|
- name: macos-x86_64
|
||||||
|
os: macos-13
|
||||||
|
- name: linux-x86_64
|
||||||
|
os: ubuntu-latest
|
||||||
|
container: quay.io/pypa/manylinux_2_28_x86_64@sha256:441c35fdc6ee809ff9260894f8468ab4fea8c15dc880f8700a3f81b7922c1cda
|
||||||
|
- name: windows-x86_64
|
||||||
|
os: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install Linux tools
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install uv
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
||||||
|
|
||||||
|
- name: Install just
|
||||||
|
if: runner.os != 'Linux'
|
||||||
|
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4
|
||||||
|
|
||||||
|
- name: Cache Cargo artifacts
|
||||||
|
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||||
|
|
||||||
|
- name: Build wheel
|
||||||
|
shell: bash
|
||||||
|
run: just --justfile crates/goose-sdk/justfile python-wheel
|
||||||
|
|
||||||
|
- name: Repair Linux wheel
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 -m pip install auditwheel
|
||||||
|
mkdir -p crates/goose-sdk/python/wheelhouse
|
||||||
|
auditwheel repair --plat manylinux_2_28_x86_64 -w crates/goose-sdk/python/wheelhouse crates/goose-sdk/python/dist/*.whl
|
||||||
|
rm crates/goose-sdk/python/dist/*.whl
|
||||||
|
mv crates/goose-sdk/python/wheelhouse/*.whl crates/goose-sdk/python/dist/
|
||||||
|
|
||||||
|
- name: Smoke test wheel
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m venv .venv-wheel-test
|
||||||
|
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||||
|
python_bin=".venv-wheel-test/Scripts/python.exe"
|
||||||
|
else
|
||||||
|
python_bin=".venv-wheel-test/bin/python"
|
||||||
|
fi
|
||||||
|
UV_NO_CONFIG=1 uv pip install --default-index https://pypi.org/simple --python "$python_bin" crates/goose-sdk/python/dist/*.whl
|
||||||
|
"$python_bin" -c 'import goose; print(goose.__name__)'
|
||||||
|
|
||||||
|
- name: Upload wheel artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
|
with:
|
||||||
|
name: goose-sdk-wheel-${{ matrix.name }}
|
||||||
|
path: crates/goose-sdk/python/dist/*.whl
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish wheels to PyPI
|
||||||
|
needs: build-wheels
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.publish
|
||||||
|
environment: pypi
|
||||||
|
steps:
|
||||||
|
- name: Download wheel artifacts
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
|
with:
|
||||||
|
pattern: goose-sdk-wheel-*
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||||
|
with:
|
||||||
|
packages-dir: dist
|
||||||
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -5065,7 +5065,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-download-manager"
|
name = "goose-download-manager"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -5078,7 +5078,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-local-inference"
|
name = "goose-local-inference"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|
@ -5144,7 +5144,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-provider-types"
|
name = "goose-provider-types"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|
@ -5174,7 +5174,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-providers"
|
name = "goose-providers"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|
@ -5207,11 +5207,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-sdk"
|
name = "goose-sdk"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
|
"anyhow",
|
||||||
|
"futures",
|
||||||
|
"goose-providers",
|
||||||
"goose-sdk-types",
|
"goose-sdk-types",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
@ -5220,7 +5224,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goose-sdk-types"
|
name = "goose-sdk-types"
|
||||||
version = "1.41.0"
|
version = "0.1.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-download-manager"
|
name = "goose-download-manager"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-local-inference"
|
name = "goose-local-inference"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
@ -27,9 +27,9 @@ etcetera = { workspace = true }
|
||||||
encoding_rs = { version = "0.8.35", default-features = false }
|
encoding_rs = { version = "0.8.35", default-features = false }
|
||||||
fs2 = { workspace = true }
|
fs2 = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
goose-download-manager = { path = "../goose-download-manager" }
|
goose-download-manager = { version = "0.1.0-alpha.0", path = "../goose-download-manager" }
|
||||||
goose-provider-types = { path = "../goose-provider-types", default-features = false }
|
goose-provider-types = { version = "0.1.0-alpha.0", path = "../goose-provider-types", default-features = false }
|
||||||
goose-sdk-types = { path = "../goose-sdk-types", default-features = false }
|
goose-sdk-types = { version = "0.1.0-alpha.0", path = "../goose-sdk-types", default-features = false }
|
||||||
hf-hub = { version = "1.0.0-rc.1", default-features = false }
|
hf-hub = { version = "1.0.0-rc.1", default-features = false }
|
||||||
include_dir = { workspace = true }
|
include_dir = { workspace = true }
|
||||||
llama-cpp-2 = { workspace = true }
|
llama-cpp-2 = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-provider-types"
|
name = "goose-provider-types"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-providers"
|
name = "goose-providers"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
@ -35,8 +35,8 @@ anyhow = { workspace = true }
|
||||||
async-stream = { workspace = true }
|
async-stream = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
goose-provider-types = { path = "../goose-provider-types", default-features = false }
|
goose-provider-types = { version = "0.1.0-alpha.0", path = "../goose-provider-types", default-features = false }
|
||||||
goose-local-inference = { path = "../goose-local-inference", default-features = false, optional = true }
|
goose-local-inference = { version = "0.1.0-alpha.0", path = "../goose-local-inference", default-features = false, optional = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rmcp = { workspace = true, features = ["server", "macros"] }
|
rmcp = { workspace = true, features = ["server", "macros"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-sdk-types"
|
name = "goose-sdk-types"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
|
||||||
4
crates/goose-sdk/.gitignore
vendored
4
crates/goose-sdk/.gitignore
vendored
|
|
@ -1,2 +1,6 @@
|
||||||
generated
|
generated
|
||||||
examples/uniffi/*.jar
|
examples/uniffi/*.jar
|
||||||
|
python/build/
|
||||||
|
python/dist/
|
||||||
|
python/src/*.egg-info/
|
||||||
|
python/src/goose/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "goose-sdk"
|
name = "goose-sdk"
|
||||||
version.workspace = true
|
version = "0.1.0-alpha.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
@ -19,15 +19,28 @@ required-features = ["uniffi"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
uniffi = ["dep:uniffi", "dep:thiserror"]
|
uniffi = [
|
||||||
|
"dep:uniffi",
|
||||||
|
"dep:thiserror",
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:goose-providers",
|
||||||
|
"dep:futures",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
goose-sdk-types = { path = "../goose-sdk-types" }
|
goose-sdk-types = { version = "0.1.0-alpha.0", path = "../goose-sdk-types" }
|
||||||
agent-client-protocol = { workspace = true, features = ["unstable"] }
|
agent-client-protocol = { workspace = true, features = ["unstable"] }
|
||||||
agent-client-protocol-schema = { workspace = true }
|
agent-client-protocol-schema = { workspace = true }
|
||||||
|
|
||||||
uniffi = { version = "0.31", features = ["cli"], optional = true }
|
uniffi = { version = "0.31", features = ["cli"], optional = true }
|
||||||
thiserror = { version = "2", optional = true }
|
thiserror = { version = "2", optional = true }
|
||||||
|
goose-providers = { version = "0.1.0-alpha.0", path = "../goose-providers", features = ["rustls-tls"], optional = true }
|
||||||
|
futures = { workspace = true, optional = true }
|
||||||
|
serde_json = { workspace = true, optional = true }
|
||||||
|
tokio = { workspace = true, features = ["rt-multi-thread", "sync"], optional = true }
|
||||||
|
anyhow = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process", "io-std", "io-util"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process", "io-std", "io-util"] }
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,23 @@ The bindings layer for Goose. It houses the shared types used for both ACP and
|
||||||
SDK access, and exposes a cross-language version of the Goose API.
|
SDK access, and exposes a cross-language version of the Goose API.
|
||||||
|
|
||||||
With `--features uniffi` the crate compiles to native bindings for Python and
|
With `--features uniffi` the crate compiles to native bindings for Python and
|
||||||
Kotlin (namespace `aaif_goose` / `aaif.goose`). The published surface is
|
Kotlin (namespace `goose` / `io.aaif.goose`). The UniFFI surface currently lets
|
||||||
currently a `ping` -> `pong` stub in `src/bindings.rs` — the scaffold for the
|
callers construct declarative providers from JSON and stream provider
|
||||||
real implementation.
|
completions.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
just python # build bindings + run examples/uniffi/ping.py
|
just python # build bindings + run examples/uniffi/provider.py
|
||||||
just kotlin # build bindings + run examples/uniffi/Ping.kt
|
just kotlin # build bindings + run examples/uniffi/Provider.kt
|
||||||
```
|
```
|
||||||
|
|
||||||
Both print `pong: aaif.io`.
|
## Python package
|
||||||
|
|
||||||
|
The PyPI package is published as `goose-sdk` and imports as `goose`.
|
||||||
|
Build a local wheel from the repository root with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just --justfile crates/goose-sdk/justfile python-wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
This regenerates the UniFFI Python bindings, copies the release native library
|
||||||
|
into the package, and writes the wheel to `crates/goose-sdk/python/dist/`.
|
||||||
|
|
|
||||||
30
crates/goose-sdk/examples/deepseek.json
Normal file
30
crates/goose-sdk/examples/deepseek.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "deepseek",
|
||||||
|
"engine": "openai",
|
||||||
|
"display_name": "DeepSeek",
|
||||||
|
"description": "Custom DeepSeek provider",
|
||||||
|
"api_key_env": "DEEPSEEK_API_KEY",
|
||||||
|
"base_url": "https://api.deepseek.com",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "deepseek-chat",
|
||||||
|
"context_limit": 128000,
|
||||||
|
"input_token_cost": null,
|
||||||
|
"output_token_cost": null,
|
||||||
|
"currency": null,
|
||||||
|
"supports_cache_control": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deepseek-reasoner",
|
||||||
|
"context_limit": 128000,
|
||||||
|
"input_token_cost": null,
|
||||||
|
"output_token_cost": null,
|
||||||
|
"currency": null,
|
||||||
|
"supports_cache_control": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"headers": null,
|
||||||
|
"timeout_seconds": null,
|
||||||
|
"preserves_thinking": true,
|
||||||
|
"supports_streaming": true
|
||||||
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package aaif.example
|
|
||||||
|
|
||||||
import aaif.goose.Client
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val client = Client()
|
|
||||||
val pong = client.ping("aaif.io")
|
|
||||||
println(pong.message)
|
|
||||||
}
|
|
||||||
31
crates/goose-sdk/examples/uniffi/Provider.kt
Normal file
31
crates/goose-sdk/examples/uniffi/Provider.kt
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package aaif.example
|
||||||
|
|
||||||
|
import io.aaif.goose.DeclarativeProvider
|
||||||
|
import io.aaif.goose.MessageRole
|
||||||
|
import io.aaif.goose.ProviderMessage
|
||||||
|
import io.aaif.goose.ProviderModelConfig
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val examplesDir = Paths.get("crates/goose-sdk/examples")
|
||||||
|
val provider = DeclarativeProvider.fromJson(examplesDir.resolve("deepseek.json").toFile().readText())
|
||||||
|
val model = ProviderModelConfig(modelName = "deepseek-v4-flash")
|
||||||
|
val messages = listOf(
|
||||||
|
ProviderMessage(
|
||||||
|
role = MessageRole.USER,
|
||||||
|
text = "what is the capital of France?",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val stream = provider.stream(
|
||||||
|
model,
|
||||||
|
"You are a knowledgable geography expert",
|
||||||
|
messages,
|
||||||
|
)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val chunk = stream.next() ?: break
|
||||||
|
chunk.text?.let { print(it) }
|
||||||
|
chunk.usageJson?.let { println("\nusage: $it") }
|
||||||
|
}
|
||||||
|
println()
|
||||||
|
}
|
||||||
53
crates/goose-sdk/examples/uniffi/README.md
Normal file
53
crates/goose-sdk/examples/uniffi/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# UniFFI examples
|
||||||
|
|
||||||
|
These examples exercise the in-process Goose SDK UniFFI bindings from Python and Kotlin.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source bin/activate-hermit
|
||||||
|
export DEEPSEEK_API_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generate bindings
|
||||||
|
|
||||||
|
Regenerate the Python and Kotlin bindings before running the examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just --justfile crates/goose-sdk/justfile _generate python
|
||||||
|
just --justfile crates/goose-sdk/justfile _generate kotlin
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes generated bindings and the debug native library under `crates/goose-sdk/generated/`.
|
||||||
|
|
||||||
|
## Python provider example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DYLD_LIBRARY_PATH=target/debug LD_LIBRARY_PATH=target/debug \
|
||||||
|
uv run --script crates/goose-sdk/examples/uniffi/provider.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kotlin provider example
|
||||||
|
|
||||||
|
Download JNA if it is not already present:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSL -o crates/goose-sdk/examples/uniffi/jna.jar \
|
||||||
|
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kotlinc -cp crates/goose-sdk/examples/uniffi/jna.jar -nowarn \
|
||||||
|
crates/goose-sdk/generated/io/aaif/goose/goose.kt \
|
||||||
|
crates/goose-sdk/examples/uniffi/Provider.kt \
|
||||||
|
-include-runtime -d crates/goose-sdk/examples/uniffi/provider.jar
|
||||||
|
|
||||||
|
java -Djna.library.path=target/debug \
|
||||||
|
--enable-native-access=ALL-UNNAMED \
|
||||||
|
-cp crates/goose-sdk/examples/uniffi/provider.jar:crates/goose-sdk/examples/uniffi/jna.jar \
|
||||||
|
aaif.example.ProviderKt
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux, use the same command; `LD_LIBRARY_PATH=target/debug` can also be set if needed. On macOS, `-Djna.library.path=target/debug` is usually enough, but `DYLD_LIBRARY_PATH=target/debug` can also be set if JNA cannot find `libgoose_sdk.dylib`.
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"""Minimal Goose SDK demo: ping the SDK and print the pong."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
|
||||||
sys.path.insert(0, str(HERE.parent.parent / "generated"))
|
|
||||||
|
|
||||||
from aaif_goose import Client # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
client = Client()
|
|
||||||
pong = client.ping("aaif.io")
|
|
||||||
print(pong.message)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
38
crates/goose-sdk/examples/uniffi/provider.py
Executable file
38
crates/goose-sdk/examples/uniffi/provider.py
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
"""Goose SDK demo: build a declarative provider and stream a completion."""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(HERE.parent.parent / "generated"))
|
||||||
|
|
||||||
|
from goose import ( # noqa: E402
|
||||||
|
DeclarativeProvider,
|
||||||
|
MessageRole,
|
||||||
|
ProviderMessage,
|
||||||
|
ProviderModelConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
provider = DeclarativeProvider.from_json((HERE.parent / "deepseek.json").read_text())
|
||||||
|
model = ProviderModelConfig(model_name="deepseek-v4-flash")
|
||||||
|
messages = [ProviderMessage(role=MessageRole.USER, text="what is the capital of France?")]
|
||||||
|
stream = provider.stream(
|
||||||
|
model,
|
||||||
|
"You are a knowledgable geography expert",
|
||||||
|
messages,
|
||||||
|
)
|
||||||
|
|
||||||
|
while chunk := stream.next():
|
||||||
|
if chunk.text:
|
||||||
|
print(chunk.text, end="")
|
||||||
|
if chunk.usage_json:
|
||||||
|
usage = json.loads(chunk.usage_json)
|
||||||
|
print(f"\nusage: {usage}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -2,27 +2,37 @@ set shell := ["bash", "-cu"]
|
||||||
set working-directory := '../..'
|
set working-directory := '../..'
|
||||||
|
|
||||||
lib_ext := if os() == "macos" { "dylib" } else if os() == "windows" { "dll" } else { "so" }
|
lib_ext := if os() == "macos" { "dylib" } else if os() == "windows" { "dll" } else { "so" }
|
||||||
lib_dir := "./target/debug"
|
lib_prefix := if os() == "windows" { "" } else { "lib" }
|
||||||
lib_path := lib_dir / "libgoose_sdk." + lib_ext
|
lib_name := lib_prefix + "goose_sdk." + lib_ext
|
||||||
bindgen := "./target/debug/goose-uniffi-bindgen"
|
debug_lib_dir := "./target/debug"
|
||||||
|
release_lib_dir := "./target/release"
|
||||||
|
debug_lib_path := debug_lib_dir / lib_name
|
||||||
|
release_lib_path := release_lib_dir / lib_name
|
||||||
|
debug_bindgen := "./target/debug/goose-uniffi-bindgen"
|
||||||
|
release_bindgen := "./target/release/goose-uniffi-bindgen"
|
||||||
config := "./crates/goose-sdk/uniffi.toml"
|
config := "./crates/goose-sdk/uniffi.toml"
|
||||||
gen_dir := "./crates/goose-sdk/generated"
|
gen_dir := "./crates/goose-sdk/generated"
|
||||||
examples_dir := "./crates/goose-sdk/examples/uniffi"
|
examples_dir := "./crates/goose-sdk/examples/uniffi"
|
||||||
|
python_dir := "./crates/goose-sdk/python"
|
||||||
|
python_package_dir := python_dir / "src/goose"
|
||||||
|
|
||||||
default:
|
_default:
|
||||||
@just --list --justfile {{justfile()}}
|
@just --list --justfile {{justfile()}}
|
||||||
|
|
||||||
_build:
|
_build:
|
||||||
cargo build -p goose-sdk --features uniffi -q
|
cargo build -p goose-sdk --features uniffi -q
|
||||||
|
|
||||||
|
_build-release:
|
||||||
|
cargo build -p goose-sdk --features uniffi --release -q
|
||||||
|
|
||||||
_generate lang: _build
|
_generate lang: _build
|
||||||
{{bindgen}} generate --library {{lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null
|
{{debug_bindgen}} generate --library {{debug_lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null
|
||||||
cp {{lib_path}} {{gen_dir}}/
|
cp {{debug_lib_path}} {{gen_dir}}/
|
||||||
touch {{gen_dir}}/__init__.py
|
touch {{gen_dir}}/__init__.py
|
||||||
|
|
||||||
python: (_generate "python")
|
python: (_generate "python")
|
||||||
DYLD_LIBRARY_PATH={{lib_dir}} LD_LIBRARY_PATH={{lib_dir}} \
|
DYLD_LIBRARY_PATH={{debug_lib_dir}} LD_LIBRARY_PATH={{debug_lib_dir}} \
|
||||||
python3 {{examples_dir}}/ping.py
|
python3 {{examples_dir}}/provider.py
|
||||||
|
|
||||||
kotlin: (_generate "kotlin")
|
kotlin: (_generate "kotlin")
|
||||||
@if [ ! -f {{examples_dir}}/jna.jar ]; then \
|
@if [ ! -f {{examples_dir}}/jna.jar ]; then \
|
||||||
|
|
@ -30,9 +40,118 @@ kotlin: (_generate "kotlin")
|
||||||
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
|
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
|
||||||
fi
|
fi
|
||||||
kotlinc -cp {{examples_dir}}/jna.jar -nowarn \
|
kotlinc -cp {{examples_dir}}/jna.jar -nowarn \
|
||||||
{{gen_dir}}/aaif/goose/aaif_goose.kt \
|
{{gen_dir}}/io/aaif/goose/goose.kt \
|
||||||
{{examples_dir}}/Ping.kt \
|
{{examples_dir}}/Provider.kt \
|
||||||
-include-runtime -d {{examples_dir}}/ping.jar 2>/dev/null
|
-include-runtime -d {{examples_dir}}/provider.jar 2>/dev/null
|
||||||
java -Djna.library.path={{lib_dir}} \
|
java -Djna.library.path={{debug_lib_dir}} \
|
||||||
--enable-native-access=ALL-UNNAMED \
|
--enable-native-access=ALL-UNNAMED \
|
||||||
-cp {{examples_dir}}/ping.jar:{{examples_dir}}/jna.jar aaif.example.PingKt
|
-cp {{examples_dir}}/provider.jar:{{examples_dir}}/jna.jar aaif.example.ProviderKt
|
||||||
|
|
||||||
|
python-bindings profile="debug":
|
||||||
|
@case "{{profile}}" in \
|
||||||
|
debug) cargo build -p goose-sdk --features uniffi -q; bindgen={{debug_bindgen}}; lib_path={{debug_lib_path}} ;; \
|
||||||
|
release) cargo build -p goose-sdk --features uniffi --release -q; bindgen={{release_bindgen}}; lib_path={{release_lib_path}} ;; \
|
||||||
|
*) echo 'profile must be debug or release' >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
rm -rf {{python_package_dir}}; \
|
||||||
|
mkdir -p {{python_package_dir}}; \
|
||||||
|
"$bindgen" generate --library "$lib_path" --config {{config}} --language python --no-format --out-dir {{python_package_dir}} 2>/dev/null; \
|
||||||
|
mv {{python_package_dir}}/goose.py {{python_package_dir}}/__init__.py; \
|
||||||
|
cp "$lib_path" {{python_package_dir}}/; \
|
||||||
|
touch {{python_package_dir}}/py.typed
|
||||||
|
|
||||||
|
python-wheel: (python-bindings "release")
|
||||||
|
rm -rf {{python_dir}}/build {{python_dir}}/dist {{python_dir}}/*.egg-info {{python_dir}}/src/*.egg-info
|
||||||
|
cd {{python_dir}} && UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple --from build pyproject-build --wheel
|
||||||
|
|
||||||
|
python-check: python-wheel
|
||||||
|
python3 -m pip install --force-reinstall {{python_dir}}/dist/*.whl
|
||||||
|
python3 -c 'import goose; print(goose.__name__)'
|
||||||
|
|
||||||
|
python-publish repository="pypi": python-wheel
|
||||||
|
UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine check {{python_dir}}/dist/*.whl
|
||||||
|
@if [ "{{repository}}" = "pypi" ]; then \
|
||||||
|
UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine upload {{python_dir}}/dist/*.whl; \
|
||||||
|
else \
|
||||||
|
UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine upload --repository {{repository}} {{python_dir}}/dist/*.whl; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
crates-publish dry_run="true":
|
||||||
|
@set -euo pipefail; \
|
||||||
|
dry_run_flag=""; \
|
||||||
|
if [ "{{dry_run}}" = "true" ]; then \
|
||||||
|
dry_run_flag="--dry-run"; \
|
||||||
|
elif [ "{{dry_run}}" != "false" ]; then \
|
||||||
|
echo 'dry_run must be true or false' >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
for crate in \
|
||||||
|
goose-provider-types \
|
||||||
|
goose-sdk-types \
|
||||||
|
goose-download-manager \
|
||||||
|
goose-local-inference \
|
||||||
|
goose-providers \
|
||||||
|
goose-sdk; do \
|
||||||
|
cargo publish -p "$crate" $dry_run_flag --allow-dirty; \
|
||||||
|
done
|
||||||
|
|
||||||
|
bump-version rust_version:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
python3 - "{{rust_version}}" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
rust_version = sys.argv[1]
|
||||||
|
match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-alpha\.(\d+))?", rust_version)
|
||||||
|
if not match:
|
||||||
|
raise SystemExit("rust_version must look like 0.1.0 or 0.1.0-alpha.0")
|
||||||
|
|
||||||
|
major, minor, patch, alpha = match.groups()
|
||||||
|
python_version = f"{major}.{minor}.{patch}" if alpha is None else f"{major}.{minor}.{patch}a{alpha}"
|
||||||
|
|
||||||
|
crate_names = [
|
||||||
|
"goose-provider-types",
|
||||||
|
"goose-sdk-types",
|
||||||
|
"goose-download-manager",
|
||||||
|
"goose-local-inference",
|
||||||
|
"goose-providers",
|
||||||
|
"goose-sdk",
|
||||||
|
]
|
||||||
|
crate_paths = {name: Path("crates") / name / "Cargo.toml" for name in crate_names}
|
||||||
|
|
||||||
|
def replace_unique(path: Path, pattern: str, replacement: str) -> None:
|
||||||
|
text = path.read_text()
|
||||||
|
new_text, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE)
|
||||||
|
if count != 1:
|
||||||
|
raise SystemExit(f"expected one match for {pattern!r} in {path}")
|
||||||
|
path.write_text(new_text)
|
||||||
|
|
||||||
|
for path in crate_paths.values():
|
||||||
|
replace_unique(path, r'^version\s*=\s*"[^"]+"', f'version = "{rust_version}"')
|
||||||
|
|
||||||
|
dependency_names = [
|
||||||
|
"goose-provider-types",
|
||||||
|
"goose-sdk-types",
|
||||||
|
"goose-download-manager",
|
||||||
|
"goose-local-inference",
|
||||||
|
"goose-providers",
|
||||||
|
]
|
||||||
|
for path in crate_paths.values():
|
||||||
|
text = path.read_text()
|
||||||
|
for dependency in dependency_names:
|
||||||
|
text = re.sub(
|
||||||
|
rf'({re.escape(dependency)}\s*=\s*[^\n]*version\s*=\s*)"[^"]+"',
|
||||||
|
rf'\1"{rust_version}"',
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
path.write_text(text)
|
||||||
|
|
||||||
|
pyproject = Path("crates/goose-sdk/python/pyproject.toml")
|
||||||
|
replace_unique(pyproject, r'^version\s*=\s*"[^"]+"', f'version = "{python_version}"')
|
||||||
|
|
||||||
|
print(f"Rust crates: {rust_version}")
|
||||||
|
print(f"Python package: {python_version}")
|
||||||
|
PY
|
||||||
|
cargo fmt --all
|
||||||
|
|
|
||||||
15
crates/goose-sdk/python/README.md
Normal file
15
crates/goose-sdk/python/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# goose-sdk
|
||||||
|
|
||||||
|
Python bindings for the Goose SDK.
|
||||||
|
|
||||||
|
This package is generated from the Rust `goose-sdk` crate using UniFFI.
|
||||||
|
|
||||||
|
## Build a local wheel
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just --justfile crates/goose-sdk/justfile python-wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
The wheel is written to `crates/goose-sdk/python/dist/`.
|
||||||
34
crates/goose-sdk/python/pyproject.toml
Normal file
34
crates/goose-sdk/python/pyproject.toml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "goose-sdk"
|
||||||
|
version = "0.1.0a0"
|
||||||
|
description = "Python bindings for the Goose SDK"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
authors = [{ name = "AAIF", email = "ai-oss-tools@block.xyz" }]
|
||||||
|
keywords = ["goose", "sdk", "ai", "agent", "uniffi"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/aaif-goose/goose"
|
||||||
|
Repository = "https://github.com/aaif-goose/goose"
|
||||||
|
Issues = "https://github.com/aaif-goose/goose/issues"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = { "" = "src" }
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
goose = ["*.so", "*.dylib", "*.dll", "py.typed"]
|
||||||
15
crates/goose-sdk/python/setup.py
Normal file
15
crates/goose-sdk/python/setup.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from setuptools import setup
|
||||||
|
from wheel.bdist_wheel import bdist_wheel
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryWheel(bdist_wheel):
|
||||||
|
def finalize_options(self):
|
||||||
|
super().finalize_options()
|
||||||
|
self.root_is_pure = False
|
||||||
|
|
||||||
|
def get_tag(self):
|
||||||
|
_, _, platform_tag = super().get_tag()
|
||||||
|
return "py3", "none", platform_tag
|
||||||
|
|
||||||
|
|
||||||
|
setup(cmdclass={"bdist_wheel": BinaryWheel})
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
//! In-process uniffi bindings for the Goose SDK.
|
//! In-process uniffi bindings for the Goose SDK.
|
||||||
//!
|
//!
|
||||||
//! This is the published API surface exposed to Python and Kotlin. Right now it
|
//! This is the API surface exposed to Python and Kotlin. It currently focuses
|
||||||
//! is a minimal `ping` -> `pong` round-trip that proves the uniffi
|
//! on declarative providers: consumers can construct a provider from JSON and
|
||||||
//! infrastructure end to end without depending on the `goose` core crate.
|
//! stream completions from it.
|
||||||
//!
|
|
||||||
//! To build the real SDK, add `goose` (and whatever else you need) as
|
|
||||||
//! dependencies and replace the [`Client`] methods below with the actual
|
|
||||||
//! agent surface.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use goose_providers::{
|
||||||
|
base::{MessageStream, Provider},
|
||||||
|
conversation::message::Message,
|
||||||
|
declarative::EnvKeyResolver,
|
||||||
|
model::ModelConfig,
|
||||||
|
};
|
||||||
|
|
||||||
/// Errors surfaced across the uniffi boundary.
|
/// Errors surfaced across the uniffi boundary.
|
||||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||||
|
|
@ -17,36 +21,180 @@ pub enum GooseError {
|
||||||
Generic(String),
|
Generic(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reply to a [`Client::ping`] call.
|
impl From<anyhow::Error> for GooseError {
|
||||||
#[derive(Debug, Clone, uniffi::Record)]
|
fn from(error: anyhow::Error) -> Self {
|
||||||
pub struct Pong {
|
Self::Generic(error.to_string())
|
||||||
/// Echo of the message that was pinged.
|
}
|
||||||
pub message: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The top-level entry point for the Goose SDK.
|
impl From<goose_providers::errors::ProviderError> for GooseError {
|
||||||
///
|
fn from(error: goose_providers::errors::ProviderError) -> Self {
|
||||||
/// This is the object that consuming languages instantiate. Today it only knows
|
Self::Generic(error.to_string())
|
||||||
/// how to answer a ping; extend it with the real agent API.
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for GooseError {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Self::Generic(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text message passed to a provider.
|
||||||
|
#[derive(Debug, Clone, uniffi::Record)]
|
||||||
|
pub struct ProviderMessage {
|
||||||
|
pub role: MessageRole,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported message roles for provider requests and streamed responses.
|
||||||
|
#[derive(Debug, Clone, uniffi::Enum)]
|
||||||
|
pub enum MessageRole {
|
||||||
|
User,
|
||||||
|
Assistant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProviderMessage {
|
||||||
|
fn to_goose_message(&self) -> Message {
|
||||||
|
match self.role {
|
||||||
|
MessageRole::User => Message::user().with_text(&self.text),
|
||||||
|
MessageRole::Assistant => Message::assistant().with_text(&self.text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Model selection and optional generation settings for a provider request.
|
||||||
|
#[derive(Debug, Clone, uniffi::Record)]
|
||||||
|
pub struct ProviderModelConfig {
|
||||||
|
pub model_name: String,
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub context_limit: Option<u64>,
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub max_tokens: Option<i32>,
|
||||||
|
#[uniffi(default = false)]
|
||||||
|
pub toolshim: bool,
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub toolshim_model: Option<String>,
|
||||||
|
/// Provider-specific request parameters as a JSON object string.
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub request_params_json: Option<String>,
|
||||||
|
#[uniffi(default = None)]
|
||||||
|
pub reasoning: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProviderModelConfig {
|
||||||
|
fn to_goose_model_config(&self) -> Result<ModelConfig, GooseError> {
|
||||||
|
let mut config = ModelConfig::new(&self.model_name)
|
||||||
|
.with_context_limit(self.context_limit.map(|limit| limit as usize))
|
||||||
|
.with_temperature(self.temperature)
|
||||||
|
.with_max_tokens(self.max_tokens)
|
||||||
|
.with_toolshim(self.toolshim)
|
||||||
|
.with_toolshim_model(self.toolshim_model.clone());
|
||||||
|
|
||||||
|
if let Some(request_params_json) = &self.request_params_json {
|
||||||
|
let request_params = serde_json::from_str(request_params_json)?;
|
||||||
|
config = config.with_merged_request_params(request_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.reasoning = self.reasoning;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One item yielded by a provider stream.
|
||||||
|
#[derive(Debug, Clone, uniffi::Record)]
|
||||||
|
pub struct ProviderStreamChunk {
|
||||||
|
/// The concatenated text content in this message chunk, if one was emitted.
|
||||||
|
pub text: Option<String>,
|
||||||
|
/// Full Goose message JSON for callers that need non-text content such as tool requests.
|
||||||
|
pub message_json: Option<String>,
|
||||||
|
/// Provider usage JSON when the provider emits usage metadata.
|
||||||
|
pub usage_json: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A declarative Goose provider constructed from provider JSON.
|
||||||
#[derive(uniffi::Object)]
|
#[derive(uniffi::Object)]
|
||||||
pub struct Client {}
|
pub struct DeclarativeProvider {
|
||||||
|
provider: Box<dyn Provider>,
|
||||||
|
runtime: Arc<tokio::runtime::Runtime>,
|
||||||
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
impl Client {
|
impl DeclarativeProvider {
|
||||||
|
/// Construct a declarative provider using the process environment to resolve
|
||||||
|
/// configured API key environment variables.
|
||||||
#[uniffi::constructor]
|
#[uniffi::constructor]
|
||||||
pub fn new() -> Arc<Self> {
|
pub fn from_json(json: String) -> Result<Arc<Self>, GooseError> {
|
||||||
Arc::new(Self {})
|
let provider = goose_providers::declarative::from_json(&json, None, EnvKeyResolver {})?;
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.map_err(|error| GooseError::Generic(error.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
provider,
|
||||||
|
runtime: Arc::new(runtime),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Round-trip a message through the SDK. Returns a [`Pong`] echoing the
|
pub fn name(&self) -> String {
|
||||||
/// supplied `message`, prefixed with `pong: `.
|
self.provider.get_name().to_string()
|
||||||
pub fn ping(&self, message: String) -> Result<Pong, GooseError> {
|
}
|
||||||
if message.is_empty() {
|
|
||||||
return Err(GooseError::Generic("message must not be empty".into()));
|
/// Start a streaming completion request. Tools are not yet exposed over the
|
||||||
}
|
/// uniffi boundary, so this calls providers with an empty tool list.
|
||||||
Ok(Pong {
|
pub fn stream(
|
||||||
message: format!("pong: {message}"),
|
&self,
|
||||||
})
|
model: ProviderModelConfig,
|
||||||
|
system: String,
|
||||||
|
messages: Vec<ProviderMessage>,
|
||||||
|
) -> Result<Arc<DeclarativeProviderStream>, GooseError> {
|
||||||
|
let model = model.to_goose_model_config()?;
|
||||||
|
let messages = messages
|
||||||
|
.iter()
|
||||||
|
.map(ProviderMessage::to_goose_message)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let stream =
|
||||||
|
self.runtime
|
||||||
|
.block_on(self.provider.stream(&model, &system, &messages, &[]))?;
|
||||||
|
|
||||||
|
Ok(Arc::new(DeclarativeProviderStream {
|
||||||
|
stream: Mutex::new(stream),
|
||||||
|
runtime: Arc::clone(&self.runtime),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A blocking iterator over provider stream chunks.
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct DeclarativeProviderStream {
|
||||||
|
stream: Mutex<MessageStream>,
|
||||||
|
runtime: Arc<tokio::runtime::Runtime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl DeclarativeProviderStream {
|
||||||
|
/// Return the next stream chunk, or `None` when the stream is exhausted.
|
||||||
|
pub fn next(&self) -> Result<Option<ProviderStreamChunk>, GooseError> {
|
||||||
|
let mut stream = self
|
||||||
|
.stream
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| GooseError::Generic("provider stream lock poisoned".to_string()))?;
|
||||||
|
|
||||||
|
let Some((message, usage)) = self.runtime.block_on(stream.next()).transpose()? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = message.as_ref().map(Message::as_concat_text);
|
||||||
|
let message_json = message.as_ref().map(serde_json::to_string).transpose()?;
|
||||||
|
let usage_json = usage.as_ref().map(serde_json::to_string).transpose()?;
|
||||||
|
|
||||||
|
Ok(Some(ProviderStreamChunk {
|
||||||
|
text,
|
||||||
|
message_json,
|
||||||
|
usage_json,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,15 +203,29 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ping_returns_pong() {
|
fn model_config_rejects_invalid_request_params_json() {
|
||||||
let client = Client::new();
|
let config = ProviderModelConfig {
|
||||||
let pong = client.ping("aaif.io".into()).expect("ping should succeed");
|
model_name: "test".to_string(),
|
||||||
assert_eq!(pong.message, "pong: aaif.io");
|
context_limit: None,
|
||||||
|
temperature: None,
|
||||||
|
max_tokens: None,
|
||||||
|
toolshim: false,
|
||||||
|
toolshim_model: None,
|
||||||
|
request_params_json: Some("not json".to_string()),
|
||||||
|
reasoning: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.to_goose_model_config().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_ping_errors() {
|
fn provider_message_converts_user_text() {
|
||||||
let client = Client::new();
|
let message = ProviderMessage {
|
||||||
assert!(client.ping(String::new()).is_err());
|
role: MessageRole::User,
|
||||||
|
text: "what is the capital of France?".to_string(),
|
||||||
|
}
|
||||||
|
.to_goose_message();
|
||||||
|
|
||||||
|
assert_eq!(message.as_concat_text(), "what is the capital of France?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,15 @@
|
||||||
//! that talks to `goose acp` over stdio.
|
//! that talks to `goose acp` over stdio.
|
||||||
//!
|
//!
|
||||||
//! With `--features uniffi` the crate additionally compiles as a
|
//! With `--features uniffi` the crate additionally compiles as a
|
||||||
//! `cdylib`/`staticlib` and exposes a small in-process API to Python and Kotlin
|
//! `cdylib`/`staticlib` and exposes an in-process API to Python and Kotlin via
|
||||||
//! via [uniffi-rs](https://github.com/mozilla/uniffi-rs).
|
//! [uniffi-rs](https://github.com/mozilla/uniffi-rs). The current uniffi surface
|
||||||
//!
|
//! lets callers construct declarative providers from JSON and stream provider
|
||||||
//! The published uniffi surface is intentionally a single `ping` -> `pong`
|
//! completions.
|
||||||
//! round-trip. It exists as a working scaffold for adding the real Goose SDK
|
|
||||||
//! API: replace [`bindings`] with the actual implementation.
|
|
||||||
|
|
||||||
pub use goose_sdk_types::{custom_notifications, custom_requests};
|
pub use goose_sdk_types::{custom_notifications, custom_requests};
|
||||||
|
|
||||||
#[cfg(feature = "uniffi")]
|
#[cfg(feature = "uniffi")]
|
||||||
uniffi::setup_scaffolding!("aaif_goose");
|
uniffi::setup_scaffolding!("goose");
|
||||||
|
|
||||||
#[cfg(feature = "uniffi")]
|
#[cfg(feature = "uniffi")]
|
||||||
pub mod bindings;
|
pub mod bindings;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
[bindings.kotlin]
|
[bindings.kotlin]
|
||||||
package_name = "aaif.goose"
|
package_name = "io.aaif.goose"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue