feat: Build a prototype FFI for goose rust library (#2206)

This builds a prototype FFI for goose rust library which only supports initializing goose agent and sending message, there is no support for tool calling yet in this library.
This commit is contained in:
meenalc
2025-04-16 16:53:03 -07:00
committed by GitHub
parent 0148ced33e
commit e073e32178
8 changed files with 910 additions and 19 deletions

141
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

127
crates/goose-ffi/README.md Normal file
View File

@@ -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.

48
crates/goose-ffi/build.rs Normal file
View File

@@ -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");
}

View File

@@ -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()

View File

@@ -0,0 +1,145 @@
#ifndef GOOSE_FFI_H
#define GOOSE_FFI_H
/* Goose FFI - C interface for the Goose AI agent framework */
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
/*
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

301
crates/goose-ffi/src/lib.rs Normal file
View File

@@ -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<Runtime> = 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(),
}
}

View File

@@ -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<String, ConfigError> = 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<Self> {
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<String> {
match &self.auth {
DatabricksAuth::Token(token) => Ok(format!("Bearer {}", token)),