агенты: добавить поиск инструментов по каталогу
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, OperationId, UsagePeriod,
|
||||
WorkspaceId,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, OperationId, SearchableTool,
|
||||
ToolAccessMode, ToolSelectionPolicy, UsagePeriod, WorkspaceId, search_tool_catalog,
|
||||
};
|
||||
use crank_registry::{
|
||||
AgentVersionRecord, CreateAgentDraftVersionRequest, CreateAgentRequest, PublishAgentRequest,
|
||||
SaveAgentBindingsRequest, UsageBucket, UsageQuery,
|
||||
SaveAgentBindingsRequest, SaveAgentCatalogConfigRequest, UsageBucket, UsageQuery,
|
||||
};
|
||||
use serde_json::json;
|
||||
use time::OffsetDateTime;
|
||||
@@ -15,13 +15,93 @@ use tracing::{info, instrument};
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
service::{
|
||||
AdminService, AgentBindingPayload, AgentMutationResult, AgentPayload, AgentSummaryView,
|
||||
CreatedAgentResponse, PublishAgentResponse, UpdateAgentPayload, agent_mcp_endpoint,
|
||||
format_timestamp, map_agent_summary_view, new_prefixed_id, today_start_utc,
|
||||
AdminService, AgentCatalogPayload, AgentMutationResult, AgentPayload, AgentSummaryView,
|
||||
CreatedAgentResponse, PublishAgentResponse, ToolSearchPreviewPayload, UpdateAgentPayload,
|
||||
agent_mcp_endpoint, format_timestamp, map_agent_summary_view, new_prefixed_id,
|
||||
today_start_utc,
|
||||
},
|
||||
};
|
||||
|
||||
impl AdminService {
|
||||
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str()))]
|
||||
pub async fn preview_tool_search(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: ToolSearchPreviewPayload,
|
||||
) -> Result<Vec<crank_core::ToolSearchMatch>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
if payload.tool_selection_policy.mode != ToolAccessMode::Search {
|
||||
return Err(ApiError::validation_with_context(
|
||||
"tool search preview requires search mode",
|
||||
json!({"field": "tool_selection_policy.mode"}),
|
||||
));
|
||||
}
|
||||
let bindings = payload
|
||||
.bindings
|
||||
.iter()
|
||||
.map(|binding| AgentOperationBinding {
|
||||
operation_id: OperationId::new(binding.operation_id.clone()),
|
||||
operation_version: binding.operation_version,
|
||||
tool_name: binding.tool_name.clone(),
|
||||
tool_title: binding.tool_title.clone(),
|
||||
tool_description_override: binding.tool_description_override.clone(),
|
||||
enabled: binding.enabled,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
validate_tool_selection_policy(&payload.tool_selection_policy, &bindings)?;
|
||||
|
||||
let mut tools = Vec::new();
|
||||
for binding in bindings.iter().filter(|binding| binding.enabled) {
|
||||
let version = self
|
||||
.registry
|
||||
.get_operation_version(
|
||||
workspace_id,
|
||||
&binding.operation_id,
|
||||
binding.operation_version,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found_with_context(
|
||||
format!("operation {} was not found", binding.operation_id.as_str()),
|
||||
json!({"operation_id": binding.operation_id.as_str()}),
|
||||
)
|
||||
})?;
|
||||
let groups = payload
|
||||
.tool_selection_policy
|
||||
.groups
|
||||
.iter()
|
||||
.filter(|group| {
|
||||
group
|
||||
.tool_names
|
||||
.iter()
|
||||
.any(|name| name == &binding.tool_name)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tools.push(SearchableTool {
|
||||
name: binding.tool_name.clone(),
|
||||
title: binding.tool_title.clone(),
|
||||
description: binding
|
||||
.tool_description_override
|
||||
.clone()
|
||||
.unwrap_or(version.snapshot.tool_description.description),
|
||||
input_schema: serde_json::Value::Null,
|
||||
group_ids: groups.iter().map(|group| group.id.clone()).collect(),
|
||||
group_context: groups
|
||||
.iter()
|
||||
.map(|group| format!("{} {}", group.name, group.description))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
});
|
||||
}
|
||||
let max_results = payload.tool_selection_policy.search.max_results;
|
||||
Ok(search_tool_catalog(
|
||||
&tools,
|
||||
&payload.query,
|
||||
&payload.group_ids,
|
||||
max_results,
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_agents(
|
||||
&self,
|
||||
@@ -69,6 +149,7 @@ impl AdminService {
|
||||
items.push(AgentSummaryView {
|
||||
operation_count: operation_ids.len(),
|
||||
operation_ids,
|
||||
tool_selection_policy: version.snapshot.tool_selection_policy,
|
||||
key_count: key_counts.get(summary.id.as_str()).copied().unwrap_or(0),
|
||||
calls_today: calls_today.get(summary.id.as_str()).copied().unwrap_or(0),
|
||||
mcp_endpoint: agent_mcp_endpoint(
|
||||
@@ -130,6 +211,7 @@ impl AdminService {
|
||||
Ok(AgentSummaryView {
|
||||
operation_count: operation_ids.len(),
|
||||
operation_ids,
|
||||
tool_selection_policy: version.snapshot.tool_selection_policy,
|
||||
key_count,
|
||||
calls_today: usage.map(|item| item.rollup.calls_total).unwrap_or(0),
|
||||
mcp_endpoint: agent_mcp_endpoint(
|
||||
@@ -304,9 +386,12 @@ impl AdminService {
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
payload: Vec<AgentBindingPayload>,
|
||||
payload: AgentCatalogPayload,
|
||||
) -> Result<AgentVersionRecord, ApiError> {
|
||||
let agent = self.get_agent(workspace_id, agent_id).await?;
|
||||
let current_version = self
|
||||
.ensure_editable_agent_version(workspace_id, agent_id)
|
||||
.await?;
|
||||
let (payload, requested_policy) = payload.into_parts();
|
||||
let bindings = payload
|
||||
.into_iter()
|
||||
.map(|binding| AgentOperationBinding {
|
||||
@@ -318,23 +403,62 @@ impl AdminService {
|
||||
enabled: binding.enabled,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let tool_selection_policy = requested_policy
|
||||
.unwrap_or_else(|| current_version.snapshot.tool_selection_policy.clone());
|
||||
validate_tool_selection_policy(&tool_selection_policy, &bindings)?;
|
||||
|
||||
self.registry
|
||||
.save_agent_bindings(SaveAgentBindingsRequest {
|
||||
.save_agent_catalog_config(SaveAgentCatalogConfigRequest {
|
||||
workspace_id,
|
||||
agent_id,
|
||||
agent_version: agent.current_draft_version,
|
||||
agent_version: current_version.version,
|
||||
bindings: &bindings,
|
||||
tool_selection_policy: &tool_selection_policy,
|
||||
})
|
||||
.await?;
|
||||
info!(
|
||||
agent_id = %agent_id.as_str(),
|
||||
version = agent.current_draft_version,
|
||||
version = current_version.version,
|
||||
binding_count = bindings.len(),
|
||||
"agent bindings saved"
|
||||
);
|
||||
|
||||
self.get_agent_version(workspace_id, agent_id, agent.current_draft_version)
|
||||
self.get_agent_version(workspace_id, agent_id, current_version.version)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn ensure_editable_agent_version(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<AgentVersionRecord, ApiError> {
|
||||
let agent = self.get_agent(workspace_id, agent_id).await?;
|
||||
let current = self
|
||||
.get_agent_version(workspace_id, agent_id, agent.current_draft_version)
|
||||
.await?;
|
||||
if agent.latest_published_version != Some(agent.current_draft_version) {
|
||||
return Ok(current);
|
||||
}
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let draft = AgentVersion {
|
||||
agent_id: agent_id.clone(),
|
||||
version: current.version + 1,
|
||||
status: AgentStatus::Draft,
|
||||
instructions: current.snapshot.instructions.clone(),
|
||||
tool_selection_policy: current.snapshot.tool_selection_policy.clone(),
|
||||
created_at: now,
|
||||
};
|
||||
self.registry
|
||||
.create_agent_draft_version(CreateAgentDraftVersionRequest {
|
||||
workspace_id,
|
||||
agent_id,
|
||||
version: &draft,
|
||||
bindings: ¤t.bindings,
|
||||
updated_at: &now,
|
||||
})
|
||||
.await?;
|
||||
self.get_agent_version(workspace_id, agent_id, draft.version)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -351,6 +475,10 @@ impl AdminService {
|
||||
let published_bindings = self
|
||||
.published_agent_bindings(workspace_id, &agent_version.bindings)
|
||||
.await?;
|
||||
validate_tool_selection_policy(
|
||||
&agent_version.snapshot.tool_selection_policy,
|
||||
&published_bindings,
|
||||
)?;
|
||||
|
||||
if published_bindings.is_empty() {
|
||||
return Err(ApiError::conflict_with_context(
|
||||
@@ -490,3 +618,22 @@ impl AdminService {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_tool_selection_policy(
|
||||
policy: &ToolSelectionPolicy,
|
||||
bindings: &[AgentOperationBinding],
|
||||
) -> Result<(), ApiError> {
|
||||
policy
|
||||
.validate_for_tools(
|
||||
bindings
|
||||
.iter()
|
||||
.filter(|binding| binding.enabled)
|
||||
.map(|binding| binding.tool_name.as_str()),
|
||||
)
|
||||
.map_err(|error| {
|
||||
ApiError::validation_with_context(
|
||||
error.to_string(),
|
||||
json!({"field": "tool_selection_policy"}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user