Add tool identity quality checks
Deploy / deploy (push) Successful in 1m34s
CI / Rust Checks (push) Successful in 5m0s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 7s
CI / Frontend E2E (push) Successful in 4m36s

This commit is contained in:
github-ops
2026-06-20 19:53:42 +00:00
parent 3b61d7a25d
commit 2719c5f7bf
5 changed files with 218 additions and 3 deletions
+3 -1
View File
@@ -62,5 +62,7 @@ pub use operation::{
};
pub use protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion};
pub use tool_quality::{ToolQualityFinding, ToolQualityReport, ToolQualitySeverity};
pub use tool_quality::{
ToolQualityFinding, ToolQualityReport, ToolQualitySeverity, analyze_tool_identity_quality,
};
pub use workspace::{Workspace, WorkspaceStatus};
+103
View File
@@ -1,5 +1,11 @@
use serde::{Deserialize, Serialize};
use crate::ToolDescription;
const MAX_TOOL_NAME_LEN: usize = 64;
const MIN_DESCRIPTION_CHARS: usize = 40;
const GENERIC_TOOL_NAMES: &[&str] = &["call_api", "manage", "execute", "request"];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolQualitySeverity {
@@ -38,3 +44,100 @@ impl ToolQualityReport {
Self::new(Vec::new())
}
}
pub fn analyze_tool_identity_quality(
tool_name: &str,
tool_description: &ToolDescription,
) -> ToolQualityReport {
let mut findings = Vec::new();
let normalized_name = tool_name.trim();
if normalized_name.is_empty() {
findings.push(finding(
ToolQualitySeverity::Error,
"tool_name_missing",
"Имя инструмента обязательно.",
Some("Укажите короткое техническое имя в snake_case, например get_customer_profile."),
Some("name"),
));
} else {
if normalized_name.chars().count() > MAX_TOOL_NAME_LEN {
findings.push(finding(
ToolQualitySeverity::Warning,
"tool_name_too_long",
"Имя инструмента слишком длинное.",
Some("Сократите имя до понятного действия и объекта."),
Some("name"),
));
}
if !is_safe_tool_name(normalized_name) {
findings.push(finding(
ToolQualitySeverity::Error,
"tool_name_invalid_format",
"Имя инструмента должно быть в безопасном snake_case формате.",
Some("Используйте только строчные английские буквы, цифры и подчеркивания."),
Some("name"),
));
}
if GENERIC_TOOL_NAMES.contains(&normalized_name) {
findings.push(finding(
ToolQualitySeverity::Warning,
"tool_name_too_generic",
"Имя инструмента слишком общее.",
Some("Назовите действие и объект явно, например create_crm_lead или get_ticket_status."),
Some("name"),
));
}
}
let description = tool_description.description.trim();
if description.is_empty() {
findings.push(finding(
ToolQualitySeverity::Error,
"tool_description_missing",
"Описание инструмента обязательно.",
Some("Опишите, когда агенту нужно использовать этот инструмент и какой результат считается успешным."),
Some("tool_description.description"),
));
} else if description.chars().count() < MIN_DESCRIPTION_CHARS {
findings.push(finding(
ToolQualitySeverity::Warning,
"tool_description_too_short",
"Описание инструмента слишком короткое.",
Some("Добавьте назначение, условия применения и ожидаемый успешный результат."),
Some("tool_description.description"),
));
}
ToolQualityReport::new(findings)
}
fn is_safe_tool_name(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_lowercase() {
return false;
}
chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
}
fn finding(
severity: ToolQualitySeverity,
code: &str,
message: &str,
suggested_action: Option<&str>,
field_path: Option<&str>,
) -> ToolQualityFinding {
ToolQualityFinding {
severity,
code: code.to_owned(),
message: message.to_owned(),
suggested_action: suggested_action.map(ToOwned::to_owned),
field_path: field_path.map(ToOwned::to_owned),
}
}