mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
[goose-llm] add providerConfig param for exposed LLM functions (#2491)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.jar
|
||||
run_cli.sh
|
||||
tokenizer_files/
|
||||
|
||||
@@ -66,14 +66,23 @@ fun main() = runBlocking {
|
||||
printMessages(msgs)
|
||||
println("---\n")
|
||||
|
||||
val sessionName = generateSessionName(msgs)
|
||||
println("Session Name: $sessionName")
|
||||
// Setup provider
|
||||
val providerName = "databricks"
|
||||
val host = System.getenv("DATABRICKS_HOST") ?: error("DATABRICKS_HOST not set")
|
||||
val token = System.getenv("DATABRICKS_TOKEN") ?: error("DATABRICKS_TOKEN not set")
|
||||
val providerConfig = """{"host": "$host", "token": "$token"}"""
|
||||
|
||||
val tooltip = generateTooltip(msgs)
|
||||
println("Tooltip: $tooltip")
|
||||
println("Provider Name: $providerName")
|
||||
println("Provider Config: $providerConfig")
|
||||
|
||||
|
||||
val sessionName = generateSessionName(providerName, providerConfig, msgs)
|
||||
println("\nSession Name: $sessionName")
|
||||
|
||||
val tooltip = generateTooltip(providerName, providerConfig, msgs)
|
||||
println("\nTooltip: $tooltip")
|
||||
|
||||
// Completion
|
||||
val provider = "databricks"
|
||||
val modelName = "goose-gpt-4-1"
|
||||
val modelConfig = ModelConfig(
|
||||
modelName,
|
||||
@@ -116,8 +125,9 @@ fun main() = runBlocking {
|
||||
val systemPreamble = "You are a helpful assistant."
|
||||
|
||||
|
||||
val req = CompletionRequest(
|
||||
provider,
|
||||
val req = createCompletionRequest(
|
||||
providerName,
|
||||
providerConfig,
|
||||
modelConfig,
|
||||
systemPreamble,
|
||||
msgs,
|
||||
@@ -127,4 +137,4 @@ fun main() = runBlocking {
|
||||
val response = completion(req)
|
||||
println("\nCompletion Response:")
|
||||
println(response.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,6 +769,8 @@ internal interface IntegrityCheckingUniffiLib : Library {
|
||||
// Integrity check functions only
|
||||
fun uniffi_goose_llm_checksum_func_completion(): Short
|
||||
|
||||
fun uniffi_goose_llm_checksum_func_create_completion_request(): Short
|
||||
|
||||
fun uniffi_goose_llm_checksum_func_create_tool_config(): Short
|
||||
|
||||
fun uniffi_goose_llm_checksum_func_generate_session_name(): Short
|
||||
@@ -821,6 +823,16 @@ internal interface UniffiLib : Library {
|
||||
// FFI functions
|
||||
fun uniffi_goose_llm_fn_func_completion(`req`: RustBuffer.ByValue): Long
|
||||
|
||||
fun uniffi_goose_llm_fn_func_create_completion_request(
|
||||
`providerName`: RustBuffer.ByValue,
|
||||
`providerConfig`: RustBuffer.ByValue,
|
||||
`modelConfig`: RustBuffer.ByValue,
|
||||
`systemPreamble`: RustBuffer.ByValue,
|
||||
`messages`: RustBuffer.ByValue,
|
||||
`extensions`: RustBuffer.ByValue,
|
||||
uniffi_out_err: UniffiRustCallStatus,
|
||||
): RustBuffer.ByValue
|
||||
|
||||
fun uniffi_goose_llm_fn_func_create_tool_config(
|
||||
`name`: RustBuffer.ByValue,
|
||||
`description`: RustBuffer.ByValue,
|
||||
@@ -829,9 +841,17 @@ internal interface UniffiLib : Library {
|
||||
uniffi_out_err: UniffiRustCallStatus,
|
||||
): RustBuffer.ByValue
|
||||
|
||||
fun uniffi_goose_llm_fn_func_generate_session_name(`messages`: RustBuffer.ByValue): Long
|
||||
fun uniffi_goose_llm_fn_func_generate_session_name(
|
||||
`providerName`: RustBuffer.ByValue,
|
||||
`providerConfig`: RustBuffer.ByValue,
|
||||
`messages`: RustBuffer.ByValue,
|
||||
): Long
|
||||
|
||||
fun uniffi_goose_llm_fn_func_generate_tooltip(`messages`: RustBuffer.ByValue): Long
|
||||
fun uniffi_goose_llm_fn_func_generate_tooltip(
|
||||
`providerName`: RustBuffer.ByValue,
|
||||
`providerConfig`: RustBuffer.ByValue,
|
||||
`messages`: RustBuffer.ByValue,
|
||||
): Long
|
||||
|
||||
fun uniffi_goose_llm_fn_func_print_messages(
|
||||
`messages`: RustBuffer.ByValue,
|
||||
@@ -1067,16 +1087,19 @@ private fun uniffiCheckContractApiVersion(lib: IntegrityCheckingUniffiLib) {
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
|
||||
if (lib.uniffi_goose_llm_checksum_func_completion() != 55281.toShort()) {
|
||||
if (lib.uniffi_goose_llm_checksum_func_completion() != 47457.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_goose_llm_checksum_func_create_completion_request() != 51008.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_goose_llm_checksum_func_create_tool_config() != 22809.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_goose_llm_checksum_func_generate_session_name() != 61290.toShort()) {
|
||||
if (lib.uniffi_goose_llm_checksum_func_generate_session_name() != 9810.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_goose_llm_checksum_func_generate_tooltip() != 7529.toShort()) {
|
||||
if (lib.uniffi_goose_llm_checksum_func_generate_tooltip() != 15466.toShort()) {
|
||||
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
||||
}
|
||||
if (lib.uniffi_goose_llm_checksum_func_print_messages() != 30278.toShort()) {
|
||||
@@ -1365,50 +1388,6 @@ public object FfiConverterString : FfiConverter<String, RustBuffer.ByValue> {
|
||||
}
|
||||
}
|
||||
|
||||
data class CompletionRequest(
|
||||
var `providerName`: kotlin.String,
|
||||
var `modelConfig`: ModelConfig,
|
||||
var `systemPreamble`: kotlin.String,
|
||||
var `messages`: List<Message>,
|
||||
var `extensions`: List<ExtensionConfig>,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
public object FfiConverterTypeCompletionRequest : FfiConverterRustBuffer<CompletionRequest> {
|
||||
override fun read(buf: ByteBuffer): CompletionRequest =
|
||||
CompletionRequest(
|
||||
FfiConverterString.read(buf),
|
||||
FfiConverterTypeModelConfig.read(buf),
|
||||
FfiConverterString.read(buf),
|
||||
FfiConverterSequenceTypeMessage.read(buf),
|
||||
FfiConverterSequenceTypeExtensionConfig.read(buf),
|
||||
)
|
||||
|
||||
override fun allocationSize(value: CompletionRequest) =
|
||||
(
|
||||
FfiConverterString.allocationSize(value.`providerName`) +
|
||||
FfiConverterTypeModelConfig.allocationSize(value.`modelConfig`) +
|
||||
FfiConverterString.allocationSize(value.`systemPreamble`) +
|
||||
FfiConverterSequenceTypeMessage.allocationSize(value.`messages`) +
|
||||
FfiConverterSequenceTypeExtensionConfig.allocationSize(value.`extensions`)
|
||||
)
|
||||
|
||||
override fun write(
|
||||
value: CompletionRequest,
|
||||
buf: ByteBuffer,
|
||||
) {
|
||||
FfiConverterString.write(value.`providerName`, buf)
|
||||
FfiConverterTypeModelConfig.write(value.`modelConfig`, buf)
|
||||
FfiConverterString.write(value.`systemPreamble`, buf)
|
||||
FfiConverterSequenceTypeMessage.write(value.`messages`, buf)
|
||||
FfiConverterSequenceTypeExtensionConfig.write(value.`extensions`, buf)
|
||||
}
|
||||
}
|
||||
|
||||
data class CompletionResponse(
|
||||
var `message`: Message,
|
||||
var `model`: kotlin.String,
|
||||
@@ -2814,6 +2793,14 @@ public object FfiConverterSequenceTypeToolConfig : FfiConverterRustBuffer<List<T
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typealias from the type name used in the UDL file to the builtin type. This
|
||||
* is needed because the UDL type name is used in function/method signatures.
|
||||
* It's also what we have an external type that references a custom type.
|
||||
*/
|
||||
public typealias CompletionRequest = kotlin.String
|
||||
public typealias FfiConverterTypeCompletionRequest = FfiConverterString
|
||||
|
||||
/**
|
||||
* Typealias from the type name used in the UDL file to the builtin type. This
|
||||
* is needed because the UDL type name is used in function/method signatures.
|
||||
@@ -2871,6 +2858,28 @@ suspend fun `completion`(`req`: CompletionRequest): CompletionResponse =
|
||||
CompletionException.ErrorHandler,
|
||||
)
|
||||
|
||||
fun `createCompletionRequest`(
|
||||
`providerName`: kotlin.String,
|
||||
`providerConfig`: JsonValueFfi,
|
||||
`modelConfig`: ModelConfig,
|
||||
`systemPreamble`: kotlin.String,
|
||||
`messages`: List<Message>,
|
||||
`extensions`: List<ExtensionConfig>,
|
||||
): CompletionRequest =
|
||||
FfiConverterTypeCompletionRequest.lift(
|
||||
uniffiRustCall { _status ->
|
||||
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_create_completion_request(
|
||||
FfiConverterString.lower(`providerName`),
|
||||
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
|
||||
FfiConverterTypeModelConfig.lower(`modelConfig`),
|
||||
FfiConverterString.lower(`systemPreamble`),
|
||||
FfiConverterSequenceTypeMessage.lower(`messages`),
|
||||
FfiConverterSequenceTypeExtensionConfig.lower(`extensions`),
|
||||
_status,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
fun `createToolConfig`(
|
||||
`name`: kotlin.String,
|
||||
`description`: kotlin.String,
|
||||
@@ -2894,9 +2903,17 @@ fun `createToolConfig`(
|
||||
*/
|
||||
@Throws(ProviderException::class)
|
||||
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
|
||||
suspend fun `generateSessionName`(`messages`: List<Message>): kotlin.String =
|
||||
suspend fun `generateSessionName`(
|
||||
`providerName`: kotlin.String,
|
||||
`providerConfig`: JsonValueFfi,
|
||||
`messages`: List<Message>,
|
||||
): kotlin.String =
|
||||
uniffiRustCallAsync(
|
||||
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_session_name(FfiConverterSequenceTypeMessage.lower(`messages`)),
|
||||
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_session_name(
|
||||
FfiConverterString.lower(`providerName`),
|
||||
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
|
||||
FfiConverterSequenceTypeMessage.lower(`messages`),
|
||||
),
|
||||
{ future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) },
|
||||
{ future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) },
|
||||
{ future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) },
|
||||
@@ -2912,9 +2929,17 @@ suspend fun `generateSessionName`(`messages`: List<Message>): kotlin.String =
|
||||
*/
|
||||
@Throws(ProviderException::class)
|
||||
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
|
||||
suspend fun `generateTooltip`(`messages`: List<Message>): kotlin.String =
|
||||
suspend fun `generateTooltip`(
|
||||
`providerName`: kotlin.String,
|
||||
`providerConfig`: JsonValueFfi,
|
||||
`messages`: List<Message>,
|
||||
): kotlin.String =
|
||||
uniffiRustCallAsync(
|
||||
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_tooltip(FfiConverterSequenceTypeMessage.lower(`messages`)),
|
||||
UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_tooltip(
|
||||
FfiConverterString.lower(`providerName`),
|
||||
FfiConverterTypeJsonValueFfi.lower(`providerConfig`),
|
||||
FfiConverterSequenceTypeMessage.lower(`messages`),
|
||||
),
|
||||
{ future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) },
|
||||
{ future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) },
|
||||
{ future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from goose_llm import (
|
||||
Message, MessageContent, TextContent, ToolRequest, ToolResponse,
|
||||
Role, ModelConfig, ToolApprovalMode,
|
||||
create_tool_config, ExtensionConfig,
|
||||
generate_session_name, generate_tooltip,
|
||||
create_completion_request, completion
|
||||
)
|
||||
|
||||
async def main():
|
||||
now = int(time.time())
|
||||
|
||||
# 1) User sends a plain-text prompt
|
||||
messages = [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
created=now,
|
||||
content=[MessageContent.TEXT(TextContent(text="What is 7 x 6?"))]
|
||||
),
|
||||
|
||||
# 2) Assistant makes a tool request
|
||||
Message(
|
||||
role=Role.ASSISTANT,
|
||||
created=now + 2,
|
||||
content=[MessageContent.TOOL_REQ(ToolRequest(
|
||||
id="calc1",
|
||||
tool_call="""
|
||||
{
|
||||
"status": "success",
|
||||
"value": {
|
||||
"name": "calculator_extension__toolname",
|
||||
"arguments": {
|
||||
"operation": "multiply",
|
||||
"numbers": [7, 6]
|
||||
},
|
||||
"needsApproval": false
|
||||
}
|
||||
}
|
||||
"""
|
||||
))]
|
||||
),
|
||||
|
||||
# 3) User sends tool result
|
||||
Message(
|
||||
role=Role.USER,
|
||||
created=now + 3,
|
||||
content=[MessageContent.TOOL_RESP(ToolResponse(
|
||||
id="calc1",
|
||||
tool_result="""
|
||||
{
|
||||
"status": "success",
|
||||
"value": [
|
||||
{"type": "text", "text": "42"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
))]
|
||||
)
|
||||
]
|
||||
|
||||
provider_name = "databricks"
|
||||
provider_config = f'''{{
|
||||
"host": "{os.environ.get("DATABRICKS_HOST")}",
|
||||
"token": "{os.environ.get("DATABRICKS_TOKEN")}"
|
||||
}}'''
|
||||
|
||||
print(f"Provider Name: {provider_name}")
|
||||
print(f"Provider Config: {provider_config}")
|
||||
|
||||
session_name = await generate_session_name(provider_name, provider_config, messages)
|
||||
print(f"\nSession Name: {session_name}")
|
||||
|
||||
tooltip = await generate_tooltip(provider_name, provider_config, messages)
|
||||
print(f"\nTooltip: {tooltip}")
|
||||
|
||||
model_config = ModelConfig(
|
||||
model_name="goose-gpt-4-1",
|
||||
max_tokens=500,
|
||||
temperature=0.1,
|
||||
context_limit=4096,
|
||||
)
|
||||
|
||||
calculator_tool = create_tool_config(
|
||||
name="calculator",
|
||||
description="Perform basic arithmetic operations",
|
||||
input_schema="""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["operation", "numbers"],
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["add", "subtract", "multiply", "divide"],
|
||||
"description": "The arithmetic operation to perform"
|
||||
},
|
||||
"numbers": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"description": "List of numbers to operate on in order"
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
approval_mode=ToolApprovalMode.AUTO
|
||||
)
|
||||
|
||||
calculator_extension = ExtensionConfig(
|
||||
name="calculator_extension",
|
||||
instructions="This extension provides a calculator tool.",
|
||||
tools=[calculator_tool]
|
||||
)
|
||||
|
||||
system_preamble = "You are a helpful assistant."
|
||||
extensions = [calculator_extension]
|
||||
|
||||
req = create_completion_request(
|
||||
provider_name,
|
||||
provider_config,
|
||||
model_config,
|
||||
system_preamble,
|
||||
messages,
|
||||
extensions
|
||||
)
|
||||
|
||||
resp = await completion(req)
|
||||
print(f"\nCompletion Response:\n{resp.message}")
|
||||
print(f"Msg content: {resp.message.content[0][0]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+21
-12
@@ -31,26 +31,19 @@ Structure:
|
||||
│ └── goose_llm.kt ← auto-generated bindings
|
||||
```
|
||||
|
||||
Create Kotlin 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
|
||||
```
|
||||
|
||||
Download jars in `bindings/kotlin/libs` directory (only need to do this once):
|
||||
```
|
||||
pushd bindings/kotlin/libs/
|
||||
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar
|
||||
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.jar
|
||||
curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar
|
||||
popd
|
||||
```
|
||||
|
||||
#### Kotlin -> Rust: run example
|
||||
|
||||
Compile & Run usage example from Kotlin -> Rust:
|
||||
```
|
||||
```bash
|
||||
pushd bindings/kotlin/
|
||||
|
||||
kotlinc \
|
||||
@@ -68,3 +61,19 @@ java \
|
||||
popd
|
||||
```
|
||||
|
||||
You will have to download jars in `bindings/kotlin/libs` directory (only the first time):
|
||||
```bash
|
||||
pushd bindings/kotlin/libs/
|
||||
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar
|
||||
curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.jar
|
||||
curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar
|
||||
popd
|
||||
```
|
||||
|
||||
#### Python -> Rust: generate bindings, run example
|
||||
|
||||
```bash
|
||||
cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language python --out-dir bindings/python
|
||||
|
||||
DYLD_LIBRARY_PATH=./target/debug python bindings/python/usage.py
|
||||
```
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::vec;
|
||||
use anyhow::Result;
|
||||
use goose_llm::{
|
||||
completion,
|
||||
extractors::generate_tooltip,
|
||||
types::completion::{
|
||||
CompletionRequest, CompletionResponse, ExtensionConfig, ToolApprovalMode, ToolConfig,
|
||||
},
|
||||
@@ -13,8 +14,12 @@ use serde_json::json;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let provider = "databricks";
|
||||
// let model_name = "goose-claude-3-5-sonnet"; // sequential tool calls
|
||||
let model_name = "goose-gpt-4-1"; // parallel tool calls
|
||||
let provider_config = json!({
|
||||
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
|
||||
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
|
||||
});
|
||||
// let model_name = "goose-gpt-4-1"; // parallel tool calls
|
||||
let model_name = "claude-3-5-haiku";
|
||||
let model_config = ModelConfig::new(model_name.to_string());
|
||||
|
||||
let calculator_tool = ToolConfig::new(
|
||||
@@ -92,18 +97,26 @@ async fn main() -> Result<()> {
|
||||
] {
|
||||
println!("\n---------------\n");
|
||||
println!("User Input: {text}");
|
||||
let messages = vec![Message::user().with_text(text)];
|
||||
let completion_response: CompletionResponse = completion(CompletionRequest {
|
||||
provider_name: provider.to_string(),
|
||||
model_config: model_config.clone(),
|
||||
system_preamble: system_preamble.to_string(),
|
||||
messages: messages,
|
||||
extensions: extensions.clone(),
|
||||
})
|
||||
let messages = vec![
|
||||
Message::user().with_text("Hi there!"),
|
||||
Message::assistant().with_text("How can I help?"),
|
||||
Message::user().with_text(text),
|
||||
];
|
||||
let completion_response: CompletionResponse = completion(CompletionRequest::new(
|
||||
provider.to_string(),
|
||||
provider_config.clone(),
|
||||
model_config.clone(),
|
||||
system_preamble.to_string(),
|
||||
messages.clone(),
|
||||
extensions.clone(),
|
||||
))
|
||||
.await?;
|
||||
// Print the response
|
||||
println!("\nCompletion Response:");
|
||||
println!("{}", serde_json::to_string_pretty(&completion_response)?);
|
||||
|
||||
let tooltip = generate_tooltip(provider, provider_config.clone().into(), &messages).await?;
|
||||
println!("\nTooltip: {}", tooltip);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -29,8 +29,12 @@ pub fn print_messages(messages: Vec<Message>) {
|
||||
pub async fn completion(req: CompletionRequest) -> Result<CompletionResponse, CompletionError> {
|
||||
let start_total = Instant::now();
|
||||
|
||||
let provider = create(&req.provider_name, req.model_config)
|
||||
.map_err(|_| CompletionError::UnknownProvider(req.provider_name.to_string()))?;
|
||||
let provider = create(
|
||||
&req.provider_name,
|
||||
req.provider_config.clone(),
|
||||
req.model_config.clone(),
|
||||
)
|
||||
.map_err(|_| CompletionError::UnknownProvider(req.provider_name.to_string()))?;
|
||||
|
||||
let system_prompt = construct_system_prompt(&req.system_preamble, &req.extensions)?;
|
||||
let tools = collect_prefixed_tools(&req.extensions);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::message::Message;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::Provider;
|
||||
use crate::providers::databricks::DatabricksProvider;
|
||||
use crate::providers::create;
|
||||
use crate::providers::errors::ProviderError;
|
||||
use crate::types::core::Role;
|
||||
use crate::{message::Message, types::json_value_ffi::JsonValueFfi};
|
||||
use anyhow::Result;
|
||||
use indoc::indoc;
|
||||
use serde_json::{json, Value};
|
||||
@@ -50,7 +49,20 @@ fn build_system_prompt() -> String {
|
||||
|
||||
/// Generates a short (≤4 words) session name
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn generate_session_name(messages: &[Message]) -> Result<String, ProviderError> {
|
||||
pub async fn generate_session_name(
|
||||
provider_name: &str,
|
||||
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()
|
||||
@@ -75,10 +87,6 @@ pub async fn generate_session_name(messages: &[Message]) -> Result<String, Provi
|
||||
let system_prompt = build_system_prompt();
|
||||
let user_msg_text = format!("Here are the user messages:\n{}", context.join("\n"));
|
||||
|
||||
// Instantiate DatabricksProvider with goose-gpt-4-1
|
||||
let model_cfg = ModelConfig::new("goose-gpt-4-1".to_string()).with_temperature(Some(0.0));
|
||||
let provider = DatabricksProvider::from_env(model_cfg)?;
|
||||
|
||||
// Use `extract` with a simple string schema
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::message::{Message, MessageContent};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::Provider;
|
||||
use crate::providers::databricks::DatabricksProvider;
|
||||
use crate::providers::create;
|
||||
use crate::providers::errors::ProviderError;
|
||||
use crate::types::core::{Content, Role};
|
||||
use crate::types::json_value_ffi::JsonValueFfi;
|
||||
use anyhow::Result;
|
||||
use indoc::indoc;
|
||||
use serde_json::{json, Value};
|
||||
@@ -54,7 +54,20 @@ fn build_system_prompt() -> String {
|
||||
/// Generates a tooltip summarizing the last two messages in the session,
|
||||
/// including any tool calls or results.
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn generate_tooltip(messages: &[Message]) -> Result<String, ProviderError> {
|
||||
pub async fn generate_tooltip(
|
||||
provider_name: &str,
|
||||
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
|
||||
if messages.len() < 2 {
|
||||
return Err(ProviderError::ExecutionError(
|
||||
@@ -128,10 +141,6 @@ pub async fn generate_tooltip(messages: &[Message]) -> Result<String, ProviderEr
|
||||
rendered.join("\n")
|
||||
);
|
||||
|
||||
// Instantiate the provider
|
||||
let model_cfg = ModelConfig::new("goose-gpt-4-1".to_string()).with_temperature(Some(0.0));
|
||||
let provider = DatabricksProvider::from_env(model_cfg)?;
|
||||
|
||||
// Schema wrapping our tooltip string
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
|
||||
@@ -26,66 +26,83 @@ pub const _DATABRICKS_KNOWN_MODELS: &[&str] = &[
|
||||
"databricks-claude-3-7-sonnet",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DatabricksAuth {
|
||||
Token(String),
|
||||
fn default_timeout() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
impl DatabricksAuth {
|
||||
pub fn token(token: String) -> Self {
|
||||
Self::Token(token)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatabricksProviderConfig {
|
||||
pub host: String,
|
||||
pub token: String,
|
||||
#[serde(default)]
|
||||
pub image_format: ImageFormat,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64, // timeout in seconds
|
||||
}
|
||||
|
||||
impl DatabricksProviderConfig {
|
||||
pub fn new(host: String, token: String) -> Self {
|
||||
Self {
|
||||
host,
|
||||
token,
|
||||
image_format: ImageFormat::OpenAi,
|
||||
timeout: default_timeout(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Self {
|
||||
let host = get_env("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST");
|
||||
let token = get_env("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN");
|
||||
Self::new(host, token)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DatabricksProvider {
|
||||
client: Client,
|
||||
host: String,
|
||||
auth: DatabricksAuth,
|
||||
config: DatabricksProviderConfig,
|
||||
model: ModelConfig,
|
||||
image_format: ImageFormat,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl DatabricksProvider {
|
||||
pub fn from_env(model: ModelConfig) -> Self {
|
||||
let config = DatabricksProviderConfig::from_env();
|
||||
DatabricksProvider::from_config(config, model)
|
||||
.expect("Failed to initialize DatabricksProvider")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DatabricksProvider {
|
||||
fn default() -> Self {
|
||||
let config = DatabricksProviderConfig::from_env();
|
||||
let model = ModelConfig::new(DATABRICKS_DEFAULT_MODEL.to_string());
|
||||
DatabricksProvider::from_env(model).expect("Failed to initialize Databricks provider")
|
||||
DatabricksProvider::from_config(config, model)
|
||||
.expect("Failed to initialize DatabricksProvider")
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabricksProvider {
|
||||
pub fn from_env(model: ModelConfig) -> Result<Self> {
|
||||
let host = get_env("DATABRICKS_HOST")?;
|
||||
let api_key = get_env("DATABRICKS_TOKEN")?;
|
||||
|
||||
pub fn from_config(config: DatabricksProviderConfig, model: ModelConfig) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(600))
|
||||
.timeout(Duration::from_secs(config.timeout))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
host,
|
||||
auth: DatabricksAuth::token(api_key),
|
||||
config,
|
||||
model,
|
||||
image_format: ImageFormat::OpenAi,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_auth_header(&self) -> Result<String> {
|
||||
match &self.auth {
|
||||
DatabricksAuth::Token(token) => Ok(format!("Bearer {}", token)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
|
||||
let base_url = Url::parse(&self.host)
|
||||
let base_url = Url::parse(&self.config.host)
|
||||
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;
|
||||
let path = format!("serving-endpoints/{}/invocations", self.model.model_name);
|
||||
let url = base_url.join(&path).map_err(|e| {
|
||||
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
|
||||
})?;
|
||||
|
||||
let auth_header = self.ensure_auth_header().await?;
|
||||
let auth_header = format!("Bearer {}", &self.config.token);
|
||||
let response = self
|
||||
.client
|
||||
.post(url)
|
||||
@@ -189,7 +206,13 @@ impl Provider for DatabricksProvider {
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<ProviderCompleteResponse, ProviderError> {
|
||||
let mut payload = create_request(&self.model, system, messages, tools, &self.image_format)?;
|
||||
let mut payload = create_request(
|
||||
&self.model,
|
||||
system,
|
||||
messages,
|
||||
tools,
|
||||
&self.config.image_format,
|
||||
)?;
|
||||
// Remove the model key which is part of the url with databricks
|
||||
payload
|
||||
.as_object_mut()
|
||||
|
||||
@@ -2,14 +2,28 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{base::Provider, databricks::DatabricksProvider, openai::OpenAiProvider};
|
||||
use super::{
|
||||
base::Provider,
|
||||
databricks::{DatabricksProvider, DatabricksProviderConfig},
|
||||
openai::{OpenAiProvider, OpenAiProviderConfig},
|
||||
};
|
||||
use crate::model::ModelConfig;
|
||||
|
||||
pub fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {
|
||||
pub fn create(
|
||||
name: &str,
|
||||
provider_config: serde_json::Value,
|
||||
model: ModelConfig,
|
||||
) -> Result<Arc<dyn Provider>> {
|
||||
// We use Arc instead of Box to be able to clone for multiple async tasks
|
||||
match name {
|
||||
"openai" => Ok(Arc::new(OpenAiProvider::from_env(model)?)),
|
||||
"databricks" => Ok(Arc::new(DatabricksProvider::from_env(model)?)),
|
||||
"openai" => {
|
||||
let config: OpenAiProviderConfig = serde_json::from_value(provider_config)?;
|
||||
Ok(Arc::new(OpenAiProvider::from_config(config, model)?))
|
||||
}
|
||||
"databricks" => {
|
||||
let config: DatabricksProviderConfig = serde_json::from_value(provider_config)?;
|
||||
Ok(Arc::new(DatabricksProvider::from_config(config, model)?))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unknown provider: {}", name)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{collections::HashMap, time::Duration};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{
|
||||
@@ -20,82 +21,112 @@ use crate::{
|
||||
pub const OPEN_AI_DEFAULT_MODEL: &str = "gpt-4o";
|
||||
pub const _OPEN_AI_KNOWN_MODELS: &[&str] = &["gpt-4o", "gpt-4.1", "o1", "o3", "o4-mini"];
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_base_path() -> String {
|
||||
"v1/chat/completions".to_string()
|
||||
}
|
||||
|
||||
fn default_host() -> String {
|
||||
"https://api.openai.com".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenAiProviderConfig {
|
||||
pub api_key: String,
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
#[serde(default = "default_base_path")]
|
||||
pub base_path: String,
|
||||
#[serde(default)]
|
||||
pub project: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_headers: Option<HashMap<String, String>>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64, // timeout in seconds
|
||||
}
|
||||
|
||||
impl OpenAiProviderConfig {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self {
|
||||
api_key,
|
||||
host: default_host(),
|
||||
organization: None,
|
||||
base_path: default_base_path(),
|
||||
project: None,
|
||||
custom_headers: None,
|
||||
timeout: 600,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Self {
|
||||
let api_key = get_env("OPENAI_API_KEY").expect("Missing OPENAI_API_KEY");
|
||||
Self::new(api_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenAiProvider {
|
||||
client: Client,
|
||||
host: String,
|
||||
base_path: String,
|
||||
api_key: String,
|
||||
organization: Option<String>,
|
||||
project: Option<String>,
|
||||
config: OpenAiProviderConfig,
|
||||
model: ModelConfig,
|
||||
custom_headers: Option<HashMap<String, String>>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl OpenAiProvider {
|
||||
pub fn from_env(model: ModelConfig) -> Self {
|
||||
let config = OpenAiProviderConfig::from_env();
|
||||
OpenAiProvider::from_config(config, model).expect("Failed to initialize OpenAiProvider")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OpenAiProvider {
|
||||
fn default() -> Self {
|
||||
let config = OpenAiProviderConfig::from_env();
|
||||
let model = ModelConfig::new(OPEN_AI_DEFAULT_MODEL.to_string());
|
||||
OpenAiProvider::from_env(model).expect("Failed to initialize OpenAI provider")
|
||||
OpenAiProvider::from_config(config, model).expect("Failed to initialize OpenAiProvider")
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAiProvider {
|
||||
pub fn from_env(model: ModelConfig) -> Result<Self> {
|
||||
let api_key: String = get_env("OPENAI_API_KEY")?;
|
||||
let host: String =
|
||||
get_env("OPENAI_HOST").unwrap_or_else(|_| "https://api.openai.com".to_string());
|
||||
let base_path: String =
|
||||
get_env("OPENAI_BASE_PATH").unwrap_or_else(|_| "v1/chat/completions".to_string());
|
||||
let organization: Option<String> = get_env("OPENAI_ORGANIZATION").ok();
|
||||
let project: Option<String> = get_env("OPENAI_PROJECT").ok();
|
||||
let custom_headers: Option<HashMap<String, String>> = get_env("OPENAI_CUSTOM_HEADERS")
|
||||
.or_else(|_| get_env("OPENAI_CUSTOM_HEADERS"))
|
||||
.ok()
|
||||
.map(parse_custom_headers);
|
||||
// parse get_env("OPENAI_TIMEOUT") to u64 or set default to 600
|
||||
let timeout_secs = get_env("OPENAI_TIMEOUT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(600);
|
||||
pub fn from_config(config: OpenAiProviderConfig, model: ModelConfig) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.timeout(Duration::from_secs(config.timeout))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
host,
|
||||
base_path,
|
||||
api_key,
|
||||
organization,
|
||||
project,
|
||||
config,
|
||||
model,
|
||||
custom_headers,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
async fn post(&self, payload: Value) -> Result<Value, ProviderError> {
|
||||
let base_url = url::Url::parse(&self.host)
|
||||
let base_url = url::Url::parse(&self.config.host)
|
||||
.map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?;
|
||||
let url = base_url.join(&self.base_path).map_err(|e| {
|
||||
let url = base_url.join(&self.config.base_path).map_err(|e| {
|
||||
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
|
||||
})?;
|
||||
|
||||
let mut request = self
|
||||
.client
|
||||
.post(url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
.header("Authorization", format!("Bearer {}", self.config.api_key));
|
||||
|
||||
// Add organization header if present
|
||||
if let Some(org) = &self.organization {
|
||||
if let Some(org) = &self.config.organization {
|
||||
request = request.header("OpenAI-Organization", org);
|
||||
}
|
||||
|
||||
// Add project header if present
|
||||
if let Some(project) = &self.project {
|
||||
if let Some(project) = &self.config.project {
|
||||
request = request.header("OpenAI-Project", project);
|
||||
}
|
||||
|
||||
if let Some(custom_headers) = &self.custom_headers {
|
||||
if let Some(custom_headers) = &self.config.custom_headers {
|
||||
for (key, value) in custom_headers {
|
||||
request = request.header(key, value);
|
||||
}
|
||||
@@ -198,14 +229,3 @@ impl Provider for OpenAiProvider {
|
||||
Ok(ProviderExtractResponse::new(data, model, usage))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_custom_headers(s: String) -> HashMap<String, String> {
|
||||
s.split(',')
|
||||
.filter_map(|header| {
|
||||
let mut parts = header.splitn(2, '=');
|
||||
let key = parts.next().map(|s| s.trim().to_string())?;
|
||||
let value = parts.next().map(|s| s.trim().to_string())?;
|
||||
Some((key, value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -19,12 +19,22 @@ struct OpenAIErrorResponse {
|
||||
error: OpenAIError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default)]
|
||||
pub enum ImageFormat {
|
||||
#[default]
|
||||
OpenAi,
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
/// Timeout in seconds.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Timeout(u32);
|
||||
impl Default for Timeout {
|
||||
fn default() -> Self {
|
||||
Timeout(60)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an image content into an image json based on format
|
||||
pub fn convert_image(image: &ImageContent, image_format: &ImageFormat) -> Value {
|
||||
match image_format {
|
||||
|
||||
@@ -11,17 +11,64 @@ use crate::types::json_value_ffi::JsonValueFfi;
|
||||
use crate::{message::Message, providers::Usage};
|
||||
use crate::{model::ModelConfig, providers::errors::ProviderError};
|
||||
|
||||
// Lifetimes are not supported in Uniffi, cause other languages don't have them
|
||||
// https://github.com/mozilla/uniffi-rs/issues/1526#issuecomment-1528851837
|
||||
#[derive(uniffi::Record)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompletionRequest {
|
||||
pub provider_name: String,
|
||||
pub provider_config: serde_json::Value,
|
||||
pub model_config: ModelConfig,
|
||||
pub system_preamble: String,
|
||||
pub messages: Vec<Message>,
|
||||
pub extensions: Vec<ExtensionConfig>,
|
||||
}
|
||||
|
||||
impl CompletionRequest {
|
||||
pub fn new(
|
||||
provider_name: String,
|
||||
provider_config: serde_json::Value,
|
||||
model_config: ModelConfig,
|
||||
system_preamble: String,
|
||||
messages: Vec<Message>,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider_name,
|
||||
provider_config,
|
||||
model_config,
|
||||
system_preamble,
|
||||
messages,
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn create_completion_request(
|
||||
provider_name: &str,
|
||||
provider_config: JsonValueFfi,
|
||||
model_config: ModelConfig,
|
||||
system_preamble: &str,
|
||||
messages: Vec<Message>,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
) -> CompletionRequest {
|
||||
CompletionRequest::new(
|
||||
provider_name.to_string(),
|
||||
provider_config.into(),
|
||||
model_config,
|
||||
system_preamble.to_string(),
|
||||
messages,
|
||||
extensions,
|
||||
)
|
||||
}
|
||||
|
||||
uniffi::custom_type!(CompletionRequest, String, {
|
||||
lower: |tc: &CompletionRequest| {
|
||||
serde_json::to_string(&tc).unwrap()
|
||||
},
|
||||
try_lift: |s: String| {
|
||||
Ok(serde_json::from_str(&s).unwrap())
|
||||
},
|
||||
});
|
||||
|
||||
// https://mozilla.github.io/uniffi-rs/latest/proc_macro/errors.html
|
||||
#[derive(Debug, Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
@@ -149,7 +196,7 @@ uniffi::custom_type!(ToolConfig, String, {
|
||||
|
||||
// — Register the newtypes with UniFFI, converting via JSON strings —
|
||||
|
||||
#[derive(Debug, Clone, Serialize, uniffi::Record)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct ExtensionConfig {
|
||||
name: String,
|
||||
instructions: Option<String>,
|
||||
|
||||
@@ -4,15 +4,32 @@ use goose_llm::extractors::generate_session_name;
|
||||
use goose_llm::message::Message;
|
||||
use goose_llm::providers::errors::ProviderError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_session_name_success() -> Result<(), ProviderError> {
|
||||
// Load .env for Databricks credentials
|
||||
fn should_run_test() -> Result<(), String> {
|
||||
dotenv().ok();
|
||||
let has_creds =
|
||||
std::env::var("DATABRICKS_HOST").is_ok() && std::env::var("DATABRICKS_TOKEN").is_ok();
|
||||
if !has_creds {
|
||||
println!("Skipping generate_session_name test – Databricks creds not set");
|
||||
return Ok(());
|
||||
if std::env::var("DATABRICKS_HOST").is_err() {
|
||||
return Err("Missing DATABRICKS_HOST".to_string());
|
||||
}
|
||||
if std::env::var("DATABRICKS_TOKEN").is_err() {
|
||||
return Err("Missing DATABRICKS_TOKEN".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _generate_session_name(messages: &[Message]) -> Result<String, ProviderError> {
|
||||
let provider_name = "databricks";
|
||||
let provider_config = serde_json::json!({
|
||||
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
|
||||
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
|
||||
});
|
||||
|
||||
generate_session_name(provider_name, provider_config.into(), messages).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_session_name_success() {
|
||||
if should_run_test().is_err() {
|
||||
println!("Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a few messages with at least two user messages
|
||||
@@ -22,7 +39,10 @@ async fn test_generate_session_name_success() -> Result<(), ProviderError> {
|
||||
Message::user().with_text("What’s the weather in New York tomorrow?"),
|
||||
];
|
||||
|
||||
let name = generate_session_name(&messages).await?;
|
||||
let name = _generate_session_name(&messages)
|
||||
.await
|
||||
.expect("Failed to generate session name");
|
||||
|
||||
println!("Generated session name: {:?}", name);
|
||||
|
||||
// Should be non-empty and at most 4 words
|
||||
@@ -34,20 +54,23 @@ async fn test_generate_session_name_success() -> Result<(), ProviderError> {
|
||||
"Name must be 4 words or less, got {}: {}",
|
||||
word_count,
|
||||
name
|
||||
);
|
||||
|
||||
Ok(())
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_session_name_no_user() {
|
||||
if should_run_test().is_err() {
|
||||
println!("Skipping 'test_generate_session_name_no_user'. Databricks creds not set");
|
||||
return;
|
||||
}
|
||||
|
||||
// No user messages → expect ExecutionError
|
||||
let messages = vec![
|
||||
Message::assistant().with_text("System starting…"),
|
||||
Message::assistant().with_text("All systems go."),
|
||||
];
|
||||
|
||||
let err = generate_session_name(&messages).await;
|
||||
let err = _generate_session_name(&messages).await;
|
||||
assert!(
|
||||
matches!(err, Err(ProviderError::ExecutionError(_))),
|
||||
"Expected ExecutionError when there are no user messages, got: {:?}",
|
||||
|
||||
@@ -6,13 +6,32 @@ use goose_llm::providers::errors::ProviderError;
|
||||
use goose_llm::types::core::{Content, ToolCall};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
|
||||
// Skip if no Databricks creds
|
||||
fn should_run_test() -> Result<(), String> {
|
||||
dotenv().ok();
|
||||
if std::env::var("DATABRICKS_HOST").is_err() || std::env::var("DATABRICKS_TOKEN").is_err() {
|
||||
println!("Skipping simple tooltip test – Databricks creds not set");
|
||||
return Ok(());
|
||||
if std::env::var("DATABRICKS_HOST").is_err() {
|
||||
return Err("Missing DATABRICKS_HOST".to_string());
|
||||
}
|
||||
if std::env::var("DATABRICKS_TOKEN").is_err() {
|
||||
return Err("Missing DATABRICKS_TOKEN".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _generate_tooltip(messages: &[Message]) -> Result<String, ProviderError> {
|
||||
let provider_name = "databricks";
|
||||
let provider_config = serde_json::json!({
|
||||
"host": std::env::var("DATABRICKS_HOST").expect("Missing DATABRICKS_HOST"),
|
||||
"token": std::env::var("DATABRICKS_TOKEN").expect("Missing DATABRICKS_TOKEN"),
|
||||
});
|
||||
|
||||
generate_tooltip(provider_name, provider_config.into(), messages).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_tooltip_simple() {
|
||||
if should_run_test().is_err() {
|
||||
println!("Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Two plain-text messages
|
||||
@@ -21,7 +40,9 @@ async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
|
||||
Message::assistant().with_text("I'm fine, thanks! How can I help?"),
|
||||
];
|
||||
|
||||
let tooltip = generate_tooltip(&messages).await?;
|
||||
let tooltip = _generate_tooltip(&messages)
|
||||
.await
|
||||
.expect("Failed to generate tooltip");
|
||||
println!("Generated tooltip: {:?}", tooltip);
|
||||
|
||||
assert!(!tooltip.trim().is_empty(), "Tooltip must not be empty");
|
||||
@@ -29,16 +50,13 @@ async fn test_generate_tooltip_simple() -> Result<(), ProviderError> {
|
||||
tooltip.len() < 100,
|
||||
"Tooltip should be reasonably short (<100 chars)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
|
||||
// Skip if no Databricks creds
|
||||
dotenv().ok();
|
||||
if std::env::var("DATABRICKS_HOST").is_err() || std::env::var("DATABRICKS_TOKEN").is_err() {
|
||||
println!("Skipping tool‐based tooltip test – Databricks creds not set");
|
||||
return Ok(());
|
||||
async fn test_generate_tooltip_with_tools() {
|
||||
if should_run_test().is_err() {
|
||||
println!("Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Assistant message with a tool request
|
||||
@@ -57,7 +75,9 @@ async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
|
||||
|
||||
let messages = vec![tool_req_msg, tool_resp_msg];
|
||||
|
||||
let tooltip = generate_tooltip(&messages).await?;
|
||||
let tooltip = _generate_tooltip(&messages)
|
||||
.await
|
||||
.expect("Failed to generate tooltip");
|
||||
println!("Generated tooltip (tools): {:?}", tooltip);
|
||||
|
||||
assert!(!tooltip.trim().is_empty(), "Tooltip must not be empty");
|
||||
@@ -65,5 +85,4 @@ async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> {
|
||||
tooltip.len() < 100,
|
||||
"Tooltip should be reasonably short (<100 chars)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ impl ProviderType {
|
||||
|
||||
fn create_provider(&self, cfg: ModelConfig) -> Result<Arc<dyn Provider>> {
|
||||
Ok(match self {
|
||||
ProviderType::OpenAi => Arc::new(OpenAiProvider::from_env(cfg)?),
|
||||
ProviderType::Databricks => Arc::new(DatabricksProvider::from_env(cfg)?),
|
||||
ProviderType::OpenAi => Arc::new(OpenAiProvider::from_env(cfg)),
|
||||
ProviderType::Databricks => Arc::new(DatabricksProvider::from_env(cfg)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user