Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 71 additions & 5 deletions bindings/kotlin/example/Usage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fun main() = runBlocking {
"value": {
"name": "calculator_extension__toolname",
"arguments": {
"operation": "multiply",
"operation": "doesnotexist",
"numbers": [7, 6]
},
"needsApproval": false
Expand All @@ -45,6 +45,51 @@ fun main() = runBlocking {
Message(
role = Role.USER,
created = now + 3,
content = listOf(
MessageContent.ToolResp(
ToolResponse(
id = "calc1",
toolResult = """
{
"status": "error",
"error": "Invalid value for operation: 'doesnotexist'. Valid values are: ['add', 'subtract', 'multiply', 'divide']"
}
""".trimIndent()
)
)
)
),

// 4) Assistant makes a tool request (ToolReq) to calculate 7×6
Message(
role = Role.ASSISTANT,
created = now + 4,
content = listOf(
MessageContent.ToolReq(
ToolRequest(
id = "calc1",
toolCall = """
{
"status": "success",
"value": {
"name": "calculator_extension__toolname",
"arguments": {
"operation": "multiply",
"numbers": [7, 6]
},
"needsApproval": false
}
}
""".trimIndent()
)
)
)
),

// 5) User (on behalf of the tool) responds with the tool result (ToolResp)
Message(
role = Role.USER,
created = now + 5,
content = listOf(
MessageContent.ToolResp(
ToolResponse(
Expand Down Expand Up @@ -124,8 +169,30 @@ fun main() = runBlocking {
val extensions = listOf(calculator_extension)
val systemPreamble = "You are a helpful assistant."

// Testing with tool calls with an error in tool name
val reqToolErr = createCompletionRequest(
providerName,
providerConfig,
modelConfig,
systemPreamble,
messages = listOf(
Message(
role = Role.USER,
created = now,
content = listOf(
MessageContent.Text(
TextContent("What is 7 x 6?")
)
)
)),
extensions = extensions
)

val respToolErr = completion(reqToolErr)
println("\nCompletion Response (one msg):\n${respToolErr.message}")
println()

val req = createCompletionRequest(
val reqAll = createCompletionRequest(
providerName,
providerConfig,
modelConfig,
Expand All @@ -134,14 +201,13 @@ fun main() = runBlocking {
extensions = extensions
)

val response = completion(req)
println("\nCompletion Response:\n${response.message}")
val respAll = completion(reqAll)
println("\nCompletion Response (all msgs):\n${respAll.message}")
println()

// ---- UI Extraction (custom schema) ----
runUiExtraction(providerName, providerConfig)


// --- Prompt Override ---
val prompt_req = createCompletionRequest(
providerName,
Expand Down
102 changes: 102 additions & 0 deletions crates/goose-llm/src/message/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,105 @@ uniffi::custom_type!(Contents, Vec<MessageContent>, {
Ok(Contents::from(contents))
},
});

#[cfg(test)]
mod tests {
use super::*;
use crate::types::core::{Content, TextContent, ToolCall, ToolError};
use serde_json::json;

// ------------------------------------------------------------
// Helpers
// ------------------------------------------------------------
fn make_tool_req_ok(id: &str) -> MessageContent {
let call = ToolCall::new("echo", json!({"text": "hi"}));
MessageContent::tool_request(id, Ok(call).into())
}

fn make_tool_resp_ok(id: &str) -> MessageContent {
let body = vec![Content::Text(TextContent {
text: "done".into(),
})];
MessageContent::tool_response(id, Ok(body).into())
}

fn make_tool_req_err(id: &str) -> MessageContent {
let err = ToolError::NotFound(format!(
"The provided function name '{}' had invalid characters",
"bad$name"
));
MessageContent::tool_request(id, Err(err).into())
}

fn make_tool_resp_err(id: &str) -> MessageContent {
let err = ToolError::InvalidParameters("Could not interpret tool use parameters".into());
MessageContent::tool_response(id, Err(err).into())
}

// ------------------------------------------------------------
// Round-trip: success
// ------------------------------------------------------------
#[test]
fn contents_roundtrip_ok() {
let items: Contents = vec![make_tool_req_ok("req-1"), make_tool_resp_ok("resp-1")].into();

// ---- serialise
let json_str = serde_json::to_string(&items).expect("serialise OK");
println!("JSON: {:?}", json_str);

assert!(
json_str.contains(r#""type":"toolReq""#)
&& json_str.contains(r#""type":"toolResp""#)
&& json_str.contains(r#""status":"success""#),
"JSON should contain both variants and success-status"
);

// ---- deserialise
let parsed: Contents = serde_json::from_str(&json_str).expect("deserialise OK");

assert_eq!(parsed, items, "full round-trip equality");
}

// ------------------------------------------------------------
// Round-trip: error (all variants collapse to ExecutionError)
// ------------------------------------------------------------
#[test]
fn contents_roundtrip_err() {
let original_items: Contents =
vec![make_tool_req_err("req-e"), make_tool_resp_err("resp-e")].into();

// ---- serialise
let json_str = serde_json::to_string(&original_items).expect("serialise OK");
println!("JSON: {:?}", json_str);

assert!(json_str.contains(r#""status":"error""#));

// ---- deserialise
let parsed: Contents = serde_json::from_str(&json_str).expect("deserialise OK");

// ─── validate structure ───────────────────────────────────
assert_eq!(parsed.len(), 2);

// ToolReq error
match &parsed[0] {
MessageContent::ToolReq(req) => match &*req.tool_call {
Err(ToolError::ExecutionError(msg)) => {
assert!(msg.contains("invalid characters"))
}
other => panic!("expected ExecutionError, got {:?}", other),
},
other => panic!("expected ToolReq, got {:?}", other),
}

// ToolResp error
match &parsed[1] {
MessageContent::ToolResp(resp) => match &*resp.tool_result {
Err(ToolError::ExecutionError(msg)) => {
assert!(msg.contains("interpret tool use parameters"))
}
other => panic!("expected ExecutionError, got {:?}", other),
},
other => panic!("expected ToolResp, got {:?}", other),
}
}
}
Loading
Loading