Files
tkmind_go/crates/goose-llm/src/extractors/tooltip.rs
T

176 lines
5.4 KiB
Rust

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;
use anyhow::Result;
use indoc::indoc;
use serde_json::{json, Value};
const TOOLTIP_EXAMPLES: &[&str] = &[
"analyzing KPIs",
"detecting anomalies",
"building artifacts in Buildkite",
"categorizing issues",
"checking dependencies",
"collecting feedback",
"deploying changes in AWS",
"drafting report in Google Docs",
"extracting action items",
"generating insights",
"logging issues",
"monitoring tickets in Zendesk",
"notifying design team",
"running integration tests",
"scanning threads in Figma",
"sending reminders in Gmail",
"sending surveys",
"sharing with stakeholders",
"summarizing findings",
"transcribing meeting",
"tracking resolution",
"updating status in Linear",
];
fn build_system_prompt() -> String {
let examples = TOOLTIP_EXAMPLES
.iter()
.map(|e| format!("- {}", e))
.collect::<Vec<_>>()
.join("\n");
indoc! {r#"
You are an assistant that summarizes the recent conversation into a tooltip.
Given the last two messages, reply with only a short tooltip (up to 4 words)
describing what is happening now.
Examples:
"#}
.to_string()
+ &examples
}
/// 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(
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(
"Need at least two messages to generate a tooltip".to_string(),
));
}
// Helper to render a single message's content
fn render_message(m: &Message) -> String {
let mut parts = Vec::new();
for content in m.content.iter() {
match content {
MessageContent::Text(text_block) => {
let txt = text_block.text.trim();
if !txt.is_empty() {
parts.push(txt.to_string());
}
}
MessageContent::ToolReq(req) => {
if let Ok(tool_call) = &req.tool_call.0 {
parts.push(format!(
"called tool '{}' with args {}",
tool_call.name, tool_call.arguments
));
} else if let Err(e) = &req.tool_call.0 {
parts.push(format!("tool request error: {}", e));
}
}
MessageContent::ToolResp(resp) => match &resp.tool_result.0 {
Ok(contents) => {
let results: Vec<String> = contents
.iter()
.map(|c| match c {
Content::Text(t) => t.text.clone(),
Content::Image(_) => "[image]".to_string(),
})
.collect();
parts.push(format!("tool responded with: {}", results.join(" ")));
}
Err(e) => {
parts.push(format!("tool error: {}", e));
}
},
_ => {} // ignore other variants
}
}
let role = match m.role {
Role::User => "User",
Role::Assistant => "Assistant",
};
format!("{}: {}", role, parts.join("; "))
}
// Take the last two messages (in correct chronological order)
let rendered: Vec<String> = messages
.iter()
.rev()
.take(2)
.map(render_message)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let system_prompt = build_system_prompt();
let user_msg_text = format!(
"Here are the last two messages:\n{}\n\nTooltip:",
rendered.join("\n")
);
// Schema wrapping our tooltip string
let schema = json!({
"type": "object",
"properties": {
"tooltip": { "type": "string" }
},
"required": ["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?;
// Pull out the tooltip field
let obj = resp
.data
.as_object()
.ok_or_else(|| ProviderError::ResponseParseError("Expected JSON object".into()))?;
let tooltip = obj
.get("tooltip")
.and_then(Value::as_str)
.ok_or_else(|| {
ProviderError::ResponseParseError("Missing or non-string `tooltip` field".into())
})?
.to_string();
Ok(tooltip)
}