[goose-llm] kotlin fn for getting structured outputs (#2547)

This commit is contained in:
Salman Mohammed
2025-05-20 07:08:54 -07:00
committed by GitHub
parent 81332ab914
commit f153204dde
13 changed files with 340 additions and 202 deletions
+13 -24
View File
@@ -31,34 +31,13 @@ Structure:
│ └── goose_llm.kt ← auto-generated bindings
```
#### Create Kotlin bindings:
```bash
# run from project root directory
cargo build -p goose-llm
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language kotlin --out-dir bindings/kotlin
```
#### Kotlin -> Rust: run example
The following `just` command creates kotlin bindings, then compiles and runs an example.
```bash
pushd bindings/kotlin/
kotlinc \
example/Usage.kt \
uniffi/goose_llm/goose_llm.kt \
-classpath "libs/kotlin-stdlib-1.9.0.jar:libs/kotlinx-coroutines-core-jvm-1.7.3.jar:libs/jna-5.13.0.jar" \
-include-runtime \
-d example.jar
java \
-Djna.library.path=$HOME/Development/goose/target/debug \
-classpath "example.jar:libs/kotlin-stdlib-1.9.0.jar:libs/kotlinx-coroutines-core-jvm-1.7.3.jar:libs/jna-5.13.0.jar" \
UsageKt
popd
just kotlin-example
```
You will have to download jars in `bindings/kotlin/libs` directory (only the first time):
@@ -70,6 +49,16 @@ curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.ja
popd
```
To just create the Kotlin bindings:
```bash
# run from project root directory
cargo build -p goose-llm
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language kotlin --out-dir bindings/kotlin
```
#### Python -> Rust: generate bindings, run example
```bash
+10 -15
View File
@@ -1,5 +1,4 @@
use crate::model::ModelConfig;
use crate::providers::create;
use crate::generate_structured_outputs;
use crate::providers::errors::ProviderError;
use crate::types::core::Role;
use crate::{message::Message, types::json_value_ffi::JsonValueFfi};
@@ -54,15 +53,6 @@ pub async fn generate_session_name(
provider_config: JsonValueFfi,
messages: &[Message],
) -> Result<String, ProviderError> {
// Use OpenAI models specifically for this task
let model_name = if provider_name == "databricks" {
"goose-gpt-4-1"
} else {
"gpt-4.1"
};
let model_cfg = ModelConfig::new(model_name.to_string()).with_temperature(Some(0.0));
let provider = create(provider_name, provider_config.into(), model_cfg)?;
// Collect up to the first 3 user messages (truncated to 300 chars each)
let context: Vec<String> = messages
.iter()
@@ -96,10 +86,15 @@ pub async fn generate_session_name(
"required": ["name"],
"additionalProperties": false
});
let user_msg = Message::user().with_text(&user_msg_text);
let resp = provider
.extract(&system_prompt, &[user_msg], &schema)
.await?;
let resp = generate_structured_outputs(
provider_name,
provider_config,
&system_prompt,
&[Message::user().with_text(&user_msg_text)],
schema,
)
.await?;
let obj = resp
.data
+11 -17
View File
@@ -1,6 +1,5 @@
use crate::generate_structured_outputs;
use crate::message::{Message, MessageContent};
use crate::model::ModelConfig;
use crate::providers::create;
use crate::providers::errors::ProviderError;
use crate::types::core::{Content, Role};
use crate::types::json_value_ffi::JsonValueFfi;
@@ -59,16 +58,7 @@ pub async fn generate_tooltip(
provider_config: JsonValueFfi,
messages: &[Message],
) -> Result<String, ProviderError> {
// Use OpenAI models specifically for this task
let model_name = if provider_name == "databricks" {
"goose-gpt-4-1"
} else {
"gpt-4.1"
};
let model_cfg = ModelConfig::new(model_name.to_string()).with_temperature(Some(0.0));
let provider = create(provider_name, provider_config.into(), model_cfg)?;
// Need at least two messages to summarize
// Need at least two messages to generate a tooltip
if messages.len() < 2 {
return Err(ProviderError::ExecutionError(
"Need at least two messages to generate a tooltip".to_string(),
@@ -151,11 +141,15 @@ pub async fn generate_tooltip(
"additionalProperties": false
});
// Call extract
let user_msg = Message::user().with_text(&user_msg_text);
let resp = provider
.extract(&system_prompt, &[user_msg], &schema)
.await?;
// Get the structured outputs
let resp = generate_structured_outputs(
provider_name,
provider_config,
&system_prompt,
&[Message::user().with_text(&user_msg_text)],
schema,
)
.await?;
// Pull out the tooltip field
let obj = resp
+2
View File
@@ -6,8 +6,10 @@ pub mod message;
mod model;
mod prompt_template;
pub mod providers;
mod structured_outputs;
pub mod types;
pub use completion::completion;
pub use message::Message;
pub use model::ModelConfig;
pub use structured_outputs::generate_structured_outputs;
+1 -1
View File
@@ -44,7 +44,7 @@ impl ProviderCompleteResponse {
}
/// Response from a structuredextraction call
#[derive(Debug, Clone)]
#[derive(Debug, Clone, uniffi::Record)]
pub struct ProviderExtractResponse {
/// The extracted JSON object
pub data: serde_json::Value,
@@ -0,0 +1,29 @@
use crate::{
providers::{create, errors::ProviderError, ProviderExtractResponse},
types::json_value_ffi::JsonValueFfi,
Message, ModelConfig,
};
/// Generates a structured output based on the provided schema,
/// system prompt and user messages.
#[uniffi::export(async_runtime = "tokio")]
pub async fn generate_structured_outputs(
provider_name: &str,
provider_config: JsonValueFfi,
system_prompt: &str,
messages: &[Message],
schema: JsonValueFfi,
) -> Result<ProviderExtractResponse, ProviderError> {
// Use OpenAI models specifically for this task
let model_name = if provider_name == "databricks" {
"goose-gpt-4-1"
} else {
"gpt-4.1"
};
let model_cfg = ModelConfig::new(model_name.to_string()).with_temperature(Some(0.0));
let provider = create(provider_name, provider_config, model_cfg)?;
let resp = provider.extract(system_prompt, messages, &schema).await?;
Ok(resp)
}
+5 -14
View File
@@ -52,7 +52,7 @@ pub fn create_completion_request(
) -> CompletionRequest {
CompletionRequest::new(
provider_name.to_string(),
provider_config.into(),
provider_config,
model_config,
system_preamble.to_string(),
messages,
@@ -141,11 +141,11 @@ pub enum ToolApprovalMode {
Smart,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)]
pub struct ToolConfig {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
pub input_schema: JsonValueFfi,
pub approval_mode: ToolApprovalMode,
}
@@ -153,7 +153,7 @@ impl ToolConfig {
pub fn new(
name: &str,
description: &str,
input_schema: serde_json::Value,
input_schema: JsonValueFfi,
approval_mode: ToolApprovalMode,
) -> Self {
Self {
@@ -182,18 +182,9 @@ pub fn create_tool_config(
input_schema: JsonValueFfi,
approval_mode: ToolApprovalMode,
) -> ToolConfig {
ToolConfig::new(name, description, input_schema.into(), approval_mode)
ToolConfig::new(name, description, input_schema, approval_mode)
}
uniffi::custom_type!(ToolConfig, String, {
lower: |tc: &ToolConfig| {
serde_json::to_string(&tc).unwrap()
},
try_lift: |s: String| {
Ok(serde_json::from_str(&s).unwrap())
},
});
// — Register the newtypes with UniFFI, converting via JSON strings —
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+5 -71
View File
@@ -1,84 +1,18 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
// `serde_json::Value` gets converted to a `String` to pass across the FFI.
// https://github.com/mozilla/uniffi-rs/blob/main/docs/manual/src/types/custom_types.md?plain=1
// https://github.com/mozilla/uniffi-rs/blob/c7f6caa3d1bf20f934346cefd8e82b5093f0dc6f/examples/custom-types/src/lib.rs#L63-L69
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonValueFfi(Value);
impl From<JsonValueFfi> for Value {
fn from(val: JsonValueFfi) -> Self {
val.0
}
}
impl From<Value> for JsonValueFfi {
fn from(val: Value) -> Self {
JsonValueFfi(val)
}
}
uniffi::custom_type!(JsonValueFfi, String, {
uniffi::custom_type!(Value, String, {
// Remote is required since 'Value' is from a different crate
remote,
lower: |obj| {
serde_json::to_string(&obj.0).unwrap()
serde_json::to_string(&obj).unwrap()
},
try_lift: |val| {
Ok(serde_json::from_str(&val).unwrap() )
},
});
// Write some tests to ensure that the conversion works as expected
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_json_value_ffi_conversion() {
let original = JsonValueFfi(json!({"key": "value"}));
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: JsonValueFfi = serde_json::from_str(&serialized).unwrap();
assert_eq!(original.0, deserialized.0);
}
#[test]
fn test_json_value_ffi_to_serde() {
let original = JsonValueFfi(json!({"key": "value"}));
let value: Value = original.into();
assert_eq!(value, json!({"key": "value"}));
}
#[test]
fn test_json_value_ffi_from_serde() {
let value = json!({"key": "value"});
let original: JsonValueFfi = value.into();
assert_eq!(original.0, json!({"key": "value"}));
}
#[test]
fn test_json_value_ffi_lower() {
let original = JsonValueFfi(json!({"key": "value"}));
let serialized = serde_json::to_string(&original).unwrap();
assert_eq!(serialized, "{\"key\":\"value\"}");
}
#[test]
fn test_json_value_ffi_try_lift() {
let json_str = "{\"key\":\"value\"}";
let deserialized: JsonValueFfi = serde_json::from_str(json_str).unwrap();
let expected = JsonValueFfi(json!({"key": "value"}));
assert_eq!(deserialized.0, expected.0);
}
#[test]
fn test_json_value_ffi_custom_type() {
let json_str = "{\"key\":\"value\"}";
let deserialized: JsonValueFfi = serde_json::from_str(json_str).unwrap();
let serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(serialized, json_str);
}
}
pub type JsonValueFfi = Value;
@@ -22,7 +22,7 @@ async fn _generate_session_name(messages: &[Message]) -> Result<String, Provider
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
});
generate_session_name(provider_name, provider_config.into(), messages).await
generate_session_name(provider_name, provider_config, messages).await
}
#[tokio::test]
+1 -1
View File
@@ -24,7 +24,7 @@ async fn _generate_tooltip(messages: &[Message]) -> Result<String, ProviderError
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
});
generate_tooltip(provider_name, provider_config.into(), messages).await
generate_tooltip(provider_name, provider_config, messages).await
}
#[tokio::test]