diff --git a/Cargo.lock b/Cargo.lock index d5142e42..cc1b222c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -842,7 +853,7 @@ dependencies = [ "bincode", "bugreport", "bytesize", - "clap", + "clap 4.5.31", "clircle", "console", "content_inspector", @@ -1090,6 +1101,25 @@ dependencies = [ "cipher", ] +[[package]] +name = "cbindgen" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" +dependencies = [ + "clap 3.2.25", + "heck 0.4.1", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml 0.5.11", +] + [[package]] name = "cc" version = "1.2.16" @@ -1212,6 +1242,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.5.31" @@ -1230,8 +1275,8 @@ checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", - "clap_lex", - "strsim", + "clap_lex 0.7.4", + "strsim 0.11.1", "terminal_size", ] @@ -1241,12 +1286,21 @@ version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.99", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.4" @@ -1262,7 +1316,7 @@ dependencies = [ "console", "indicatif", "once_cell", - "strsim", + "strsim 0.11.1", "textwrap", "zeroize", ] @@ -1325,7 +1379,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.8.20", "yaml-rust2", ] @@ -1492,7 +1546,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.5.31", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1599,7 +1653,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.99", ] @@ -2323,7 +2377,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.0.18" +version = "1.0.17" dependencies = [ "anyhow", "async-stream", @@ -2378,7 +2432,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.0.18" +version = "1.0.17" dependencies = [ "anyhow", "async-trait", @@ -2393,7 +2447,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml", + "toml 0.8.20", "tracing", "tracing-subscriber", "winapi", @@ -2401,14 +2455,14 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.0.18" +version = "1.0.17" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bat", "chrono", - "clap", + "clap 4.5.31", "cliclack", "console", "etcetera", @@ -2437,9 +2491,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "goose-ffi" +version = "1.0.17" +dependencies = [ + "cbindgen", + "futures", + "goose", + "libc", + "once_cell", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "goose-mcp" -version = "1.0.18" +version = "1.0.17" dependencies = [ "anyhow", "async-trait", @@ -2485,7 +2553,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.0.18" +version = "1.0.17" dependencies = [ "anyhow", "async-trait", @@ -2493,7 +2561,7 @@ dependencies = [ "axum-extra", "bytes", "chrono", - "clap", + "clap 4.5.31", "config", "dirs 6.0.0", "etcetera", @@ -2612,12 +2680,27 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -4108,6 +4191,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "outref" version = "0.5.2" @@ -5484,6 +5573,12 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -5503,6 +5598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -5621,9 +5717,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.20", "version-compare", ] @@ -6003,6 +6099,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.20" diff --git a/crates/goose-ffi/Cargo.toml b/crates/goose-ffi/Cargo.toml new file mode 100644 index 00000000..4c6f1f60 --- /dev/null +++ b/crates/goose-ffi/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "goose-ffi" +build = "build.rs" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description.workspace = true + +[lib] +name = "goose_ffi" +crate-type = ["cdylib"] + +[dependencies] +goose = { path = "../goose" } +futures = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +libc = "0.2" +once_cell = "1.18" + +[build-dependencies] +cbindgen = "0.24.0" diff --git a/crates/goose-ffi/README.md b/crates/goose-ffi/README.md new file mode 100644 index 00000000..accb9e15 --- /dev/null +++ b/crates/goose-ffi/README.md @@ -0,0 +1,127 @@ +# Goose FFI + +Foreign Function Interface (FFI) for the Goose AI agent framework, allowing integration with other programming languages. + +## Overview + +The Goose FFI library provides C-compatible bindings for the Goose AI agent framework, enabling you to: + +- Create and manage Goose agents from any language with C FFI support +- Configure and use the Databricks AI provider for now but is extensible to other providers as needed +- Send messages to agents and receive responses + +## Building + +To build the FFI library, you'll need Rust and Cargo installed. Then run: + +```bash +# Build the library in debug mode +cargo build --package goose_ffi + +# Build the library in release mode (recommended for production) +cargo build --release --package goose_ffi +``` + +This will generate a dynamic library (.so, .dll, or .dylib depending on your platform) in the `target` directory, and automatically generate the C header file in the `include` directory. + +You can also build cross-platform binaries using cross command. For example to build for linux x86_64 architecture from Mac would require running + +```bash +CROSS_BUILD_OPTS="--platform linux/amd64 --no-cache" CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build -p goose-ffi --release --target x86_64-unknown-linux-gnu --no-default-features +``` +Note that this works only for gnu linux as it requires glibc. + +## Generated C Header + +The library uses cbindgen to automatically generate a C header file (`goose_ffi.h`) during the build process. This header contains all the necessary types and function declarations to use the library from C or any language with C FFI support. + +## Examples + +The FFI library includes examples in multiple languages to demonstrate how to use it. + +### Python Example + +The `examples/goose_agent.py` demonstrates using the FFI library from Python with ctypes. It shows: + +1. How to create a proper Python wrapper around the Goose FFI interface +2. Loading the shared library dynamically based on platform +3. Setting up C-compatible structures +4. Creating an object-oriented API for easier use + +Note: Tool callback functionality shown in earlier versions is not currently available and will be implemented in a future release. + +To run the Python example: + +```bash +# First, build the FFI library +cargo build --release --package goose_ffi + +# Then set the environment variables & run the example +DATABRICKS_HOST=... DATABRICKS_API_KEY=... python crates/goose-ffi/examples/goose_agent.py +``` + +You need to have Python 3.6+ installed with the `ctypes` module (included in standard library). + + +``` +> Tell me about the Eiffel Tower +``` + +The agent will respond with information about the Eiffel Tower. + +## Using from Other Languages + +The Goose FFI library can be used from many programming languages with C FFI support, including: + +- Python (via ctypes or cffi) +- JavaScript/Node.js (via node-ffi) +- Ruby (via fiddle) +- C#/.NET (via P/Invoke) +- Go (via cgo) +- Java / Kotlin (via JNA or JNI) + +Check the documentation for FFI support in your language of choice for details on how to load and use a C shared library. + +## Provider Configuration + +The FFI interface uses a provider type enumeration to specify which AI provider to use: + +```c +// C enum (defined in examples/simple_agent.c) +typedef enum { + PROVIDER_DATABRICKS = 0, // Databricks AI provider +} ProviderType; +``` + +```python +# Python enum (defined in examples/goose_agent.py) +class ProviderType(IntEnum): + DATABRICKS = 0 # Databricks AI provider +``` + +Currently, only the Databricks provider (provider_type = 0) is supported. If you attempt to use any other provider type, an error will be returned. + +### Environment-based Configuration + +The library supports configuration via environment variables, which makes it easier to use in containerized or CI/CD environments without hardcoding credentials: + +#### Databricks Provider (type = 0) + +``` +DATABRICKS_API_KEY=dapi... # Databricks API key +DATABRICKS_HOST=... # Databricks host URL (e.g., "https://your-workspace.cloud.databricks.com") +``` + +These environment variables will be used automatically if you don't provide the corresponding parameters when creating an agent. + +## Thread Safety + +The FFI library is designed to be thread-safe. Each agent instance is independent, and tools callbacks are handled in a thread-safe manner. However, the same agent instance should not be used from multiple threads simultaneously without external synchronization. + +## Error Handling + +Functions that can fail return either null pointers or special result structures that indicate success or failure. Always check return values and clean up resources using the appropriate free functions. + +## Memory Management + +The FFI interface handles memory allocation and deallocation. Use the provided free functions (like `goose_free_string` and `goose_free_async_result`) to release resources when you're done with them. diff --git a/crates/goose-ffi/build.rs b/crates/goose-ffi/build.rs new file mode 100644 index 00000000..a26c3409 --- /dev/null +++ b/crates/goose-ffi/build.rs @@ -0,0 +1,48 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config = cbindgen::Config { + language: cbindgen::Language::C, + documentation: true, + header: Some( + r#" +#ifndef GOOSE_FFI_H +#define GOOSE_FFI_H + +/* Goose FFI - C interface for the Goose AI agent framework */ +"# + .trim_start() + .to_string(), + ), + trailer: Some("#endif // GOOSE_FFI_H".to_string()), + includes: vec![], + sys_includes: vec!["stdint.h".to_string(), "stdbool.h".to_string()], + export: cbindgen::ExportConfig { + prefix: Some("goose_".to_string()), + ..Default::default() + }, + documentation_style: cbindgen::DocumentationStyle::C, + enumeration: cbindgen::EnumConfig { + prefix_with_name: true, + derive_helper_methods: true, + ..Default::default() + }, + ..Default::default() + }; + + let bindings = cbindgen::Builder::new() + .with_crate(&crate_dir) + .with_config(config) + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(&crate_dir).join("include"); + std::fs::create_dir_all(&out_path).expect("Failed to create include directory"); + bindings.write_to_file(out_path.join("goose_ffi.h")); + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/crates/goose-ffi/examples/goose_agent.py b/crates/goose-ffi/examples/goose_agent.py new file mode 100644 index 00000000..76f3fed5 --- /dev/null +++ b/crates/goose-ffi/examples/goose_agent.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Python example for using the Goose FFI interface. + +This example demonstrates how to: +1. Load the Goose FFI library +2. Create an agent with a provider +3. Add a tool extension +4. Send messages to the agent +5. Handle tool calls and responses +""" + +import ctypes +import os +import platform +from ctypes import c_char_p, c_bool, c_uint32, c_void_p, Structure, POINTER + +class ProviderType: + DATABRICKS = 0 + +# Platform-specific dynamic lib name +if platform.system() == "Darwin": + LIB_NAME = "libgoose_ffi.dylib" +elif platform.system() == "Linux": + LIB_NAME = "libgoose_ffi.so" +elif platform.system() == "Windows": + LIB_NAME = "goose_ffi.dll" +else: + raise RuntimeError("Unsupported platform") + +# Adjust to your actual build output directory +LIB_PATH = os.path.join(os.path.dirname(__file__), "../../..", "target", "debug", LIB_NAME) + +# Load library +goose = ctypes.CDLL(LIB_PATH) + +# Forward declaration for goose_Agent +class goose_Agent(Structure): + pass + +# Agent pointer type +goose_AgentPtr = POINTER(goose_Agent) + +# C struct mappings +class ProviderConfig(Structure): + _fields_ = [ + ("provider_type", c_uint32), + ("api_key", c_char_p), + ("model_name", c_char_p), + ("host", c_char_p), + ] + +class AsyncResult(Structure): + _fields_ = [ + ("succeeded", c_bool), + ("error_message", c_char_p), + ] + +# Function signatures +goose.goose_agent_new.argtypes = [POINTER(ProviderConfig)] +goose.goose_agent_new.restype = goose_AgentPtr + +goose.goose_agent_free.argtypes = [goose_AgentPtr] +goose.goose_agent_free.restype = None + +goose.goose_agent_send_message.argtypes = [goose_AgentPtr, c_char_p] +goose.goose_agent_send_message.restype = c_void_p + +goose.goose_free_string.argtypes = [c_void_p] +goose.goose_free_string.restype = None + +goose.goose_free_async_result.argtypes = [POINTER(AsyncResult)] +goose.goose_free_async_result.restype = None + +class GooseAgent: + def __init__(self, provider_type=ProviderType.DATABRICKS, api_key=None, model_name=None, host=None): + self.config = ProviderConfig( + provider_type=provider_type, + api_key=api_key.encode("utf-8") if api_key else None, + model_name=model_name.encode("utf-8") if model_name else None, + host=host.encode("utf-8") if host else None, + ) + self.agent = goose.goose_agent_new(ctypes.byref(self.config)) + if not self.agent: + raise RuntimeError("Failed to create Goose agent") + + def __del__(self): + if getattr(self, "agent", None): + goose.goose_agent_free(self.agent) + + def send_message(self, message: str) -> str: + msg = message.encode("utf-8") + response_ptr = goose.goose_agent_send_message(self.agent, msg) + if not response_ptr: + return "Error or NULL response from agent" + response = ctypes.string_at(response_ptr).decode("utf-8") + # Free the string using the proper C function provided by the library + # This correctly releases memory allocated by the Rust side + goose.goose_free_string(response_ptr) + return response + +def main(): + api_key = os.getenv("DATABRICKS_API_KEY") + host = os.getenv("DATABRICKS_HOST") + agent = GooseAgent(api_key=api_key, model_name="claude-3-7-sonnet", host=host) + + print("Type a message (or 'quit' to exit):") + while True: + user_input = input("> ") + if user_input.lower() in ("quit", "exit"): + break + reply = agent.send_message(user_input) + print(f"Agent: {reply}\n") + +if __name__ == "__main__": + main() diff --git a/crates/goose-ffi/include/goose_ffi.h b/crates/goose-ffi/include/goose_ffi.h new file mode 100644 index 00000000..283e1471 --- /dev/null +++ b/crates/goose-ffi/include/goose_ffi.h @@ -0,0 +1,145 @@ +#ifndef GOOSE_FFI_H +#define GOOSE_FFI_H + +/* Goose FFI - C interface for the Goose AI agent framework */ + + +#include +#include +#include +#include +#include +#include + +/* + Provider Type enumeration + Currently only Databricks is supported + */ +enum goose_ProviderType { + /* + Databricks AI provider + */ + goose_ProviderType_Databricks = 0, +}; +typedef uint32_t goose_ProviderType; + +/* + Result type for async operations + + - succeeded: true if the operation succeeded, false otherwise + - error_message: Error message if succeeded is false, NULL otherwise + */ +typedef struct goose_AsyncResult { + bool succeeded; + char *error_message; +} goose_AsyncResult; + +/* + Pointer type for the agent + */ +typedef goose_Agent *goose_AgentPtr; + +/* + Provider configuration used to initialize an AI provider + + - provider_type: Provider type (0 = Databricks, other values will produce an error) + - api_key: Provider API key (null for default from environment variables) + - model_name: Model name to use (null for provider default) + - host: Provider host URL (null for default from environment variables) + */ +typedef struct goose_ProviderConfigFFI { + goose_ProviderType provider_type; + const char *api_key; + const char *model_name; + const char *host; +} goose_ProviderConfigFFI; + +/* + Free an async result structure + + This function frees the memory allocated for an AsyncResult structure, + including any error message it contains. + + # Safety + + The result pointer must be a valid pointer returned by a goose FFI function, + or NULL. + */ +void goose_free_async_result(struct goose_AsyncResult *result); + +/* + Create a new agent with the given provider configuration + + # Parameters + + - config: Provider configuration + + # Returns + + A new agent pointer, or a null pointer if creation failed + + # Safety + + The config pointer must be valid or NULL. The resulting agent must be freed + with goose_agent_free when no longer needed. + */ +goose_AgentPtr goose_agent_new(const struct goose_ProviderConfigFFI *config); + +/* + Free an agent + + This function frees the memory allocated for an agent. + + # Parameters + + - agent_ptr: Agent pointer returned by goose_agent_new + + # Safety + + The agent_ptr must be a valid pointer returned by goose_agent_new, + or have a null internal pointer. The agent_ptr must not be used after + calling this function. + */ +void goose_agent_free(goose_AgentPtr agent_ptr); + +/* + Send a message to the agent and get the response + + This function sends a message to the agent and returns the response. + Tool handling is not yet supported and will be implemented in a future commit + so this may change significantly + + # Parameters + + - agent_ptr: Agent pointer + - message: Message to send + + # Returns + + A C string with the agent's response, or NULL on error. + This string must be freed with goose_free_string when no longer needed. + + # Safety + + The agent_ptr must be a valid pointer returned by goose_agent_new. + The message must be a valid C string. + */ +char *goose_agent_send_message(goose_AgentPtr agent_ptr, const char *message); + +/* + Free a string allocated by goose FFI functions + + This function frees memory allocated for strings returned by goose FFI functions. + + # Parameters + + - s: String to free + + # Safety + + The string must have been allocated by a goose FFI function, or be NULL. + The string must not be used after calling this function. + */ +void goose_free_string(char *s); + +#endif // GOOSE_FFI_H diff --git a/crates/goose-ffi/src/lib.rs b/crates/goose-ffi/src/lib.rs new file mode 100644 index 00000000..06a9db5a --- /dev/null +++ b/crates/goose-ffi/src/lib.rs @@ -0,0 +1,301 @@ +use std::ffi::{c_char, CStr, CString}; +use std::ptr; +use std::sync::Arc; + +use futures::StreamExt; +use goose::agents::Agent; +use goose::message::Message; +use goose::model::ModelConfig; +use goose::providers::databricks::DatabricksProvider; +use once_cell::sync::OnceCell; +use tokio::runtime::Runtime; + +// This class is in alpha and not yet ready for production use +// and the API is not yet stable. Use at your own risk. + +// Thread-safe global runtime +static RUNTIME: OnceCell = OnceCell::new(); + +// Get or initialize the global runtime +fn get_runtime() -> &'static Runtime { + RUNTIME.get_or_init(|| { + // Runtime with all features enabled + Runtime::new().expect("Failed to create Tokio runtime") + }) +} + +/// Pointer type for the agent +pub type AgentPtr = *mut Agent; +/// Provider Type enumeration +/// Currently only Databricks is supported +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum ProviderType { + /// Databricks AI provider + Databricks = 0, +} + +/// Provider configuration used to initialize an AI provider +/// +/// - provider_type: Provider type (0 = Databricks, other values will produce an error) +/// - api_key: Provider API key (null for default from environment variables) +/// - model_name: Model name to use (null for provider default) +/// - host: Provider host URL (null for default from environment variables) +#[repr(C)] +pub struct ProviderConfigFFI { + pub provider_type: ProviderType, + pub api_key: *const c_char, + pub model_name: *const c_char, + pub host: *const c_char, +} + +// Extension configuration will be implemented in a future commit + +/// Role enum for message participants +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum MessageRole { + /// User message role + User = 0, + /// Assistant message role + Assistant = 1, + /// System message role + System = 2, +} + +/// Message structure for agent interactions +/// +/// - role: Message role (User, Assistant, or System) +/// - content: Text content of the message +#[repr(C)] +pub struct MessageFFI { + pub role: MessageRole, + pub content: *const c_char, +} + +// Tool callbacks will be implemented in a future commit + +/// Result type for async operations +/// +/// - succeeded: true if the operation succeeded, false otherwise +/// - error_message: Error message if succeeded is false, NULL otherwise +#[repr(C)] +pub struct AsyncResult { + pub succeeded: bool, + pub error_message: *mut c_char, +} + +/// Free an async result structure +/// +/// This function frees the memory allocated for an AsyncResult structure, +/// including any error message it contains. +/// +/// # Safety +/// +/// The result pointer must be a valid pointer returned by a goose FFI function, +/// or NULL. +#[no_mangle] +pub unsafe extern "C" fn goose_free_async_result(result: *mut AsyncResult) { + if !result.is_null() { + let result = &mut *result; + if !result.error_message.is_null() { + let _ = CString::from_raw(result.error_message); + } + let _ = Box::from_raw(result); + } +} + +/// Create a new agent with the given provider configuration +/// +/// # Parameters +/// +/// - config: Provider configuration +/// +/// # Returns +/// +/// A new agent pointer, or a null pointer if creation failed +/// +/// # Safety +/// +/// The config pointer must be valid or NULL. The resulting agent must be freed +/// with goose_agent_free when no longer needed. +#[no_mangle] +pub unsafe extern "C" fn goose_agent_new(config: *const ProviderConfigFFI) -> AgentPtr { + // Check for null pointer + if config.is_null() { + eprintln!("Error: config pointer is null"); + return ptr::null_mut(); + } + + let config = &*config; + + // We currently only support Databricks provider + // This match ensures future compiler errors if new provider types are added without handling + match config.provider_type { + ProviderType::Databricks => (), // Databricks provider is supported + } + + // Get api_key from config or environment + let api_key = if !config.api_key.is_null() { + CStr::from_ptr(config.api_key).to_string_lossy().to_string() + } else { + match std::env::var("DATABRICKS_API_KEY") { + Ok(key) => key, + Err(_) => { + eprintln!("Error: api_key not provided and DATABRICKS_API_KEY environment variable not set"); + return ptr::null_mut(); + } + } + }; + + // Check and get required model_name (no env fallback for model) + if config.model_name.is_null() { + eprintln!("Error: model_name is required but was null"); + return ptr::null_mut(); + } + let model_name = CStr::from_ptr(config.model_name) + .to_string_lossy() + .to_string(); + + // Get host from config or environment + let host = if !config.host.is_null() { + CStr::from_ptr(config.host).to_string_lossy().to_string() + } else { + match std::env::var("DATABRICKS_HOST") { + Ok(url) => url, + Err(_) => { + eprintln!( + "Error: host not provided and DATABRICKS_HOST environment variable not set" + ); + return ptr::null_mut(); + } + } + }; + + // Create model config with model name + let model_config = ModelConfig::new(model_name); + + // Create Databricks provider with required parameters + match DatabricksProvider::from_params(host, api_key, model_config) { + Ok(provider) => { + let agent = Agent::new(Arc::new(provider)); + Box::into_raw(Box::new(agent)) + } + Err(e) => { + eprintln!("Error creating Databricks provider: {:?}", e); + ptr::null_mut() + } + } +} + +/// Free an agent +/// +/// This function frees the memory allocated for an agent. +/// +/// # Parameters +/// +/// - agent_ptr: Agent pointer returned by goose_agent_new +/// +/// # Safety +/// +/// The agent_ptr must be a valid pointer returned by goose_agent_new, +/// or have a null internal pointer. The agent_ptr must not be used after +/// calling this function. +#[no_mangle] +pub unsafe extern "C" fn goose_agent_free(agent_ptr: AgentPtr) { + if !agent_ptr.is_null() { + let _ = Box::from_raw(agent_ptr); + } +} + +/// Send a message to the agent and get the response +/// +/// This function sends a message to the agent and returns the response. +/// Tool handling is not yet supported and will be implemented in a future commit +/// so this may change significantly +/// +/// # Parameters +/// +/// - agent_ptr: Agent pointer +/// - message: Message to send +/// +/// # Returns +/// +/// A C string with the agent's response, or NULL on error. +/// This string must be freed with goose_free_string when no longer needed. +/// +/// # Safety +/// +/// The agent_ptr must be a valid pointer returned by goose_agent_new. +/// The message must be a valid C string. +#[no_mangle] +pub unsafe extern "C" fn goose_agent_send_message( + agent_ptr: AgentPtr, + message: *const c_char, +) -> *mut c_char { + if agent_ptr.is_null() || message.is_null() { + return ptr::null_mut(); + } + + let agent = &mut *agent_ptr; + let message = CStr::from_ptr(message).to_string_lossy().to_string(); + + let messages = vec![Message::user().with_text(&message)]; + + // Block on the async call using our global runtime + let response = get_runtime().block_on(async { + let mut stream = match agent.reply(&messages, None).await { + Ok(stream) => stream, + Err(e) => return format!("Error getting reply from agent: {}", e), + }; + + let mut full_response = String::new(); + + while let Some(message_result) = stream.next().await { + match message_result { + Ok(message) => { + // Get text or serialize to JSON + // Note: Message doesn't have as_text method, we'll serialize to JSON + if let Ok(json) = serde_json::to_string(&message) { + full_response.push_str(&json); + } + } + Err(e) => { + full_response.push_str(&format!("\nError in message stream: {}", e)); + } + } + } + full_response + }); + + string_to_c_char(&response) +} + +// Tool schema creation will be implemented in a future commit + +/// Free a string allocated by goose FFI functions +/// +/// This function frees memory allocated for strings returned by goose FFI functions. +/// +/// # Parameters +/// +/// - s: String to free +/// +/// # Safety +/// +/// The string must have been allocated by a goose FFI function, or be NULL. +/// The string must not be used after calling this function. +#[no_mangle] +pub unsafe extern "C" fn goose_free_string(s: *mut c_char) { + if !s.is_null() { + let _ = CString::from_raw(s); + } +} + +// Helper function to convert a Rust string to a C char pointer +fn string_to_c_char(s: &str) -> *mut c_char { + match CString::new(s) { + Ok(c_string) => c_string.into_raw(), + Err(_) => ptr::null_mut(), + } +} diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 2451f351..43b21ece 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -84,7 +84,6 @@ impl DatabricksProvider { // For compatibility for now we check both config and secret for databricks host // but it is not actually a secret value let mut host: Result = config.get_param("DATABRICKS_HOST"); - if host.is_err() { host = config.get_secret("DATABRICKS_HOST") } @@ -123,6 +122,31 @@ impl DatabricksProvider { }) } + /// Create a new DatabricksProvider with the specified host and token + /// + /// # Arguments + /// + /// * `host` - The Databricks host URL + /// * `token` - The Databricks API token + /// * `model` - The model configuration + /// + /// # Returns + /// + /// Returns a Result containing the new DatabricksProvider instance + pub fn from_params(host: String, api_key: String, model: ModelConfig) -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(600)) + .build()?; + + Ok(Self { + client, + host, + auth: DatabricksAuth::token(api_key), + model, + image_format: ImageFormat::OpenAi, + }) + } + async fn ensure_auth_header(&self) -> Result { match &self.auth { DatabricksAuth::Token(token) => Ok(format!("Bearer {}", token)),