fix(hands): hand_researcher 参数容错 — LLM不传action字段时自动推断
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
根因: glm等国内LLM调用hand_researcher时不带action字段, 导致"missing field action"反复报错触发LoopGuard拦截。 修复: execute()先尝试严格反序列化,失败时调用infer_action() 从输入字段推断意图: - 有query → search - 有url → fetch - 有urls → summarize - 都没有 → 友好错误提示 验证: 160 tests PASS
This commit is contained in:
@@ -301,6 +301,61 @@ impl ResearcherHand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Infer action from input fields when LLM omits the `action` field.
|
||||||
|
/// Many LLMs (especially non-OpenAI models like glm) call tools without
|
||||||
|
/// including the enum tag, e.g. sending `{"query": "search terms"}` instead
|
||||||
|
/// of `{"action": "search", "query": "search terms"}`.
|
||||||
|
fn infer_action(input: &Value) -> Result<ResearcherAction> {
|
||||||
|
// Has "url" (singular) → fetch
|
||||||
|
if let Some(url) = input.get("url").and_then(|v| v.as_str()) {
|
||||||
|
if !url.is_empty() && url.starts_with("http") {
|
||||||
|
return Ok(ResearcherAction::Fetch { url: url.to_string() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Has "urls" (plural) → summarize
|
||||||
|
if let Some(urls) = input.get("urls").and_then(|v| v.as_array()) {
|
||||||
|
let url_list: Vec<String> = urls.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||||
|
.collect();
|
||||||
|
if !url_list.is_empty() {
|
||||||
|
return Ok(ResearcherAction::Summarize { urls: url_list });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Has "query" (string or object) → search
|
||||||
|
if let Some(query_val) = input.get("query") {
|
||||||
|
let query: ResearchQuery = if query_val.is_string() {
|
||||||
|
// LLM sent plain string: {"query": "search terms"}
|
||||||
|
ResearchQuery {
|
||||||
|
query: query_val.as_str().unwrap_or("").to_string(),
|
||||||
|
engine: SearchEngine::Auto,
|
||||||
|
depth: ResearchDepth::Standard,
|
||||||
|
max_results: 10,
|
||||||
|
include_related: false,
|
||||||
|
time_limit_secs: 60,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LLM sent object: {"query": {"query": "...", "engine": "..."}}
|
||||||
|
serde_json::from_value(query_val.clone()).unwrap_or_else(|_| ResearchQuery {
|
||||||
|
query: query_val.get("query")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
engine: SearchEngine::Auto,
|
||||||
|
depth: ResearchDepth::Standard,
|
||||||
|
max_results: 10,
|
||||||
|
include_related: false,
|
||||||
|
time_limit_secs: 60,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if !query.query.trim().is_empty() {
|
||||||
|
return Ok(ResearcherAction::Search { query });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(zclaw_types::ZclawError::HandError(
|
||||||
|
"无法识别搜索意图:请提供 query(搜索)或 url(获取网页)参数".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute a web search — route to the configured backend
|
/// Execute a web search — route to the configured backend
|
||||||
async fn execute_search(&self, query: &ResearchQuery) -> Result<Vec<SearchResult>> {
|
async fn execute_search(&self, query: &ResearchQuery) -> Result<Vec<SearchResult>> {
|
||||||
query.validate().map_err(|e| zclaw_types::ZclawError::HandError(e))?;
|
query.validate().map_err(|e| zclaw_types::ZclawError::HandError(e))?;
|
||||||
@@ -1030,8 +1085,11 @@ impl Hand for ResearcherHand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
|
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
|
||||||
let action: ResearcherAction = serde_json::from_value(input.clone())
|
// Try strict deserialization first, then fall back to inference
|
||||||
.map_err(|e| zclaw_types::ZclawError::HandError(format!("Invalid action: {}", e)))?;
|
let action: ResearcherAction = match serde_json::from_value(input.clone()) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => Self::infer_action(&input)?,
|
||||||
|
};
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user