mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore(release): v0.79.0
This commit is contained in:
@@ -14,6 +14,7 @@ pub mod memory_profile_prompt_service;
|
||||
pub mod memory_rules_loader_service;
|
||||
pub mod memory_source_resolver_service;
|
||||
pub mod novel_service;
|
||||
pub mod request_tool_policy_prompt_service;
|
||||
pub mod sysinfo_service;
|
||||
pub mod update_check_service;
|
||||
pub mod update_window;
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
//! 请求级工具策略提示词服务
|
||||
//!
|
||||
//! 将本次请求的工具偏好(例如“开启联网搜索”)统一转换为系统提示词附加项,
|
||||
//! 避免通过改写用户原始消息来注入策略。
|
||||
//!
|
||||
//! 设计目标:
|
||||
//! - 单一策略入口:Aster 聊天入口与统一执行入口复用同一策略解析/校验逻辑
|
||||
//! - 请求级优先:`effective_web_search = request.web_search ?? mode_default`
|
||||
//! - 配置驱动:工具白/黑名单支持环境变量覆盖,保留扩展性
|
||||
|
||||
use aster::agents::Agent;
|
||||
use aster::tools::ToolContext;
|
||||
use proxycast_agent::event_converter::{TauriAgentEvent, TauriToolResult};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const REQUEST_TOOL_POLICY_MARKER: &str = "【请求级工具策略】";
|
||||
|
||||
const DEFAULT_REQUIRED_TOOLS: &[&str] = &["WebSearch"];
|
||||
const DEFAULT_ALLOWED_TOOLS: &[&str] = &["WebSearch", "WebFetch"];
|
||||
const WEB_SEARCH_REQUIRED_TOOLS_ENV: &str = "PROXYCAST_WEB_SEARCH_REQUIRED_TOOLS";
|
||||
const WEB_SEARCH_ALLOWED_TOOLS_ENV: &str = "PROXYCAST_WEB_SEARCH_ALLOWED_TOOLS";
|
||||
const WEB_SEARCH_DISALLOWED_TOOLS_ENV: &str = "PROXYCAST_WEB_SEARCH_DISALLOWED_TOOLS";
|
||||
const WEB_SEARCH_PREFLIGHT_ENABLED_ENV: &str = "PROXYCAST_WEB_SEARCH_PREFLIGHT_ENABLED";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RequestToolPolicy {
|
||||
/// 本次请求是否开启联网搜索策略
|
||||
pub effective_web_search: bool,
|
||||
/// 必须至少成功一次的工具(默认 WebSearch)
|
||||
pub required_tools: Vec<String>,
|
||||
/// 允许的联网工具集合(默认 WebSearch/WebFetch)
|
||||
pub allowed_tools: Vec<String>,
|
||||
/// 禁止工具集合(可配置)
|
||||
pub disallowed_tools: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ToolAttemptRecord {
|
||||
pub tool_id: String,
|
||||
pub tool_name: String,
|
||||
pub success: Option<bool>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WebSearchExecutionTracker {
|
||||
ordered_tool_ids: Vec<String>,
|
||||
attempts_by_id: HashMap<String, ToolAttemptRecord>,
|
||||
}
|
||||
|
||||
impl WebSearchExecutionTracker {
|
||||
pub fn record_tool_start(
|
||||
&mut self,
|
||||
policy: &RequestToolPolicy,
|
||||
tool_id: &str,
|
||||
tool_name: &str,
|
||||
) {
|
||||
if !policy.effective_web_search || tool_id.trim().is_empty() || tool_name.trim().is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.attempts_by_id.contains_key(tool_id) {
|
||||
self.ordered_tool_ids.push(tool_id.to_string());
|
||||
self.attempts_by_id.insert(
|
||||
tool_id.to_string(),
|
||||
ToolAttemptRecord {
|
||||
tool_id: tool_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
success: None,
|
||||
error: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_tool_end(
|
||||
&mut self,
|
||||
policy: &RequestToolPolicy,
|
||||
tool_id: &str,
|
||||
success: bool,
|
||||
error: Option<&str>,
|
||||
) {
|
||||
if !policy.effective_web_search || tool_id.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(record) = self.attempts_by_id.get_mut(tool_id) {
|
||||
record.success = Some(success);
|
||||
record.error = error
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_web_search_requirement(
|
||||
&self,
|
||||
policy: &RequestToolPolicy,
|
||||
) -> Result<(), String> {
|
||||
if !policy.effective_web_search {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let disallowed_attempts: Vec<&ToolAttemptRecord> = self
|
||||
.ordered_tool_ids
|
||||
.iter()
|
||||
.filter_map(|tool_id| self.attempts_by_id.get(tool_id))
|
||||
.filter(|record| matches_tool_list(&record.tool_name, &policy.disallowed_tools))
|
||||
.collect();
|
||||
if !disallowed_attempts.is_empty() {
|
||||
let disallowed_names = disallowed_attempts
|
||||
.iter()
|
||||
.map(|record| record.tool_name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return Err(format!(
|
||||
"联网搜索策略阻止了禁止工具调用: {}。\n尝试记录: {}",
|
||||
disallowed_names,
|
||||
self.format_attempts()
|
||||
));
|
||||
}
|
||||
|
||||
let required_attempts: Vec<&ToolAttemptRecord> = self
|
||||
.ordered_tool_ids
|
||||
.iter()
|
||||
.filter_map(|tool_id| self.attempts_by_id.get(tool_id))
|
||||
.filter(|record| policy.matches_any_required_tool(&record.tool_name))
|
||||
.collect();
|
||||
|
||||
if required_attempts.is_empty() {
|
||||
return Err(format!(
|
||||
"联网搜索已开启,但未检测到必需工具调用。必须先调用 {} 至少一次后再给出最终答复。\n尝试记录: {}",
|
||||
policy.required_tools.join(", "),
|
||||
self.format_attempts()
|
||||
));
|
||||
}
|
||||
|
||||
if required_attempts
|
||||
.iter()
|
||||
.any(|record| record.success.unwrap_or(false))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"联网搜索已开启,但必需工具调用全部失败,无法给出符合约束的最终答复。\n失败原因与尝试记录: {}",
|
||||
self.format_attempts()
|
||||
))
|
||||
}
|
||||
|
||||
pub fn format_attempts(&self) -> String {
|
||||
if self.ordered_tool_ids.is_empty() {
|
||||
return "无工具调用".to_string();
|
||||
}
|
||||
|
||||
self.ordered_tool_ids
|
||||
.iter()
|
||||
.filter_map(|tool_id| self.attempts_by_id.get(tool_id))
|
||||
.map(|record| {
|
||||
let status = match record.success {
|
||||
Some(true) => "success".to_string(),
|
||||
Some(false) => {
|
||||
format!("failed({})", record.error.as_deref().unwrap_or("unknown"))
|
||||
}
|
||||
None => "pending".to_string(),
|
||||
};
|
||||
format!("{}#{}:{}", record.tool_name, record.tool_id, status)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreflightToolExecution {
|
||||
pub events: Vec<TauriAgentEvent>,
|
||||
}
|
||||
|
||||
impl PreflightToolExecution {
|
||||
fn none() -> Self {
|
||||
Self { events: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestToolPolicy {
|
||||
pub fn matches_any_required_tool(&self, tool_name: &str) -> bool {
|
||||
matches_tool_list(tool_name, &self.required_tools)
|
||||
}
|
||||
|
||||
pub fn matches_any_allowed_tool(&self, tool_name: &str) -> bool {
|
||||
matches_tool_list(tool_name, &self.allowed_tools)
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析请求级工具策略
|
||||
///
|
||||
/// 规则:
|
||||
/// - `effective_web_search = request_web_search.unwrap_or(mode_default)`
|
||||
/// - 白/黑名单支持环境变量覆盖:
|
||||
/// - `PROXYCAST_WEB_SEARCH_REQUIRED_TOOLS`
|
||||
/// - `PROXYCAST_WEB_SEARCH_ALLOWED_TOOLS`
|
||||
/// - `PROXYCAST_WEB_SEARCH_DISALLOWED_TOOLS`
|
||||
pub fn resolve_request_tool_policy(
|
||||
request_web_search: Option<bool>,
|
||||
mode_default: bool,
|
||||
) -> RequestToolPolicy {
|
||||
let effective_web_search = request_web_search.unwrap_or(mode_default);
|
||||
let required_tools = parse_tool_list_env(WEB_SEARCH_REQUIRED_TOOLS_ENV, DEFAULT_REQUIRED_TOOLS);
|
||||
let mut allowed_tools =
|
||||
parse_tool_list_env(WEB_SEARCH_ALLOWED_TOOLS_ENV, DEFAULT_ALLOWED_TOOLS);
|
||||
let disallowed_tools = parse_tool_list_env(WEB_SEARCH_DISALLOWED_TOOLS_ENV, &[]);
|
||||
|
||||
for required in &required_tools {
|
||||
if !allowed_tools
|
||||
.iter()
|
||||
.any(|candidate| is_same_tool(candidate, required))
|
||||
{
|
||||
allowed_tools.push(required.clone());
|
||||
}
|
||||
}
|
||||
|
||||
RequestToolPolicy {
|
||||
effective_web_search,
|
||||
required_tools,
|
||||
allowed_tools,
|
||||
disallowed_tools,
|
||||
}
|
||||
}
|
||||
|
||||
/// 合并请求级工具策略到系统提示词
|
||||
///
|
||||
/// - `effective_web_search=false`:保持原始 system prompt 不变
|
||||
/// - 已包含 marker 时:不重复追加
|
||||
pub fn merge_system_prompt_with_request_tool_policy(
|
||||
base_prompt: Option<String>,
|
||||
policy: &RequestToolPolicy,
|
||||
) -> Option<String> {
|
||||
if !policy.effective_web_search {
|
||||
return base_prompt;
|
||||
}
|
||||
|
||||
let disallowed_line = if policy.disallowed_tools.is_empty() {
|
||||
"无".to_string()
|
||||
} else {
|
||||
policy.disallowed_tools.join(", ")
|
||||
};
|
||||
|
||||
let policy_prompt = format!(
|
||||
"{REQUEST_TOOL_POLICY_MARKER}\n\
|
||||
- 用户在本次请求中已开启“联网搜索”开关。\n\
|
||||
- 必须先调用 {} 至少一次(必要时再调用 WebFetch),再输出最终答复。\n\
|
||||
- 若工具调用失败,必须返回失败原因与尝试记录;不要在未完成必需工具调用前直接给最终结论。\n\
|
||||
- 允许工具: {}\n\
|
||||
- 禁止工具: {}",
|
||||
policy.required_tools.join(", "),
|
||||
policy.allowed_tools.join(", "),
|
||||
disallowed_line
|
||||
);
|
||||
|
||||
match base_prompt {
|
||||
Some(base) => {
|
||||
if base.contains(REQUEST_TOOL_POLICY_MARKER) {
|
||||
Some(base)
|
||||
} else if base.trim().is_empty() {
|
||||
Some(policy_prompt)
|
||||
} else {
|
||||
Some(format!("{base}\n\n{policy_prompt}"))
|
||||
}
|
||||
}
|
||||
None => Some(policy_prompt),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tool_list_env(key: &str, default_values: &[&str]) -> Vec<String> {
|
||||
let from_env = std::env::var(key)
|
||||
.ok()
|
||||
.map(|raw| parse_tool_list(&raw))
|
||||
.filter(|tools| !tools.is_empty());
|
||||
|
||||
let values =
|
||||
from_env.unwrap_or_else(|| default_values.iter().map(|item| item.to_string()).collect());
|
||||
dedup_tools(values)
|
||||
}
|
||||
|
||||
fn parse_tool_list(raw: &str) -> Vec<String> {
|
||||
raw.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(|item| item.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn dedup_tools(values: Vec<String>) -> Vec<String> {
|
||||
let mut result: Vec<String> = Vec::new();
|
||||
for value in values {
|
||||
if !result.iter().any(|existing| is_same_tool(existing, &value)) {
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn matches_tool_list(tool_name: &str, list: &[String]) -> bool {
|
||||
list.iter()
|
||||
.any(|candidate| is_same_tool(tool_name, candidate))
|
||||
}
|
||||
|
||||
fn is_same_tool(a: &str, b: &str) -> bool {
|
||||
let normalized_a = normalize_tool_name(a);
|
||||
let normalized_b = normalize_tool_name(b);
|
||||
if normalized_a.is_empty() || normalized_b.is_empty() {
|
||||
return false;
|
||||
}
|
||||
normalized_a == normalized_b
|
||||
|| normalized_a.contains(&normalized_b)
|
||||
|| normalized_b.contains(&normalized_a)
|
||||
}
|
||||
|
||||
fn normalize_tool_name(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter(|ch| ch.is_ascii_alphanumeric())
|
||||
.flat_map(|ch| ch.to_lowercase())
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// 当开启联网搜索时,在正式回复前执行一次 WebSearch 预调用。
|
||||
///
|
||||
/// 目标:
|
||||
/// - 通过执行层保证至少一次 WebSearch 调用(而非仅依赖提示词)
|
||||
/// - 统一生成 tool_start/tool_end 事件,供前端落地
|
||||
/// - 若预调用失败,返回失败原因并由上层中断本次回答
|
||||
pub async fn execute_web_search_preflight_if_needed(
|
||||
agent: &Agent,
|
||||
session_id: &str,
|
||||
message_text: &str,
|
||||
working_directory: Option<&Path>,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
policy: &RequestToolPolicy,
|
||||
tracker: &mut WebSearchExecutionTracker,
|
||||
) -> Result<PreflightToolExecution, String> {
|
||||
if !policy.effective_web_search || !is_web_search_preflight_enabled() {
|
||||
return Ok(PreflightToolExecution::none());
|
||||
}
|
||||
|
||||
let registry_arc = agent.tool_registry().clone();
|
||||
let registry = registry_arc.read().await;
|
||||
let available_tools = registry.get_definitions();
|
||||
let preflight_tool = available_tools
|
||||
.iter()
|
||||
.find(|definition| {
|
||||
policy.matches_any_required_tool(&definition.name)
|
||||
&& normalize_tool_name(&definition.name).contains("websearch")
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"联网搜索已开启,但未找到可执行的必需工具定义。required_tools={}, available_tools={}",
|
||||
policy.required_tools.join(", "),
|
||||
available_tools
|
||||
.iter()
|
||||
.map(|definition| definition.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
})?;
|
||||
|
||||
let query = derive_preflight_query(message_text);
|
||||
let params = serde_json::json!({ "query": query });
|
||||
let arguments = serde_json::to_string(¶ms).ok();
|
||||
let tool_id = format!("preflight-websearch-{}", Uuid::new_v4());
|
||||
tracker.record_tool_start(policy, &tool_id, &preflight_tool.name);
|
||||
|
||||
let mut context = ToolContext::new(
|
||||
working_directory
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| std::env::current_dir().ok())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.with_session_id(session_id.to_string());
|
||||
if let Some(token) = cancel_token {
|
||||
context = context.with_cancellation_token(token);
|
||||
}
|
||||
|
||||
let mut events = vec![TauriAgentEvent::ToolStart {
|
||||
tool_name: preflight_tool.name.clone(),
|
||||
tool_id: tool_id.clone(),
|
||||
arguments,
|
||||
}];
|
||||
|
||||
let result = registry
|
||||
.execute(&preflight_tool.name, params, &context, None)
|
||||
.await
|
||||
.map_err(|error| format!("执行 WebSearch 预调用失败: {}", error.to_string()));
|
||||
|
||||
match result {
|
||||
Ok(tool_result) => {
|
||||
tracker.record_tool_end(
|
||||
policy,
|
||||
&tool_id,
|
||||
tool_result.success,
|
||||
tool_result.error.as_deref(),
|
||||
);
|
||||
let event = TauriAgentEvent::ToolEnd {
|
||||
tool_id,
|
||||
result: TauriToolResult {
|
||||
success: tool_result.success,
|
||||
output: tool_result.output.unwrap_or_default(),
|
||||
error: tool_result.error,
|
||||
images: None,
|
||||
},
|
||||
};
|
||||
events.push(event);
|
||||
|
||||
if events
|
||||
.last()
|
||||
.and_then(|event| match event {
|
||||
TauriAgentEvent::ToolEnd { result, .. } => Some(result.success),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Ok(PreflightToolExecution { events })
|
||||
} else {
|
||||
let failure = events.last().and_then(|event| match event {
|
||||
TauriAgentEvent::ToolEnd { result, .. } => result.error.clone(),
|
||||
_ => None,
|
||||
});
|
||||
Err(format!(
|
||||
"联网搜索预调用失败: {}",
|
||||
failure.unwrap_or_else(|| "unknown".to_string())
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracker.record_tool_end(policy, &tool_id, false, Some(error.as_str()));
|
||||
events.push(TauriAgentEvent::ToolEnd {
|
||||
tool_id,
|
||||
result: TauriToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(error.clone()),
|
||||
images: None,
|
||||
},
|
||||
});
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_web_search_preflight_enabled() -> bool {
|
||||
match std::env::var(WEB_SEARCH_PREFLIGHT_ENABLED_ENV) {
|
||||
Ok(raw) => match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"0" | "false" | "no" | "off" => false,
|
||||
_ => true,
|
||||
},
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_preflight_query(message_text: &str) -> String {
|
||||
let trimmed = message_text.trim();
|
||||
if trimmed.chars().count() >= 2 {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed.is_empty() {
|
||||
return "最新信息".to_string();
|
||||
}
|
||||
|
||||
// 兜底补齐最短长度,避免触发 WebSearch.query minLength 校验失败
|
||||
let mut fallback = trimmed.to_string();
|
||||
while fallback.chars().count() < 2 {
|
||||
fallback.push_str(" 信息");
|
||||
}
|
||||
fallback
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolves_effective_web_search_with_request_override() {
|
||||
let policy = resolve_request_tool_policy(Some(false), true);
|
||||
assert!(!policy.effective_web_search);
|
||||
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
assert!(policy.effective_web_search);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_effective_web_search_with_mode_default() {
|
||||
let policy = resolve_request_tool_policy(None, true);
|
||||
assert!(policy.effective_web_search);
|
||||
|
||||
let policy = resolve_request_tool_policy(None, false);
|
||||
assert!(!policy.effective_web_search);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_original_prompt_when_disabled() {
|
||||
let base = Some("base".to_string());
|
||||
let policy = resolve_request_tool_policy(Some(false), false);
|
||||
assert_eq!(
|
||||
merge_system_prompt_with_request_tool_policy(base.clone(), &policy),
|
||||
base
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appends_policy_prompt_when_enabled() {
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
let merged =
|
||||
merge_system_prompt_with_request_tool_policy(Some("base".to_string()), &policy)
|
||||
.expect("merged prompt should exist");
|
||||
assert!(merged.contains(REQUEST_TOOL_POLICY_MARKER));
|
||||
assert!(merged.contains("必须先调用"));
|
||||
assert!(merged.contains("WebSearch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_when_marker_exists() {
|
||||
let base = Some(format!("{REQUEST_TOOL_POLICY_MARKER}\nexists"));
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
assert_eq!(
|
||||
merge_system_prompt_with_request_tool_policy(base.clone(), &policy),
|
||||
base
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_requires_websearch_when_enabled() {
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
let mut tracker = WebSearchExecutionTracker::default();
|
||||
tracker.record_tool_start(&policy, "tool-1", "WebFetch");
|
||||
tracker.record_tool_end(&policy, "tool-1", true, None);
|
||||
let err = tracker
|
||||
.validate_web_search_requirement(&policy)
|
||||
.expect_err("missing web search should fail");
|
||||
assert!(err.contains("未检测到必需工具调用"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_accepts_successful_websearch() {
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
let mut tracker = WebSearchExecutionTracker::default();
|
||||
tracker.record_tool_start(&policy, "tool-1", "WebSearch");
|
||||
tracker.record_tool_end(&policy, "tool-1", true, None);
|
||||
assert!(tracker.validate_web_search_requirement(&policy).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_reports_failure_record() {
|
||||
let policy = resolve_request_tool_policy(Some(true), false);
|
||||
let mut tracker = WebSearchExecutionTracker::default();
|
||||
tracker.record_tool_start(&policy, "tool-1", "WebSearch");
|
||||
tracker.record_tool_end(&policy, "tool-1", false, Some("network timeout"));
|
||||
let err = tracker
|
||||
.validate_web_search_requirement(&policy)
|
||||
.expect_err("failed required tool should fail");
|
||||
assert!(err.contains("network timeout"));
|
||||
assert!(err.contains("尝试记录"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user