use crank_core::{HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Target}; use crank_registry::PublishedAgentTool; use serde_json::{Value, json}; pub fn tool_definitions(tool: &PublishedAgentTool) -> Vec { let safety = effective_safety_policy(tool); let requires_confirmation = safety.class.requires_confirmation(); let input_schema = if requires_confirmation { add_confirmation_token(schema_to_json_schema(&tool.operation.input_schema)) } else { schema_to_json_schema(&tool.operation.input_schema) }; let description = if requires_confirmation { format!( "{}\n\nЭта операция выполняется в два шага: первый вызов возвращает токен подтверждения без обращения к внешнему API, второй вызов должен повторить те же аргументы и добавить _crank_confirmation_token.", tool.tool_description ) } else { tool.tool_description.clone() }; vec![tool_definition( &tool.tool_name, &tool.tool_title, &description, input_schema, )] } pub fn tool_definition(name: &str, title: &str, description: &str, input_schema: Value) -> Value { json!({ "name": name, "title": title, "description": description, "inputSchema": input_schema }) } fn effective_safety_policy(tool: &PublishedAgentTool) -> OperationSafetyPolicy { tool.operation .execution_config .safety .clone() .unwrap_or_else(|| OperationSafetyPolicy { class: infer_safety_class(tool), confirmation: None, }) } fn infer_safety_class(tool: &PublishedAgentTool) -> OperationSafetyClass { match &tool.operation.target { Target::Rest(target) => match target.method { HttpMethod::Get => OperationSafetyClass::Read, HttpMethod::Delete => OperationSafetyClass::Destructive, HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch => OperationSafetyClass::Write, }, } } fn add_confirmation_token(mut schema: Value) -> Value { let Some(object) = schema.as_object_mut() else { return schema; }; let properties = object.entry("properties").or_insert_with(|| json!({})); if let Some(properties) = properties.as_object_mut() { properties.insert( "_crank_confirmation_token".to_owned(), json!({ "type": "string", "description": "Одноразовый токен подтверждения, который Crank возвращает после первого вызова опасной операции." }), ); } schema } pub fn schema_to_json_schema(schema: &crank_schema::Schema) -> Value { match schema.kind { crank_schema::SchemaKind::Object => { let mut properties = serde_json::Map::new(); let mut required = Vec::new(); for (field_name, field_schema) in &schema.fields { properties.insert(field_name.clone(), schema_to_json_schema(field_schema)); if field_schema.required { required.push(Value::String(field_name.clone())); } } json!({ "type": "object", "properties": properties, "required": required }) } crank_schema::SchemaKind::Array => json!({ "type": "array", "items": schema .items .as_deref() .map(schema_to_json_schema) .unwrap_or_else(|| json!({})) }), crank_schema::SchemaKind::String => json!({ "type": "string" }), crank_schema::SchemaKind::Integer => json!({ "type": "integer" }), crank_schema::SchemaKind::Number => json!({ "type": "number" }), crank_schema::SchemaKind::Boolean => json!({ "type": "boolean" }), crank_schema::SchemaKind::Enum => json!({ "type": "string", "enum": schema.enum_values }), crank_schema::SchemaKind::Null => json!({ "type": "null" }), crank_schema::SchemaKind::Oneof => json!({ "anyOf": schema.variants.iter().map(schema_to_json_schema).collect::>() }), } }