агенты: добавить поиск инструментов по каталогу

This commit is contained in:
2026-07-21 13:12:46 +03:00
parent 63f8ee333f
commit 99bd05c145
35 changed files with 2088 additions and 155 deletions
+1
View File
@@ -1,5 +1,6 @@
mod integration {
mod catalog_access;
mod common;
mod tool_search;
mod transport_protocol;
}
+19 -4
View File
@@ -18,7 +18,7 @@ use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ExecutionConfig, HttpMethod,
Operation, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind,
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, Target, ToolDescription,
WorkspaceId,
ToolSelectionPolicy, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_registry::{
@@ -45,7 +45,7 @@ use crank_community_mcp::{
session::{InMemorySessionStore, SharedSessionStore, TransportSessionStore},
};
fn test_workspace_id() -> WorkspaceId {
pub(super) fn test_workspace_id() -> WorkspaceId {
WorkspaceId::new("ws_default")
}
@@ -93,7 +93,7 @@ fn test_agent_id(agent_slug: &str) -> AgentId {
AgentId::new(format!("agent_{agent_slug}"))
}
fn build_test_app(
pub(super) fn build_test_app(
registry: PostgresRegistry,
refresh_interval: Duration,
public_base_url: Option<String>,
@@ -363,6 +363,21 @@ pub(super) async fn publish_agent_with_bindings(
registry: &PostgresRegistry,
agent_slug: &str,
bindings: Vec<AgentOperationBinding>,
) {
publish_agent_with_policy(
registry,
agent_slug,
bindings,
ToolSelectionPolicy::default(),
)
.await;
}
pub(super) async fn publish_agent_with_policy(
registry: &PostgresRegistry,
agent_slug: &str,
bindings: Vec<AgentOperationBinding>,
tool_selection_policy: ToolSelectionPolicy,
) {
let agent_id = AgentId::new(format!("agent_{agent_slug}"));
let agent = Agent {
@@ -383,7 +398,7 @@ pub(super) async fn publish_agent_with_bindings(
version: 1,
status: AgentStatus::Draft,
instructions: json!({}),
tool_selection_policy: json!({}),
tool_selection_policy,
created_at: OffsetDateTime::parse("2026-03-26T10:00:00Z", &Rfc3339).unwrap(),
};
@@ -0,0 +1,154 @@
use super::common::*;
use std::time::Duration;
use crank_core::{
PlatformApiKeyScope, ToolAccessMode, ToolGroup, ToolSearchSettings, ToolSelectionPolicy,
};
use crank_registry::PublishRequest;
use serde_json::json;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
#[tokio::test]
async fn search_mode_discovers_and_calls_tools_through_meta_tools() {
let registry = test_registry().await;
let upstream_base_url = spawn_upstream_server().await;
let invoice = test_operation(&upstream_base_url, "create_invoice");
let ticket = test_operation(&upstream_base_url, "create_support_ticket");
for operation in [&invoice, &ticket] {
registry
.create_operation(&test_workspace_id(), operation, Some("alice"))
.await
.unwrap();
registry
.publish_operation(PublishRequest {
workspace_id: &test_workspace_id(),
operation_id: &operation.id,
version: 1,
published_at: &OffsetDateTime::parse("2026-03-26T10:00:00Z", &Rfc3339).unwrap(),
published_by: Some("alice"),
})
.await
.unwrap();
}
publish_agent_with_policy(
&registry,
"business-search",
vec![
binding_for_operation(&invoice),
binding_for_operation(&ticket),
],
ToolSelectionPolicy {
mode: ToolAccessMode::Search,
groups: vec![
ToolGroup {
id: "finance".to_owned(),
name: "Finance".to_owned(),
description: "Invoices and payments".to_owned(),
tool_names: vec![invoice.name.clone()],
},
ToolGroup {
id: "support".to_owned(),
name: "Support".to_owned(),
description: "Customer support tickets".to_owned(),
tool_names: vec![ticket.name.clone()],
},
],
search: ToolSearchSettings { max_results: 5 },
},
)
.await;
let api_key = create_platform_api_key(
&registry,
"business-search",
"mcp-search",
&[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
)
.await;
let base_url = spawn_mcp_server(build_test_app(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
))
.await;
let client = reqwest::Client::new();
let mcp_url = agent_mcp_url(&base_url, "business-search");
let session = initialize_session(&client, &mcp_url, &api_key).await;
let listed = post_jsonrpc(
&client,
&mcp_url,
&api_key,
Some(&session),
json!({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}),
)
.await;
assert_eq!(
listed["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|tool| tool["name"].as_str().unwrap())
.collect::<Vec<_>>(),
vec!["search_tools", "call_tool"]
);
let search = post_jsonrpc(
&client,
&mcp_url,
&api_key,
Some(&session),
json!({
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"search_tools","arguments":{"query":"invoice","group_ids":["finance"]}}
}),
)
.await;
assert_eq!(
search["result"]["structuredContent"]["tools"][0]["name"],
"create_invoice"
);
assert_eq!(
search["result"]["structuredContent"]["catalog_revision"],
"agent-version-1"
);
let stale_call = post_jsonrpc(
&client,
&mcp_url,
&api_key,
Some(&session),
json!({
"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"call_tool","arguments":{
"name":"create_invoice",
"arguments":{"email":"user@example.com"},
"catalog_revision":"agent-version-0"
}}
}),
)
.await;
assert_eq!(stale_call["result"]["isError"], true);
assert_eq!(
stale_call["result"]["structuredContent"]["error"]["code"],
"catalog_revision_changed"
);
let call = post_jsonrpc(
&client,
&mcp_url,
&api_key,
Some(&session),
json!({
"jsonrpc":"2.0","id":5,"method":"tools/call",
"params":{"name":"call_tool","arguments":{
"name":"create_invoice",
"arguments":{"email":"user@example.com"},
"catalog_revision":"agent-version-1"
}}
}),
)
.await;
assert_eq!(call["result"]["isError"], false);
assert_eq!(call["result"]["structuredContent"]["id"], "lead_123");
}
@@ -19,7 +19,8 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ExecutionConfig, HttpMethod,
Operation, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
PlatformApiKeyStatus, Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
PlatformApiKeyStatus, Protocol, RestTarget, Target, ToolAccessMode, ToolDescription, ToolGroup,
ToolSearchSettings, ToolSelectionPolicy, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_registry::{