add build and maven publish for kotlin ffi (#10375)

This commit is contained in:
Jack Amadeo
2026-07-14 12:55:57 -04:00
committed by GitHub
parent f47cebf855
commit 376a9e54b6
23 changed files with 796 additions and 125 deletions
+173
View File
@@ -0,0 +1,173 @@
name: Maven SDK
on:
workflow_dispatch:
inputs:
publish:
description: "Publish gdk to Maven Central after building"
required: true
default: false
type: boolean
permissions:
contents: read
jobs:
build-native:
name: Build native library (${{ matrix.name }})
runs-on: ${{ matrix.os }}
container: ${{ matrix.container || null }}
strategy:
fail-fast: false
matrix:
include:
- name: darwin-aarch64
os: macos-14
resource-prefix: darwin-aarch64
- name: linux-x86-64
os: ubuntu-latest
container: quay.io/pypa/manylinux_2_28_x86_64@sha256:441c35fdc6ee809ff9260894f8468ab4fea8c15dc880f8700a3f81b7922c1cda
resource-prefix: linux-x86-64
- name: win32-x86-64
os: windows-latest
resource-prefix: win32-x86-64
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Build native library
shell: bash
run: cargo build -p goose-sdk --features uniffi --release
- name: Stage native library
shell: bash
run: |
case "${{ runner.os }}" in
Linux) lib="target/release/libgoose_sdk.so" ;;
macOS) lib="target/release/libgoose_sdk.dylib" ;;
Windows) lib="target/release/goose_sdk.dll" ;;
*) echo "unsupported runner OS: ${{ runner.os }}" >&2; exit 1 ;;
esac
mkdir -p "maven-native/${{ matrix.resource-prefix }}"
cp "$lib" "maven-native/${{ matrix.resource-prefix }}/"
- name: Upload native library artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: goose-sdk-native-${{ matrix.name }}
path: maven-native/**
if-no-files-found: error
package:
name: Package Maven artifact
needs: build-native
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
- name: Set up Java
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
distribution: temurin
java-version: "17"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
- name: Download native library artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: goose-sdk-native-*
path: native-artifacts
merge-multiple: true
- name: Generate Kotlin bindings
shell: bash
run: |
cargo build -p goose-sdk --features uniffi --release
crates/goose-sdk/scripts/prepare-maven-package.sh \
target/release/libgoose_sdk.so \
linux-x86-64
- name: Restore downloaded native libraries
shell: bash
run: cp -R native-artifacts/* crates/goose-sdk/maven/src/main/resources/
- name: Build Maven publication
working-directory: crates/goose-sdk/maven
run: gradle --no-daemon publishToMavenLocal
- name: Stage Maven local artifacts
shell: bash
run: |
version=$(grep -m1 '^version =' crates/goose-sdk/Cargo.toml | sed -E 's/version = "([^"]+)"/\1/')
mkdir -p maven-artifacts
cp -R "$HOME/.m2/repository/io/github/aaif-goose/gdk/$version" maven-artifacts/
- name: Upload Maven artifact bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: goose-sdk-maven
path: maven-artifacts/**
if-no-files-found: error
publish:
name: Publish to Maven Central
needs: package
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && inputs.publish
environment: maven-central
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
- name: Set up Java
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
with:
distribution: temurin
java-version: "17"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
- name: Download native library artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: goose-sdk-native-*
path: native-artifacts
merge-multiple: true
- name: Generate Kotlin bindings
shell: bash
run: |
cargo build -p goose-sdk --features uniffi --release
crates/goose-sdk/scripts/prepare-maven-package.sh \
target/release/libgoose_sdk.so \
linux-x86-64
- name: Restore downloaded native libraries
shell: bash
run: cp -R native-artifacts/* crates/goose-sdk/maven/src/main/resources/
- name: Publish to Maven Central
working-directory: crates/goose-sdk/maven
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
run: gradle --no-daemon publishAndReleaseToMavenCentral
+11
View File
@@ -84,3 +84,14 @@ result
# Goose self-test artifacts
gooseselftest/
.tasks/
# Generated goose-sdk Maven bindings and native libraries
/crates/goose-sdk/maven/src/main/
/crates/goose-sdk/maven/.gradle/
/crates/goose-sdk/maven/.kotlin/
/crates/goose-sdk/maven/build/
# Generated goose-sdk Kotlin example build artifacts
/crates/goose-sdk/examples/uniffi/kotlin/.gradle/
/crates/goose-sdk/examples/uniffi/kotlin/.kotlin/
/crates/goose-sdk/examples/uniffi/kotlin/build/
+15 -2
View File
@@ -4,13 +4,13 @@ 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.
With `--features uniffi` the crate compiles to native bindings for Python and
Kotlin (namespace `goose` / `io.aaif.goose`). The UniFFI surface currently lets
Kotlin (namespace `goose` / `io.github.aaif_goose`). The UniFFI surface currently lets
callers construct declarative providers from JSON and stream provider
completions.
```bash
just python # build bindings + run examples/uniffi/provider.py
just kotlin # build bindings + run examples/uniffi/Provider.kt
just kotlin # build the Maven artifact + run examples/uniffi/kotlin
```
## Python package
@@ -24,3 +24,16 @@ 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/`.
## Maven package
The Maven Central artifact is published as `io.github.aaif-goose:gdk` and uses
the Rust crate version from `crates/goose-sdk/Cargo.toml`.
```bash
just --justfile crates/goose-sdk/justfile maven-package
```
This regenerates the UniFFI Kotlin bindings and packages them with the native
library in a JVM jar. CI builds the native libraries for supported platforms and
can optionally publish the combined artifact to Maven Central.
@@ -1,31 +0,0 @@
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()
}
+22 -17
View File
@@ -6,16 +6,26 @@ These examples exercise the in-process Goose SDK UniFFI bindings from Python and
```bash
source bin/activate-hermit
```
The Python example uses the declarative DeepSeek provider:
```bash
export DEEPSEEK_API_KEY=...
```
The Kotlin/JVM Maven smoke test uses the native OpenAI provider:
```bash
export OPENAI_API_KEY=...
```
## Generate bindings
Regenerate the Python and Kotlin bindings before running the examples:
Regenerate the Python bindings before running the Python example:
```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/`.
@@ -27,27 +37,22 @@ DYLD_LIBRARY_PATH=target/debug LD_LIBRARY_PATH=target/debug \
uv run --script crates/goose-sdk/examples/uniffi/provider.py
```
## Kotlin provider example
## Kotlin/JVM Maven smoke test
Download JNA if it is not already present:
Build the local Maven artifact and run the downstream smoke test app:
```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
just --justfile crates/goose-sdk/justfile maven-package
cd crates/goose-sdk/examples/uniffi/kotlin
gradle --no-daemon run
```
Compile and run:
Or run the same flow through the goose-sdk justfile:
```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
just --justfile crates/goose-sdk/justfile kotlin
```
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`.
The Kotlin example consumes the local Maven artifact `io.github.aaif-goose:gdk` from `mavenLocal()` and imports the generated package namespace `io.github.aaif_goose`.
On newer JDKs, the example enables native access with `--enable-native-access=ALL-UNNAMED` because the SDK uses JNA to load the bundled native library.
@@ -0,0 +1,31 @@
# Kotlin/JVM GDK smoke test
This is a small downstream Kotlin/JVM app that consumes the Maven artifact
`io.github.aaif-goose:gdk` from `mavenLocal()`.
From the repository root, first build and publish the Maven artifact locally:
```bash
source bin/activate-hermit
just --justfile crates/goose-sdk/justfile maven-package
```
Then run the smoke test:
```bash
cd crates/goose-sdk/examples/uniffi/kotlin
gradle --no-daemon run
```
Set `DATABRICKS_HOST` and `DATABRICKS_TOKEN` before running the example.
`DATABRICKS_HOST` should be the Databricks workspace URL, for example
`https://dbc-xxxxxxxx-xxxx.cloud.databricks.com`. The example uses the native
GDK `DatabricksProvider`, not the declarative JSON provider. The expected output
is a streamed completion from Databricks followed by optional usage metadata.
The important failure to watch for is `UnsatisfiedLinkError` or a missing native
library resource, which would mean the bundled native library was not loaded
correctly.
The example sets `--enable-native-access=ALL-UNNAMED` because JNA loads the
bundled Goose native library. Newer JDKs warn when native access is not enabled
explicitly, and future JDKs may require it.
@@ -0,0 +1,34 @@
plugins {
kotlin("jvm") version "2.2.21"
application
}
fun gooseSdkVersion(): String {
val cargoToml = file("../../../Cargo.toml").readText()
return Regex("(?m)^version\\s*=\\s*\"([^\"]+)\"")
.find(cargoToml)
?.groupValues
?.get(1)
?: error("Could not find goose-sdk version in ../../../Cargo.toml")
}
dependencies {
implementation("io.github.aaif-goose:gdk:${gooseSdkVersion()}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
application {
mainClass.set("MainKt")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenLocal()
mavenCentral()
}
}
rootProject.name = "goose-sdk-kotlin-smoke-test"
@@ -0,0 +1,35 @@
import io.github.aaif_goose.MessageRole
import io.github.aaif_goose.ProviderMessage
import io.github.aaif_goose.ProviderModelConfig
import io.github.aaif_goose.streamFlow
import io.github.aaif_goose.providers.openai.defaultModel
import io.github.aaif_goose.providers.openai.provider as openAiProvider
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val apiKey = System.getenv("OPENAI_API_KEY")
require(!apiKey.isNullOrBlank()) {
"Set OPENAI_API_KEY before running this example."
}
val provider = openAiProvider(apiKey)
val model = ProviderModelConfig(modelName = defaultModel())
val messages = listOf(
ProviderMessage(
role = MessageRole.USER,
text = "What is the capital of France? Answer in one sentence.",
),
)
provider
.streamFlow(
model,
"You are a knowledgeable geography expert.",
messages,
)
.collect { chunk ->
chunk.text?.let { print(it) }
chunk.usageJson?.let { println("\nusage: $it") }
}
println()
}
+7 -6
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env -S uv run --script
"""Goose SDK demo: build a declarative provider and stream a completion."""
import asyncio
import json
import sys
from pathlib import Path
@@ -8,24 +9,24 @@ HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent.parent / "generated"))
from goose import ( # noqa: E402
DeclarativeProvider,
MessageRole,
ProviderMessage,
ProviderModelConfig,
declarative_provider_from_json,
)
def main() -> None:
provider = DeclarativeProvider.from_json((HERE.parent / "deepseek.json").read_text())
async def main() -> None:
provider = declarative_provider_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(
stream = await provider.stream(
model,
"You are a knowledgable geography expert",
messages,
)
while chunk := stream.next():
while chunk := await stream.next():
if chunk.text:
print(chunk.text, end="")
if chunk.usage_json:
@@ -35,4 +36,4 @@ def main() -> None:
if __name__ == "__main__":
main()
asyncio.run(main())
+20 -12
View File
@@ -15,6 +15,9 @@ gen_dir := "./crates/goose-sdk/generated"
examples_dir := "./crates/goose-sdk/examples/uniffi"
python_dir := "./crates/goose-sdk/python"
python_package_dir := python_dir / "src/goose"
maven_dir := "./crates/goose-sdk/maven"
maven_resources_dir := maven_dir / "src/main/resources"
maven_kotlin_dir := maven_dir / "src/main/kotlin"
_default:
@just --list --justfile {{justfile()}}
@@ -34,18 +37,8 @@ python: (_generate "python")
DYLD_LIBRARY_PATH={{debug_lib_dir}} LD_LIBRARY_PATH={{debug_lib_dir}} \
python3 {{examples_dir}}/provider.py
kotlin: (_generate "kotlin")
@if [ ! -f {{examples_dir}}/jna.jar ]; then \
curl -sSL -o {{examples_dir}}/jna.jar \
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
fi
kotlinc -cp {{examples_dir}}/jna.jar -nowarn \
{{gen_dir}}/io/aaif/goose/goose.kt \
{{examples_dir}}/Provider.kt \
-include-runtime -d {{examples_dir}}/provider.jar 2>/dev/null
java -Djna.library.path={{debug_lib_dir}} \
--enable-native-access=ALL-UNNAMED \
-cp {{examples_dir}}/provider.jar:{{examples_dir}}/jna.jar aaif.example.ProviderKt
kotlin: maven-package
cd {{examples_dir}}/kotlin && gradle --no-daemon run
python-bindings profile="debug":
@case "{{profile}}" in \
@@ -76,6 +69,20 @@ 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 upload --repository {{repository}} {{python_dir}}/dist/*.whl; \
fi
maven-bindings profile="release":
@case "{{profile}}" in \
debug) cargo build -p goose-sdk --features uniffi -q; lib_path={{debug_lib_path}} ;; \
release) cargo build -p goose-sdk --features uniffi --release -q; lib_path={{release_lib_path}} ;; \
*) echo 'profile must be debug or release' >&2; exit 1 ;; \
esac; \
crates/goose-sdk/scripts/prepare-maven-package.sh "$lib_path"
maven-package: (maven-bindings "release")
cd {{maven_dir}} && ./gradlew --no-daemon publishToMavenLocal
maven-publish: (maven-bindings "release")
cd {{maven_dir}} && ./gradlew --no-daemon publishAndReleaseToMavenCentral
crates-publish dry_run="true":
@set -euo pipefail; \
dry_run_flag=""; \
@@ -153,5 +160,6 @@ bump-version rust_version:
print(f"Rust crates: {rust_version}")
print(f"Python package: {python_version}")
print(f"Maven artifact: {rust_version}")
PY
cargo fmt --all
+29
View File
@@ -0,0 +1,29 @@
# Goose SDK Maven package
This project packages the UniFFI-generated Kotlin/JVM bindings for `goose-sdk`
as the Maven artifact `io.github.aaif-goose:gdk`.
The artifact version is read from `crates/goose-sdk/Cargo.toml`, so it stays in
lockstep with the Rust crate version. The jar includes the generated Kotlin API
and native libraries under JNA platform resource directories.
Build locally from the repository root:
```bash
just --justfile crates/goose-sdk/justfile maven-package
```
Publish to Maven Central from the repository root:
```bash
just --justfile crates/goose-sdk/justfile maven-publish
```
Publishing requires the standard Gradle properties used by
`com.vanniktech.maven.publish` for Maven Central credentials and in-memory PGP
signing, for example via environment variables:
- `ORG_GRADLE_PROJECT_mavenCentralUsername`
- `ORG_GRADLE_PROJECT_mavenCentralPassword`
- `ORG_GRADLE_PROJECT_signingInMemoryKey`
- `ORG_GRADLE_PROJECT_signingInMemoryKeyPassword`
+86
View File
@@ -0,0 +1,86 @@
plugins {
kotlin("jvm") version "2.2.21"
`java-library`
id("com.vanniktech.maven.publish") version "0.34.0"
}
group = "io.github.aaif-goose"
version = gooseSdkVersion()
fun gooseSdkVersion(): String {
val cargoToml = file("../Cargo.toml").readText()
return Regex("(?m)^version\\s*=\\s*\"([^\"]+)\"")
.find(cargoToml)
?.groupValues
?.get(1)
?: error("Could not find goose-sdk version in ../Cargo.toml")
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
withSourcesJar()
}
dependencies {
api("net.java.dev.jna:jna:5.14.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
tasks.jar {
manifest {
attributes(
"Implementation-Title" to "Goose SDK",
"Implementation-Version" to project.version,
)
}
}
tasks.withType<GenerateModuleMetadata>().configureEach {
dependsOn(tasks.named("plainJavadocJar"))
}
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
if (providers.gradleProperty("signingInMemoryKey").isPresent) {
signAllPublications()
}
coordinates(
groupId = "io.github.aaif-goose",
artifactId = "gdk",
version = project.version.toString(),
)
pom {
name.set("Goose GDK")
description.set("Kotlin/JVM bindings for the Goose SDK")
inceptionYear.set("2026")
url.set("https://github.com/aaif-goose/goose")
licenses {
license {
name.set("Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0")
distribution.set("repo")
}
}
developers {
developer {
id.set("aaif")
name.set("Agentic AI Foundation")
email.set("ai-oss-tools@block.xyz")
}
}
scm {
connection.set("scm:git:https://github.com/aaif-goose/goose.git")
developerConnection.set("scm:git:ssh://git@github.com/aaif-goose/goose.git")
url.set("https://github.com/aaif-goose/goose")
}
}
}
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v gradle >/dev/null 2>&1; then
exec gradle "$@"
fi
echo "gradle is required to build the goose-sdk Maven package" >&2
echo "Install gradle or run inside the Hermit environment." >&2
exit 1
@@ -0,0 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
}
}
rootProject.name = "goose-sdk-maven"
@@ -0,0 +1,45 @@
package io.github.aaif_goose
import com.sun.jna.Platform
import java.nio.file.Files
internal object NativeLibraryLoader {
init {
val componentName = "goose"
if (System.getProperty("uniffi.component.$componentName.libraryOverride") == null) {
val resource = nativeResourcePath()
val stream = NativeLibraryLoader::class.java.classLoader.getResourceAsStream(resource)
?: error("Goose SDK native library resource not found: $resource")
val library = Files.createTempFile("goose-sdk-", nativeLibraryFileName()).toFile()
library.deleteOnExit()
stream.use { input -> library.outputStream().use { output -> input.copyTo(output) } }
System.setProperty("uniffi.component.$componentName.libraryOverride", library.absolutePath)
}
}
fun ensureLoaded() = Unit
private fun nativeResourcePath(): String = "${jnaResourcePrefix()}/${nativeLibraryFileName()}"
private fun nativeLibraryFileName(): String = when (osName()) {
"darwin" -> "libgoose_sdk.dylib"
"linux" -> "libgoose_sdk.so"
"win32" -> "goose_sdk.dll"
else -> error("Unsupported OS: ${System.getProperty("os.name")}")
}
private fun jnaResourcePrefix(): String = "${osName()}-${archName()}"
private fun osName(): String = when {
System.getProperty("os.name").startsWith("Mac OS X") -> "darwin"
System.getProperty("os.name").startsWith("Linux") -> "linux"
System.getProperty("os.name").startsWith("Windows") -> "win32"
else -> System.getProperty("os.name").lowercase().replace(Regex("\\s+"), "-")
}
private fun archName(): String = when {
Platform.isARM() && Platform.is64Bit() -> "aarch64"
Platform.isIntel() && Platform.is64Bit() -> "x86-64"
else -> System.getProperty("os.arch")
}
}
@@ -0,0 +1,15 @@
package io.github.aaif_goose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
public fun Provider.streamFlow(
model: ProviderModelConfig,
system: String,
messages: List<ProviderMessage>,
): Flow<ProviderStreamChunk> = flow {
val stream = stream(model, system, messages)
while (true) {
emit(stream.next() ?: break)
}
}
@@ -0,0 +1,6 @@
package io.github.aaif_goose.providers.databricks
public fun provider(host: String, token: String): io.github.aaif_goose.Provider =
io.github.aaif_goose.databricksProvider(host, token)
public fun defaultModel(): String = io.github.aaif_goose.databricksDefaultModel()
@@ -0,0 +1,5 @@
package io.github.aaif_goose.providers.openai
public fun provider(apiKey: String): io.github.aaif_goose.Provider = io.github.aaif_goose.openaiProvider(apiKey)
public fun defaultModel(): String = io.github.aaif_goose.openaiDefaultModel()
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) echo "darwin-aarch64" ;;
Darwin-x86_64) echo "darwin-x86-64" ;;
Linux-x86_64) echo "linux-x86-64" ;;
MINGW64_NT-*-x86_64|MSYS_NT-*-x86_64|CYGWIN_NT-*-x86_64) echo "win32-x86-64" ;;
*) echo "unsupported platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;;
esac
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -lt 1 ]; then
echo "usage: $0 <native-lib> [jna-resource-prefix]" >&2
exit 1
fi
native_lib="$1"
resource_prefix="${2:-$(crates/goose-sdk/scripts/maven-resource-prefix.sh)}"
case "$resource_prefix" in
darwin-aarch64|linux-x86-64|win32-x86-64) ;;
*) echo "unsupported JNA resource prefix: $resource_prefix" >&2; exit 1 ;;
esac
if [ ! -f "$native_lib" ]; then
echo "native library not found: $native_lib" >&2
exit 1
fi
bindgen="target/release/goose-uniffi-bindgen"
if [ ! -x "$bindgen" ]; then
cargo build -p goose-sdk --features uniffi --release -q
fi
maven_dir="crates/goose-sdk/maven"
kotlin_dir="$maven_dir/src/main/kotlin"
support_kotlin_dir="$maven_dir/src/support/kotlin"
resources_dir="$maven_dir/src/main/resources"
rm -rf "$kotlin_dir/io/github/aaif_goose" "$resources_dir/$resource_prefix"
mkdir -p "$kotlin_dir" "$resources_dir/$resource_prefix" "$resources_dir/META-INF"
cp LICENSE "$resources_dir/META-INF/LICENSE"
"$bindgen" generate \
--library "$native_lib" \
--config crates/goose-sdk/uniffi.toml \
--language kotlin \
--no-format \
--out-dir "$kotlin_dir" 2>/dev/null
python3 - "$kotlin_dir/io/github/aaif_goose/goose.kt" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text()
needle = 'private fun findLibraryName(componentName: String): String {\n'
replacement = needle + ' NativeLibraryLoader.ensureLoaded()\n'
if needle not in text:
raise SystemExit('could not find findLibraryName in generated Kotlin bindings')
path.write_text(text.replace(needle, replacement, 1))
PY
cp -R "$support_kotlin_dir"/. "$kotlin_dir"/
cp "$native_lib" "$resources_dir/$resource_prefix/"
+153 -56
View File
@@ -4,14 +4,18 @@
//! on declarative providers: consumers can construct a provider from JSON and
//! stream completions from it.
use std::sync::{Arc, Mutex};
use std::{future::Future, sync::Arc, sync::OnceLock};
use futures::StreamExt;
use goose_providers::{
base::{MessageStream, Provider},
api_client::{ApiClient, AuthMethod},
base::{MessageStream, Provider as GooseProvider},
conversation::message::Message,
databricks::DatabricksProvider as GooseDatabricksProvider,
databricks_auth::DatabricksAuth,
declarative::EnvKeyResolver,
model::ModelConfig,
openai::OpenAiProviderBuilder,
};
/// Errors surfaced across the uniffi boundary.
@@ -113,88 +117,181 @@ pub struct ProviderStreamChunk {
pub usage_json: Option<String>,
}
/// A declarative Goose provider constructed from provider JSON.
#[derive(uniffi::Object)]
pub struct DeclarativeProvider {
provider: Box<dyn Provider>,
runtime: Arc<tokio::runtime::Runtime>,
}
static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
#[uniffi::export]
impl DeclarativeProvider {
/// Construct a declarative provider using the process environment to resolve
/// configured API key environment variables.
#[uniffi::constructor]
pub fn from_json(json: String) -> Result<Arc<Self>, GooseError> {
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),
}))
fn runtime() -> Result<&'static tokio::runtime::Runtime, GooseError> {
if let Some(runtime) = RUNTIME.get() {
return Ok(runtime);
}
pub fn name(&self) -> String {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| GooseError::Generic(error.to_string()))?;
let _ = RUNTIME.set(runtime);
Ok(RUNTIME.get().expect("runtime was initialized"))
}
async fn run_on_runtime<T>(
future: impl Future<Output = T> + Send + 'static,
) -> Result<T, GooseError>
where
T: Send + 'static,
{
let (sender, receiver) = tokio::sync::oneshot::channel();
runtime()?.spawn(async move {
let _ = sender.send(future.await);
});
receiver
.await
.map_err(|_| GooseError::Generic("runtime task was cancelled".to_string()))
}
struct ProviderHandle {
provider: Arc<dyn GooseProvider>,
}
impl ProviderHandle {
fn new(provider: Box<dyn GooseProvider>) -> Self {
Self {
provider: Arc::from(provider),
}
}
fn name(&self) -> String {
self.provider.get_name().to_string()
}
/// Start a streaming completion request. Tools are not yet exposed over the
/// uniffi boundary, so this calls providers with an empty tool list.
pub fn stream(
async fn stream(
&self,
model: ProviderModelConfig,
system: String,
messages: Vec<ProviderMessage>,
) -> Result<Arc<DeclarativeProviderStream>, GooseError> {
) -> Result<Arc<ProviderStream>, GooseError> {
let model = model.to_goose_model_config()?;
let messages = messages
.iter()
.map(ProviderMessage::to_goose_message)
.collect::<Vec<_>>();
let provider = Arc::clone(&self.provider);
let stream =
self.runtime
.block_on(self.provider.stream(&model, &system, &messages, &[]))?;
run_on_runtime(async move { provider.stream(&model, &system, &messages, &[]).await })
.await??;
Ok(Arc::new(DeclarativeProviderStream {
stream: Mutex::new(stream),
runtime: Arc::clone(&self.runtime),
Ok(Arc::new(ProviderStream {
stream: Arc::new(tokio::sync::Mutex::new(stream)),
}))
}
}
/// A blocking iterator over provider stream chunks.
/// A Goose provider backed by one of Goose's native provider implementations.
#[derive(uniffi::Object)]
pub struct DeclarativeProviderStream {
stream: Mutex<MessageStream>,
runtime: Arc<tokio::runtime::Runtime>,
pub struct Provider {
handle: ProviderHandle,
}
impl Provider {
fn new(provider: Box<dyn GooseProvider>) -> Arc<Self> {
Arc::new(Self {
handle: ProviderHandle::new(provider),
})
}
}
#[uniffi::export]
impl DeclarativeProviderStream {
impl Provider {
pub fn name(&self) -> String {
self.handle.name()
}
/// Start a streaming completion request. Tools are not yet exposed over the
/// uniffi boundary, so this calls providers with an empty tool list.
pub async fn stream(
&self,
model: ProviderModelConfig,
system: String,
messages: Vec<ProviderMessage>,
) -> Result<Arc<ProviderStream>, GooseError> {
self.handle.stream(model, system, messages).await
}
}
#[uniffi::export]
pub fn declarative_provider_from_json(json: String) -> Result<Arc<Provider>, GooseError> {
let provider = goose_providers::declarative::from_json(&json, None, EnvKeyResolver {})?;
Ok(Provider::new(provider))
}
#[uniffi::export]
pub fn openai_default_model() -> String {
goose_providers::openai::OPEN_AI_DEFAULT_MODEL.to_string()
}
#[uniffi::export]
pub fn openai_provider(api_key: String) -> Result<Arc<Provider>, GooseError> {
let api_client = ApiClient::new_with_tls(
"https://api.openai.com".to_string(),
AuthMethod::BearerToken(api_key),
None,
)?;
let provider = OpenAiProviderBuilder::new(api_client).build();
Ok(Provider::new(Box::new(provider)))
}
#[uniffi::export]
pub fn databricks_default_model() -> String {
goose_providers::databricks::DATABRICKS_DEFAULT_MODEL.to_string()
}
#[uniffi::export]
pub fn databricks_provider(host: String, token: String) -> Result<Arc<Provider>, GooseError> {
let retry_config = GooseDatabricksProvider::load_retry_config(|key| std::env::var(key).ok());
let provider = GooseDatabricksProvider::new(
host,
DatabricksAuth::token(token),
retry_config,
None,
None,
None,
None,
None,
None,
None,
)?;
Ok(Provider::new(Box::new(provider)))
}
/// An async iterator over provider stream chunks.
#[derive(uniffi::Object)]
pub struct ProviderStream {
stream: Arc<tokio::sync::Mutex<MessageStream>>,
}
#[uniffi::export]
impl ProviderStream {
/// 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()))?;
pub async fn next(&self) -> Result<Option<ProviderStreamChunk>, GooseError> {
let stream = Arc::clone(&self.stream);
run_on_runtime(async move {
let mut stream = stream.lock().await;
let Some((message, usage)) = stream.next().await.transpose()? else {
return Ok(None);
};
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()?;
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,
}))
Ok(Some(ProviderStreamChunk {
text,
message_json,
usage_json,
}))
})
.await?
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
[bindings.kotlin]
package_name = "io.aaif.goose"
package_name = "io.github.aaif_goose"