[goose-llm] kotlin fn for getting structured outputs (#2547)
This commit is contained in:
+13
-24
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -44,7 +44,7 @@ impl ProviderCompleteResponse {
|
||||
}
|
||||
|
||||
/// Response from a structured‐extraction 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)
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user