huskies: merge 855
This commit is contained in:
@@ -7,6 +7,8 @@ use serde_json::{Value, json};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::agent_log::AgentLogWriter;
|
||||
use crate::http::mcp::dispatch::dispatch_tool_call;
|
||||
use crate::http::mcp::tools_list::list_tools;
|
||||
use crate::slog;
|
||||
|
||||
use super::super::{AgentEvent, TokenUsage};
|
||||
@@ -65,14 +67,16 @@ impl AgentRuntime for OpenAiRuntime {
|
||||
.unwrap_or_else(|| "gpt-4o".to_string())
|
||||
};
|
||||
|
||||
let mcp_port = ctx.mcp_port;
|
||||
let mcp_base = format!("http://localhost:{mcp_port}/mcp");
|
||||
let app_ctx = ctx
|
||||
.app_ctx
|
||||
.clone()
|
||||
.ok_or_else(|| "OpenAI runtime requires app_ctx to be set".to_string())?;
|
||||
|
||||
let client = Client::new();
|
||||
let cancelled = Arc::clone(&self.cancelled);
|
||||
|
||||
// Step 1: Fetch MCP tool definitions and convert to OpenAI format.
|
||||
let openai_tools = fetch_and_convert_mcp_tools(&client, &mcp_base).await?;
|
||||
let openai_tools = convert_mcp_tools_to_openai();
|
||||
|
||||
// Step 2: Build the initial conversation messages.
|
||||
let system_text = build_system_text(&ctx);
|
||||
@@ -248,7 +252,7 @@ impl AgentRuntime for OpenAiRuntime {
|
||||
text: format!("\n[Tool call: {tool_name}]\n"),
|
||||
});
|
||||
|
||||
let tool_result = call_mcp_tool(&client, &mcp_base, tool_name, &args).await;
|
||||
let tool_result = dispatch_tool_call(tool_name, args.clone(), &app_ctx).await;
|
||||
|
||||
let result_content = match &tool_result {
|
||||
Ok(result) => {
|
||||
@@ -313,38 +317,13 @@ fn build_system_text(ctx: &RuntimeContext) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch MCP tool definitions from huskies' MCP server and convert
|
||||
/// them to OpenAI function-calling format.
|
||||
async fn fetch_and_convert_mcp_tools(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
) -> Result<Vec<Value>, String> {
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(mcp_base)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch MCP tools: {e}"))?;
|
||||
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MCP tools response: {e}"))?;
|
||||
|
||||
let tools = body["result"]["tools"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "No tools array in MCP response".to_string())?;
|
||||
/// Load MCP tool definitions directly and convert to OpenAI function-calling format.
|
||||
fn convert_mcp_tools_to_openai() -> Vec<Value> {
|
||||
let tools = list_tools();
|
||||
|
||||
let mut openai_tools = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
for tool in &tools {
|
||||
let name = tool["name"].as_str().unwrap_or("").to_string();
|
||||
let description = tool["description"].as_str().unwrap_or("").to_string();
|
||||
|
||||
@@ -370,7 +349,7 @@ async fn fetch_and_convert_mcp_tools(
|
||||
"[openai] Loaded {} MCP tools as function definitions",
|
||||
openai_tools.len()
|
||||
);
|
||||
Ok(openai_tools)
|
||||
openai_tools
|
||||
}
|
||||
|
||||
/// Convert an MCP inputSchema (JSON Schema) to OpenAI-compatible
|
||||
@@ -435,53 +414,6 @@ fn clean_schema_properties(properties: &Value) -> Value {
|
||||
Value::Object(cleaned)
|
||||
}
|
||||
|
||||
/// Call an MCP tool via huskies' MCP server.
|
||||
async fn call_mcp_tool(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
tool_name: &str,
|
||||
args: &Value,
|
||||
) -> Result<String, String> {
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": args
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(mcp_base)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("MCP tool call failed: {e}"))?;
|
||||
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MCP tool response: {e}"))?;
|
||||
|
||||
if let Some(error) = body.get("error") {
|
||||
let msg = error["message"].as_str().unwrap_or("Unknown MCP error");
|
||||
return Err(format!("MCP tool '{tool_name}' error: {msg}"));
|
||||
}
|
||||
|
||||
// MCP tools/call returns { result: { content: [{ type: "text", text: "..." }] } }
|
||||
let content = &body["result"]["content"];
|
||||
if let Some(arr) = content.as_array() {
|
||||
let texts: Vec<&str> = arr.iter().filter_map(|c| c["text"].as_str()).collect();
|
||||
if !texts.is_empty() {
|
||||
return Ok(texts.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to serializing the entire result.
|
||||
Ok(body["result"].to_string())
|
||||
}
|
||||
|
||||
/// Parse token usage from an OpenAI API response.
|
||||
fn parse_usage(response: &Value) -> Option<TokenUsage> {
|
||||
let usage = response.get("usage")?;
|
||||
@@ -506,6 +438,12 @@ fn parse_usage(response: &Value) -> Option<TokenUsage> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::context::AppContext;
|
||||
|
||||
fn test_app_ctx() -> Arc<AppContext> {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
Arc::new(AppContext::new_test(tmp.path().to_path_buf()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_mcp_schema_simple_object() {
|
||||
@@ -616,7 +554,7 @@ mod tests {
|
||||
prompt: "Do the thing".to_string(),
|
||||
cwd: "/tmp/wt".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
@@ -634,7 +572,7 @@ mod tests {
|
||||
prompt: "Do the thing".to_string(),
|
||||
cwd: "/tmp/wt".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
@@ -685,7 +623,7 @@ mod tests {
|
||||
prompt: "test".to_string(),
|
||||
cwd: "/tmp".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
@@ -702,7 +640,7 @@ mod tests {
|
||||
prompt: "test".to_string(),
|
||||
cwd: "/tmp".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user