Warn about broad tool response projections
This commit is contained in:
@@ -63,7 +63,9 @@ pub use operation::{
|
||||
pub use protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
|
||||
pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
||||
pub use tool_quality::{
|
||||
ToolQualityFinding, ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode,
|
||||
ToolQualitySeverity, analyze_tool_identity_quality, analyze_tool_schema_quality,
|
||||
ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet, ToolQualityReport,
|
||||
ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
|
||||
analyze_tool_identity_quality, analyze_tool_response_projection_quality,
|
||||
analyze_tool_schema_quality,
|
||||
};
|
||||
pub use workspace::{Workspace, WorkspaceStatus};
|
||||
|
||||
@@ -7,6 +7,7 @@ const MIN_DESCRIPTION_CHARS: usize = 40;
|
||||
const GENERIC_TOOL_NAMES: &[&str] = &["call_api", "manage", "execute", "request"];
|
||||
const MULTI_ACTION_FIELD_NAMES: &[&str] = &["action", "mode", "operation", "type"];
|
||||
const ENUM_LIKE_FIELD_NAMES: &[&str] = &["mode", "type", "status", "category"];
|
||||
const MANY_PROJECTED_FIELDS_THRESHOLD: usize = 12;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -58,6 +59,17 @@ pub struct ToolQualitySchemaNode {
|
||||
pub enum_values: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ToolQualityMappingRule {
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct ToolQualityMappingSet {
|
||||
pub rules: Vec<ToolQualityMappingRule>,
|
||||
}
|
||||
|
||||
impl ToolQualityReport {
|
||||
pub fn new(findings: Vec<ToolQualityFinding>) -> Self {
|
||||
let blocking = findings
|
||||
@@ -175,6 +187,37 @@ pub fn analyze_tool_schema_quality(
|
||||
ToolQualityReport::new(findings)
|
||||
}
|
||||
|
||||
pub fn analyze_tool_response_projection_quality(
|
||||
output_mapping: &ToolQualityMappingSet,
|
||||
) -> ToolQualityReport {
|
||||
let mut findings = Vec::new();
|
||||
|
||||
for (index, rule) in output_mapping.rules.iter().enumerate() {
|
||||
let source = rule.source.trim();
|
||||
if source == "$.response.body" || source == "$.response.data" {
|
||||
findings.push(finding(
|
||||
ToolQualitySeverity::Warning,
|
||||
"response_projection_full_body",
|
||||
"Результат инструмента отдает агенту весь ответ API.",
|
||||
Some("Выберите только поля, которые нужны агенту для ответа пользователю или следующего действия."),
|
||||
Some(&format!("output_mapping.rules.{index}.source")),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if output_mapping.rules.len() >= MANY_PROJECTED_FIELDS_THRESHOLD {
|
||||
findings.push(finding(
|
||||
ToolQualitySeverity::Info,
|
||||
"response_projection_many_fields",
|
||||
"В результат инструмента попадает много полей.",
|
||||
Some("Проверьте, что каждое поле действительно нужно агенту. Лишние данные увеличивают риск ошибок и галлюцинаций."),
|
||||
Some("output_mapping.rules"),
|
||||
));
|
||||
}
|
||||
|
||||
ToolQualityReport::new(findings)
|
||||
}
|
||||
|
||||
fn is_safe_tool_name(value: &str) -> bool {
|
||||
let mut chars = value.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crank_core::{
|
||||
ToolDescription, ToolQualityFinding, ToolQualityReport, ToolQualitySchemaNode,
|
||||
ToolQualitySeverity, analyze_tool_identity_quality, analyze_tool_schema_quality,
|
||||
ToolDescription, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
||||
ToolQualityReport, ToolQualitySchemaNode, ToolQualitySeverity, analyze_tool_identity_quality,
|
||||
analyze_tool_response_projection_quality, analyze_tool_schema_quality,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -181,6 +182,59 @@ fn recommends_enum_for_mode_like_string_parameter() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warns_when_output_mapping_returns_full_response_body() {
|
||||
let mapping = ToolQualityMappingSet {
|
||||
rules: vec![ToolQualityMappingRule {
|
||||
source: "$.response.body".to_owned(),
|
||||
target: "$.output".to_owned(),
|
||||
}],
|
||||
};
|
||||
|
||||
let report = analyze_tool_response_projection_quality(&mapping);
|
||||
|
||||
assert!(has_finding(
|
||||
&report,
|
||||
"response_projection_full_body",
|
||||
ToolQualitySeverity::Warning
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_output_mapping_with_selected_fields() {
|
||||
let mapping = ToolQualityMappingSet {
|
||||
rules: vec![ToolQualityMappingRule {
|
||||
source: "$.response.body.rates.EUR".to_owned(),
|
||||
target: "$.output.rate".to_owned(),
|
||||
}],
|
||||
};
|
||||
|
||||
let report = analyze_tool_response_projection_quality(&mapping);
|
||||
|
||||
assert!(!report.blocking);
|
||||
assert!(report.findings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_large_output_projection_as_info() {
|
||||
let mapping = ToolQualityMappingSet {
|
||||
rules: (0..12)
|
||||
.map(|index| ToolQualityMappingRule {
|
||||
source: format!("$.response.body.field_{index}"),
|
||||
target: format!("$.output.field_{index}"),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let report = analyze_tool_response_projection_quality(&mapping);
|
||||
|
||||
assert!(has_finding(
|
||||
&report,
|
||||
"response_projection_many_fields",
|
||||
ToolQualitySeverity::Info
|
||||
));
|
||||
}
|
||||
|
||||
fn has_finding(report: &ToolQualityReport, code: &str, severity: ToolQualitySeverity) -> bool {
|
||||
report
|
||||
.findings
|
||||
|
||||
Reference in New Issue
Block a user