mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
A provider example
This commit is contained in:
Generated
+4
@@ -5142,7 +5142,11 @@ version = "1.40.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol",
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
"futures",
|
||||
"goose-providers",
|
||||
"goose-sdk-types",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
|
||||
@@ -19,7 +19,15 @@ required-features = ["uniffi"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
uniffi = ["dep:uniffi", "dep:thiserror"]
|
||||
uniffi = [
|
||||
"dep:uniffi",
|
||||
"dep:thiserror",
|
||||
"dep:anyhow",
|
||||
"dep:goose-providers",
|
||||
"dep:futures",
|
||||
"dep:serde_json",
|
||||
"dep:tokio",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
goose-sdk-types = { path = "../goose-sdk-types" }
|
||||
@@ -28,6 +36,11 @@ agent-client-protocol-schema = { workspace = true }
|
||||
|
||||
uniffi = { version = "0.31", features = ["cli"], optional = true }
|
||||
thiserror = { version = "2", optional = true }
|
||||
goose-providers = { version = "1.39.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]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process", "io-std", "io-util"] }
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Goose SDK demo: build a declarative provider and stream a completion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent.parent / "generated"))
|
||||
|
||||
from aaif_goose import ( # noqa: E402
|
||||
DeclarativeProvider,
|
||||
MessageRole,
|
||||
ProviderMessage,
|
||||
ProviderModelConfig,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
provider_json = (HERE.parent.parent.parent / "goose-providers" / "examples" / "deepseek.json").read_text()
|
||||
provider = DeclarativeProvider.from_json(provider_json)
|
||||
model = ProviderModelConfig(
|
||||
model_name="deepseek-v4-flash",
|
||||
context_limit=None,
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
toolshim=False,
|
||||
toolshim_model=None,
|
||||
request_params_json=None,
|
||||
reasoning=None,
|
||||
)
|
||||
messages = [ProviderMessage(role=MessageRole.USER, text="what is the capital of France?")]
|
||||
stream = provider.stream(
|
||||
model,
|
||||
"",
|
||||
"You are a knowledgable geography expert",
|
||||
messages,
|
||||
)
|
||||
|
||||
print(f"{provider.name()}:")
|
||||
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()
|
||||
@@ -1,14 +1,18 @@
|
||||
//! In-process uniffi bindings for the Goose SDK.
|
||||
//!
|
||||
//! This is the published API surface exposed to Python and Kotlin. Right now it
|
||||
//! is a minimal `ping` -> `pong` round-trip that proves the uniffi
|
||||
//! infrastructure end to end without depending on the `goose` core crate.
|
||||
//!
|
||||
//! 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.
|
||||
//! This is the API surface exposed to Python and Kotlin. It currently focuses
|
||||
//! on declarative providers: consumers can construct a provider from JSON and
|
||||
//! stream completions from it.
|
||||
|
||||
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.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -17,36 +21,178 @@ pub enum GooseError {
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
/// A reply to a [`Client::ping`] call.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct Pong {
|
||||
/// Echo of the message that was pinged.
|
||||
pub message: String,
|
||||
impl From<anyhow::Error> for GooseError {
|
||||
fn from(error: anyhow::Error) -> Self {
|
||||
Self::Generic(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// The top-level entry point for the Goose SDK.
|
||||
///
|
||||
/// This is the object that consuming languages instantiate. Today it only knows
|
||||
/// how to answer a ping; extend it with the real agent API.
|
||||
impl From<goose_providers::errors::ProviderError> for GooseError {
|
||||
fn from(error: goose_providers::errors::ProviderError) -> Self {
|
||||
Self::Generic(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
pub context_limit: Option<u64>,
|
||||
pub temperature: Option<f32>,
|
||||
pub max_tokens: Option<i32>,
|
||||
pub toolshim: bool,
|
||||
pub toolshim_model: Option<String>,
|
||||
/// Provider-specific request parameters as a JSON object string.
|
||||
pub request_params_json: Option<String>,
|
||||
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)]
|
||||
pub struct Client {}
|
||||
pub struct DeclarativeProvider {
|
||||
provider: Box<dyn Provider>,
|
||||
runtime: Arc<tokio::runtime::Runtime>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Client {
|
||||
impl DeclarativeProvider {
|
||||
/// Construct a declarative provider using the process environment to resolve
|
||||
/// configured API key environment variables.
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {})
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Round-trip a message through the SDK. Returns a [`Pong`] echoing the
|
||||
/// supplied `message`, prefixed with `pong: `.
|
||||
pub fn ping(&self, message: String) -> Result<Pong, GooseError> {
|
||||
if message.is_empty() {
|
||||
return Err(GooseError::Generic("message must not be empty".into()));
|
||||
}
|
||||
Ok(Pong {
|
||||
message: format!("pong: {message}"),
|
||||
})
|
||||
pub 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(
|
||||
&self,
|
||||
model: ProviderModelConfig,
|
||||
session_id: String,
|
||||
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,
|
||||
&session_id,
|
||||
&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 +201,29 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ping_returns_pong() {
|
||||
let client = Client::new();
|
||||
let pong = client.ping("aaif.io".into()).expect("ping should succeed");
|
||||
assert_eq!(pong.message, "pong: aaif.io");
|
||||
fn model_config_rejects_invalid_request_params_json() {
|
||||
let config = ProviderModelConfig {
|
||||
model_name: "test".to_string(),
|
||||
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]
|
||||
fn empty_ping_errors() {
|
||||
let client = Client::new();
|
||||
assert!(client.ping(String::new()).is_err());
|
||||
fn provider_message_converts_user_text() {
|
||||
let message = ProviderMessage {
|
||||
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,12 +5,10 @@
|
||||
//! that talks to `goose acp` over stdio.
|
||||
//!
|
||||
//! With `--features uniffi` the crate additionally compiles as a
|
||||
//! `cdylib`/`staticlib` and exposes a small in-process API to Python and Kotlin
|
||||
//! via [uniffi-rs](https://github.com/mozilla/uniffi-rs).
|
||||
//!
|
||||
//! The published uniffi surface is intentionally a single `ping` -> `pong`
|
||||
//! round-trip. It exists as a working scaffold for adding the real Goose SDK
|
||||
//! API: replace [`bindings`] with the actual implementation.
|
||||
//! `cdylib`/`staticlib` and exposes an in-process API to Python and Kotlin via
|
||||
//! [uniffi-rs](https://github.com/mozilla/uniffi-rs). The current uniffi surface
|
||||
//! lets callers construct declarative providers from JSON and stream provider
|
||||
//! completions.
|
||||
|
||||
pub use goose_sdk_types::{custom_notifications, custom_requests};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user