220 lines
9.4 KiB
Rust
220 lines
9.4 KiB
Rust
use crank_adapter_rest::RestAdapterError;
|
||
use crank_runtime::RuntimeError;
|
||
use serde::Serialize;
|
||
use serde_json::{Value, json};
|
||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||
pub struct ToolErrorContract {
|
||
pub code: &'static str,
|
||
pub error_code: &'static str,
|
||
pub message: String,
|
||
pub recoverable: bool,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub suggested_action: Option<&'static str>,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub upstream_status: Option<u16>,
|
||
pub request_id: String,
|
||
}
|
||
|
||
pub fn tool_error_contract_from_runtime(
|
||
error: &RuntimeError,
|
||
request_id: &str,
|
||
) -> ToolErrorContract {
|
||
let error_code = runtime_error_code(error);
|
||
ToolErrorContract {
|
||
code: error_code,
|
||
error_code,
|
||
message: safe_runtime_error_message(error),
|
||
recoverable: is_recoverable_runtime_error(error),
|
||
suggested_action: suggested_action(error),
|
||
upstream_status: upstream_status(error),
|
||
request_id: request_id.to_owned(),
|
||
}
|
||
}
|
||
|
||
pub fn generic_tool_error_contract(
|
||
error_code: &'static str,
|
||
message: impl Into<String>,
|
||
request_id: &str,
|
||
recoverable: bool,
|
||
suggested_action: Option<&'static str>,
|
||
) -> ToolErrorContract {
|
||
ToolErrorContract {
|
||
code: error_code,
|
||
error_code,
|
||
message: message.into(),
|
||
recoverable,
|
||
suggested_action,
|
||
upstream_status: None,
|
||
request_id: request_id.to_owned(),
|
||
}
|
||
}
|
||
|
||
pub fn tool_error_text(error: &ToolErrorContract) -> String {
|
||
match error.suggested_action {
|
||
Some(action) => format!("{} {}", error.message, action),
|
||
None => error.message.clone(),
|
||
}
|
||
}
|
||
|
||
pub fn tool_error_value(error: &ToolErrorContract) -> Value {
|
||
serde_json::to_value(error).unwrap_or_else(|_| {
|
||
json!({
|
||
"code": "runtime_error",
|
||
"error_code": "runtime_error",
|
||
"message": "Не удалось выполнить инструмент.",
|
||
"recoverable": false,
|
||
"request_id": error.request_id
|
||
})
|
||
})
|
||
}
|
||
|
||
pub fn runtime_error_code(error: &RuntimeError) -> &'static str {
|
||
match error {
|
||
RuntimeError::Schema(_) => "schema_validation_error",
|
||
RuntimeError::Mapping(_) => "mapping_error",
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => {
|
||
upstream_status_code(*status)
|
||
}
|
||
RuntimeError::RestAdapter(RestAdapterError::Transport(_)) => "upstream_transport_error",
|
||
RuntimeError::RestAdapter(_) => "adapter_execution_error",
|
||
RuntimeError::ProtocolAdapter(_) => "adapter_execution_error",
|
||
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
|
||
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
|
||
RuntimeError::UnsupportedExecutionMode { .. } => "streaming_mode_error",
|
||
RuntimeError::InvalidPreparedRequest { .. } => "runtime_error",
|
||
RuntimeError::ConfirmationRequired { .. } => "confirmation_required",
|
||
RuntimeError::InvalidConfirmationToken { .. } => "invalid_confirmation_token",
|
||
RuntimeError::ConfirmationStoreUnavailable { .. } => "confirmation_unavailable",
|
||
RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found",
|
||
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
|
||
"secret_not_found"
|
||
}
|
||
RuntimeError::InvalidAuthSecretValue { .. } => "secret_value_error",
|
||
RuntimeError::SecretCrypto { .. } => "secret_crypto_error",
|
||
}
|
||
}
|
||
|
||
fn upstream_status_code(status: u16) -> &'static str {
|
||
match status {
|
||
401 | 403 => "upstream_auth_error",
|
||
404 => "upstream_not_found",
|
||
408 | 429 => "upstream_rate_limited",
|
||
500..=599 => "upstream_server_error",
|
||
_ => "upstream_status_error",
|
||
}
|
||
}
|
||
|
||
fn safe_runtime_error_message(error: &RuntimeError) -> String {
|
||
match error {
|
||
RuntimeError::Schema(_) => "Входные параметры не прошли проверку схемы.".to_owned(),
|
||
RuntimeError::Mapping(_) => {
|
||
"Не удалось сопоставить параметры инструмента с API-запросом.".to_owned()
|
||
}
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => {
|
||
format!("Внешний API вернул HTTP {}.", status)
|
||
}
|
||
RuntimeError::RestAdapter(RestAdapterError::Transport(_)) => {
|
||
"Не удалось подключиться к внешнему API.".to_owned()
|
||
}
|
||
RuntimeError::RestAdapter(_) => "Не удалось выполнить запрос к внешнему API.".to_owned(),
|
||
RuntimeError::ProtocolAdapter(_) => "Не удалось выполнить протокольный адаптер.".to_owned(),
|
||
RuntimeError::UnsupportedProtocol { .. } => {
|
||
"Протокол операции не поддерживается.".to_owned()
|
||
}
|
||
RuntimeError::ConcurrencyLimitExceeded { .. } => {
|
||
"Сервис временно перегружен и не может выполнить инструмент.".to_owned()
|
||
}
|
||
RuntimeError::UnsupportedExecutionMode { .. } => {
|
||
"Режим выполнения операции не поддерживается.".to_owned()
|
||
}
|
||
RuntimeError::InvalidPreparedRequest { .. } => {
|
||
"Не удалось подготовить корректный API-запрос.".to_owned()
|
||
}
|
||
RuntimeError::ConfirmationRequired {
|
||
confirmation_token,
|
||
expires_in_ms,
|
||
..
|
||
} => format!(
|
||
"Операция требует подтверждения. Повторите вызов с _crank_confirmation_token=\"{}\" в течение {} секунд.",
|
||
confirmation_token,
|
||
expires_in_ms / 1000
|
||
),
|
||
RuntimeError::InvalidConfirmationToken { .. } => {
|
||
"Токен подтверждения недействителен, истек или уже был использован.".to_owned()
|
||
}
|
||
RuntimeError::ConfirmationStoreUnavailable { .. } => {
|
||
"Хранилище подтверждений временно недоступно.".to_owned()
|
||
}
|
||
RuntimeError::MissingAuthProfile { .. } => "Профиль авторизации не найден.".to_owned(),
|
||
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
|
||
"Секрет авторизации не найден.".to_owned()
|
||
}
|
||
RuntimeError::InvalidAuthSecretValue { .. } => {
|
||
"Секрет авторизации имеет неподходящий формат.".to_owned()
|
||
}
|
||
RuntimeError::SecretCrypto { .. } => {
|
||
"Не удалось расшифровать секрет авторизации.".to_owned()
|
||
}
|
||
}
|
||
}
|
||
|
||
fn is_recoverable_runtime_error(error: &RuntimeError) -> bool {
|
||
matches!(
|
||
error,
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus {
|
||
status: 408 | 429 | 500..=599,
|
||
..
|
||
}) | RuntimeError::RestAdapter(RestAdapterError::Transport(_))
|
||
| RuntimeError::ConcurrencyLimitExceeded { .. }
|
||
| RuntimeError::SecretCrypto { .. }
|
||
| RuntimeError::ConfirmationRequired { .. }
|
||
)
|
||
}
|
||
|
||
fn suggested_action(error: &RuntimeError) -> Option<&'static str> {
|
||
match error {
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus {
|
||
status: 401 | 403, ..
|
||
}) => Some("Проверьте настройки авторизации внешнего API."),
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status: 404, .. }) => {
|
||
Some("Проверьте путь endpoint-а и параметры запроса.")
|
||
}
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus {
|
||
status: 408 | 429 | 500..=599,
|
||
..
|
||
})
|
||
| RuntimeError::RestAdapter(RestAdapterError::Transport(_))
|
||
| RuntimeError::ConcurrencyLimitExceeded { .. } => Some("Повторите запрос позже."),
|
||
RuntimeError::Schema(_)
|
||
| RuntimeError::Mapping(_)
|
||
| RuntimeError::InvalidPreparedRequest { .. } => {
|
||
Some("Проверьте параметры вызова инструмента.")
|
||
}
|
||
RuntimeError::ConfirmationRequired { .. } => {
|
||
Some("Повторите вызов с указанным токеном подтверждения.")
|
||
}
|
||
RuntimeError::InvalidConfirmationToken { .. } => {
|
||
Some("Запросите новый токен подтверждения.")
|
||
}
|
||
RuntimeError::ConfirmationStoreUnavailable { .. } => Some("Повторите запрос позже."),
|
||
RuntimeError::MissingAuthProfile { .. }
|
||
| RuntimeError::MissingSecret { .. }
|
||
| RuntimeError::MissingSecretVersion { .. }
|
||
| RuntimeError::InvalidAuthSecretValue { .. }
|
||
| RuntimeError::SecretCrypto { .. } => {
|
||
Some("Проверьте настройки авторизации и секретов в Crank.")
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn upstream_status(error: &RuntimeError) -> Option<u16> {
|
||
match error {
|
||
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => {
|
||
Some(*status)
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|