Extract admin demo seeding module
This commit is contained in:
@@ -4,19 +4,16 @@ use std::sync::Arc;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use crank_core::{
|
||||
AgentId, AuditSink, AuthProfile, CapabilityProfile, CommunityCapabilityProfile,
|
||||
EditionCapabilities, ExecutionMode, IdentityError, IdentityProvider, InvocationLevel,
|
||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, MembershipRole,
|
||||
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
|
||||
PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol,
|
||||
AuditSink, AuthProfile, CapabilityProfile, CommunityCapabilityProfile, EditionCapabilities,
|
||||
ExecutionMode, IdentityError, IdentityProvider, InvocationLog, InvocationLogId, NoopAuditSink,
|
||||
OperationSecurityLevel, OwnerOnlyPolicyEngine, PolicyEngine, ProductEdition, Protocol,
|
||||
ResponseCachePolicy, Target, ToolQualityMappingRule, ToolQualityMappingSet,
|
||||
ToolQualitySchemaKind, ToolQualitySchemaNode, UsagePeriod, WizardState, WorkspaceId,
|
||||
ToolQualitySchemaKind, ToolQualitySchemaNode, UsagePeriod, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::{
|
||||
AgentSummary, CreateInvocationLogRequest, ListInvocationLogsQuery, OperationAgentRef,
|
||||
OperationSummary, OperationUsageSummary, PostgresRegistry, RegistryOperation, SampleKind,
|
||||
UsageBucket,
|
||||
AgentSummary, CreateInvocationLogRequest, OperationAgentRef, OperationSummary,
|
||||
OperationUsageSummary, PostgresRegistry, RegistryOperation, UsageBucket,
|
||||
};
|
||||
use crank_runtime::{PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
@@ -28,6 +25,7 @@ use uuid::Uuid;
|
||||
mod agents;
|
||||
mod api_keys;
|
||||
mod auth;
|
||||
mod demo;
|
||||
mod import_export;
|
||||
mod observability;
|
||||
mod operations;
|
||||
@@ -379,282 +377,6 @@ impl AdminService {
|
||||
self.get_workspace(workspace_id).await.map(|_| ())
|
||||
}
|
||||
|
||||
async fn seed_default_workspace_demo(
|
||||
&self,
|
||||
owner_user_id: &crank_core::UserId,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<(), ApiError> {
|
||||
self.registry
|
||||
.ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner)
|
||||
.await?;
|
||||
|
||||
let rest_operation = self
|
||||
.ensure_demo_operation(workspace_id, demo_rest_operation_payload())
|
||||
.await?;
|
||||
self.ensure_operation_published(workspace_id, &rest_operation)
|
||||
.await?;
|
||||
self.ensure_demo_json_samples(
|
||||
workspace_id,
|
||||
&rest_operation.id,
|
||||
&demo_rest_input_sample(),
|
||||
&demo_rest_output_sample(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let archived_operation = self
|
||||
.ensure_demo_operation(workspace_id, demo_archived_operation_payload())
|
||||
.await?;
|
||||
self.ensure_operation_archived(workspace_id, &archived_operation)
|
||||
.await?;
|
||||
|
||||
let revops_agent = self
|
||||
.ensure_demo_agent(workspace_id, demo_revops_agent_payload())
|
||||
.await?;
|
||||
self.ensure_demo_agent_bindings(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
vec![AgentBindingPayload {
|
||||
operation_id: rest_operation.id.as_str().to_owned(),
|
||||
operation_version: rest_operation.current_draft_version,
|
||||
tool_name: "create_crm_lead".to_owned(),
|
||||
tool_title: "Create CRM Lead".to_owned(),
|
||||
tool_description_override: Some(
|
||||
"Create a new CRM lead in the revenue workspace.".to_owned(),
|
||||
),
|
||||
enabled: true,
|
||||
}],
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.ensure_demo_platform_api_key(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
"Web Console Demo Key",
|
||||
vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.seed_demo_invocation_logs(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id),
|
||||
&rest_operation.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_platform_api_key(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
name: &str,
|
||||
scopes: Vec<PlatformApiKeyScope>,
|
||||
revoke: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let existing = self
|
||||
.registry
|
||||
.list_platform_api_keys(workspace_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|record| record.api_key.name == name);
|
||||
let key = match existing {
|
||||
Some(record) => record,
|
||||
None => {
|
||||
self.create_agent_platform_api_key(
|
||||
workspace_id,
|
||||
agent_id,
|
||||
PlatformApiKeyPayload {
|
||||
name: name.to_owned(),
|
||||
scopes,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.api_key
|
||||
}
|
||||
};
|
||||
|
||||
if revoke && key.api_key.status != PlatformApiKeyStatus::Revoked {
|
||||
self.revoke_agent_platform_api_key(workspace_id, agent_id, &key.api_key.id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: OperationPayload,
|
||||
) -> Result<OperationSummary, ApiError> {
|
||||
if let Some(existing) = self
|
||||
.find_operation_by_name(workspace_id, &payload.name)
|
||||
.await?
|
||||
{
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let operation_name = payload.name.clone();
|
||||
self.create_operation(workspace_id, payload).await?;
|
||||
self.find_operation_by_name(workspace_id, &operation_name)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::internal("demo operation was created but not found"))
|
||||
}
|
||||
|
||||
async fn ensure_operation_published(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
summary: &OperationSummary,
|
||||
) -> Result<(), ApiError> {
|
||||
if summary.latest_published_version.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.publish_operation(workspace_id, &summary.id, summary.current_draft_version)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_operation_archived(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
summary: &OperationSummary,
|
||||
) -> Result<(), ApiError> {
|
||||
if summary.status == OperationStatus::Archived {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.archive_operation(workspace_id, &summary.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_json_samples(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
input: &Value,
|
||||
output: &Value,
|
||||
) -> Result<(), ApiError> {
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let samples = self
|
||||
.registry
|
||||
.list_sample_metadata(operation_id, summary.current_draft_version)
|
||||
.await?;
|
||||
|
||||
if !samples
|
||||
.iter()
|
||||
.any(|sample| sample.sample_kind == SampleKind::InputJson)
|
||||
{
|
||||
self.save_json_sample(workspace_id, operation_id, SampleKind::InputJson, input)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !samples
|
||||
.iter()
|
||||
.any(|sample| sample.sample_kind == SampleKind::OutputJson)
|
||||
{
|
||||
self.save_json_sample(workspace_id, operation_id, SampleKind::OutputJson, output)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_agent(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: AgentPayload,
|
||||
) -> Result<AgentSummaryView, ApiError> {
|
||||
let summary =
|
||||
if let Some(existing) = self.find_agent_by_slug(workspace_id, &payload.slug).await? {
|
||||
existing
|
||||
} else {
|
||||
self.create_agent(workspace_id, payload.clone()).await?;
|
||||
self.find_agent_by_slug(workspace_id, &payload.slug)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::internal("demo agent was created but not found"))?
|
||||
};
|
||||
|
||||
self.get_agent(workspace_id, &summary.id).await
|
||||
}
|
||||
|
||||
async fn ensure_demo_agent_bindings(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
bindings: Vec<AgentBindingPayload>,
|
||||
publish: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let summary = self.get_agent(workspace_id, agent_id).await?;
|
||||
self.save_agent_bindings(workspace_id, agent_id, bindings)
|
||||
.await?;
|
||||
if publish && summary.latest_published_version.is_none() {
|
||||
self.publish_agent(workspace_id, agent_id, summary.current_draft_version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn seed_demo_invocation_logs(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
revops_agent_id: &AgentId,
|
||||
rest_operation_id: &OperationId,
|
||||
) -> Result<(), ApiError> {
|
||||
if !self
|
||||
.registry
|
||||
.list_invocation_logs(ListInvocationLogsQuery {
|
||||
workspace_id,
|
||||
level: None,
|
||||
search_text: None,
|
||||
source: None,
|
||||
operation_id: None,
|
||||
agent_id: None,
|
||||
created_after: None,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rest_operation = self
|
||||
.get_operation_version(
|
||||
workspace_id,
|
||||
rest_operation_id,
|
||||
self.get_operation(workspace_id, rest_operation_id)
|
||||
.await?
|
||||
.current_draft_version,
|
||||
)
|
||||
.await?;
|
||||
self.record_invocation(InvocationRecordRequest {
|
||||
workspace_id,
|
||||
agent_id: Some(revops_agent_id),
|
||||
operation: &rest_operation.snapshot,
|
||||
request_id: None,
|
||||
source: InvocationSource::AgentToolCall,
|
||||
level: InvocationLevel::Info,
|
||||
status: InvocationStatus::Ok,
|
||||
message: "lead created in CRM".to_owned(),
|
||||
status_code: Some(201),
|
||||
error_kind: None,
|
||||
duration_ms: 182,
|
||||
request_preview: json!({
|
||||
"path": {},
|
||||
"query": {},
|
||||
"headers": { "x-demo-source": "crank-seed" },
|
||||
"body": demo_rest_request_sample()
|
||||
}),
|
||||
response_preview: demo_rest_response_sample(),
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn record_invocation(
|
||||
&self,
|
||||
request: InvocationRecordRequest<'_>,
|
||||
@@ -754,170 +476,6 @@ fn validate_profile_email(value: &str) -> Result<String, ApiError> {
|
||||
Ok(email)
|
||||
}
|
||||
|
||||
fn demo_revops_agent_payload() -> AgentPayload {
|
||||
AgentPayload {
|
||||
slug: "revops-copilot".to_owned(),
|
||||
display_name: "RevOps Copilot".to_owned(),
|
||||
description: "Sales operations assistant with CRM tools.".to_owned(),
|
||||
instructions: json!({
|
||||
"system": "Prefer CRM mutations first, then lookup tools for confirmation."
|
||||
}),
|
||||
tool_selection_policy: json!({
|
||||
"max_tools": 8,
|
||||
"prefer_tag": ["sales", "finance"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_rest_operation_payload() -> OperationPayload {
|
||||
let input = demo_rest_input_sample();
|
||||
let request = demo_rest_request_sample();
|
||||
let response = demo_rest_response_sample();
|
||||
let output = demo_rest_output_sample();
|
||||
|
||||
OperationPayload {
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create CRM Lead".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Rest(crank_core::RestTarget {
|
||||
base_url: "https://crm.demo.internal".to_owned(),
|
||||
method: crank_core::HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::from([("x-demo-source".to_owned(), "crank-seed".to_owned())]),
|
||||
}),
|
||||
input_schema: Schema::from_json_sample(&input),
|
||||
output_schema: Schema::from_json_sample(&output),
|
||||
input_mapping: infer_mapping_from_samples(
|
||||
&input,
|
||||
JsonPathRoot::Mcp,
|
||||
&request,
|
||||
JsonPathRoot::RequestBody,
|
||||
),
|
||||
output_mapping: infer_mapping_from_samples(
|
||||
&response,
|
||||
JsonPathRoot::ResponseBody,
|
||||
&output,
|
||||
JsonPathRoot::Output,
|
||||
),
|
||||
execution_config: crank_core::ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
idempotency: None,
|
||||
safety: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Create CRM Lead".to_owned(),
|
||||
description: "Create a lead record in the CRM system.".to_owned(),
|
||||
tags: vec!["sales".to_owned(), "crm".to_owned()],
|
||||
examples: vec![crank_core::ToolExample {
|
||||
input: json!({
|
||||
"email": "sarah.connor@example.com",
|
||||
"company": "Cyberdyne"
|
||||
}),
|
||||
}],
|
||||
},
|
||||
wizard_state: Some(WizardState {
|
||||
input_sample: Some(input),
|
||||
output_sample: Some(output),
|
||||
test_input: Some(demo_rest_input_sample()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_archived_operation_payload() -> OperationPayload {
|
||||
let input = json!({
|
||||
"contactId": "contact_123",
|
||||
"reason": "Duplicate profile"
|
||||
});
|
||||
let request = json!({
|
||||
"contactId": "contact_123",
|
||||
"reason": "Duplicate profile"
|
||||
});
|
||||
let response = json!({
|
||||
"archived": true,
|
||||
"contactId": "contact_123"
|
||||
});
|
||||
|
||||
OperationPayload {
|
||||
name: "marketing_archive_contact".to_owned(),
|
||||
display_name: "Archive Marketing Contact".to_owned(),
|
||||
category: "marketing".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Rest(crank_core::RestTarget {
|
||||
base_url: "https://marketing.demo.internal".to_owned(),
|
||||
method: crank_core::HttpMethod::Patch,
|
||||
path_template: "/v1/contacts/archive".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: Schema::from_json_sample(&input),
|
||||
output_schema: Schema::from_json_sample(&response),
|
||||
input_mapping: infer_mapping_from_samples(
|
||||
&input,
|
||||
JsonPathRoot::Mcp,
|
||||
&request,
|
||||
JsonPathRoot::RequestBody,
|
||||
),
|
||||
output_mapping: infer_mapping_from_samples(
|
||||
&response,
|
||||
JsonPathRoot::ResponseBody,
|
||||
&response,
|
||||
JsonPathRoot::Output,
|
||||
),
|
||||
execution_config: crank_core::ExecutionConfig {
|
||||
timeout_ms: 6_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
idempotency: None,
|
||||
safety: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Archive Marketing Contact".to_owned(),
|
||||
description: "Legacy archived flow kept for audit only.".to_owned(),
|
||||
tags: vec!["marketing".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
wizard_state: Some(WizardState {
|
||||
input_sample: Some(input),
|
||||
output_sample: Some(response),
|
||||
test_input: Some(request),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_rest_input_sample() -> Value {
|
||||
json!({
|
||||
"firstName": "Sarah",
|
||||
"lastName": "Connor",
|
||||
"email": "sarah.connor@example.com",
|
||||
"company": "Cyberdyne",
|
||||
"source": "website"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_request_sample() -> Value {
|
||||
demo_rest_input_sample()
|
||||
}
|
||||
|
||||
fn demo_rest_response_sample() -> Value {
|
||||
json!({
|
||||
"id": "lead_1001",
|
||||
"status": "created",
|
||||
"owner": "revops"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_output_sample() -> Value {
|
||||
demo_rest_response_sample()
|
||||
}
|
||||
|
||||
fn new_prefixed_id(prefix: &str) -> String {
|
||||
format!("{prefix}_{}", Uuid::now_v7().simple())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, InvocationLevel, InvocationSource, InvocationStatus, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol,
|
||||
Target, WizardState, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, infer_mapping_from_samples};
|
||||
use crank_registry::{ListInvocationLogsQuery, OperationSummary, SampleKind};
|
||||
use crank_schema::Schema;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
service::{
|
||||
AdminService, AgentBindingPayload, AgentPayload, AgentSummaryView, InvocationRecordRequest,
|
||||
OperationPayload, PlatformApiKeyPayload,
|
||||
},
|
||||
};
|
||||
|
||||
impl AdminService {
|
||||
pub(super) async fn seed_default_workspace_demo(
|
||||
&self,
|
||||
owner_user_id: &crank_core::UserId,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<(), ApiError> {
|
||||
self.registry
|
||||
.ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner)
|
||||
.await?;
|
||||
|
||||
let rest_operation = self
|
||||
.ensure_demo_operation(workspace_id, demo_rest_operation_payload())
|
||||
.await?;
|
||||
self.ensure_operation_published(workspace_id, &rest_operation)
|
||||
.await?;
|
||||
self.ensure_demo_json_samples(
|
||||
workspace_id,
|
||||
&rest_operation.id,
|
||||
&demo_rest_input_sample(),
|
||||
&demo_rest_output_sample(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let archived_operation = self
|
||||
.ensure_demo_operation(workspace_id, demo_archived_operation_payload())
|
||||
.await?;
|
||||
self.ensure_operation_archived(workspace_id, &archived_operation)
|
||||
.await?;
|
||||
|
||||
let revops_agent = self
|
||||
.ensure_demo_agent(workspace_id, demo_revops_agent_payload())
|
||||
.await?;
|
||||
self.ensure_demo_agent_bindings(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
vec![AgentBindingPayload {
|
||||
operation_id: rest_operation.id.as_str().to_owned(),
|
||||
operation_version: rest_operation.current_draft_version,
|
||||
tool_name: "create_crm_lead".to_owned(),
|
||||
tool_title: "Create CRM Lead".to_owned(),
|
||||
tool_description_override: Some(
|
||||
"Create a new CRM lead in the revenue workspace.".to_owned(),
|
||||
),
|
||||
enabled: true,
|
||||
}],
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.ensure_demo_platform_api_key(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
"Web Console Demo Key",
|
||||
vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.seed_demo_invocation_logs(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id),
|
||||
&rest_operation.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_platform_api_key(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
name: &str,
|
||||
scopes: Vec<PlatformApiKeyScope>,
|
||||
revoke: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let existing = self
|
||||
.registry
|
||||
.list_platform_api_keys(workspace_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|record| record.api_key.name == name);
|
||||
let key = match existing {
|
||||
Some(record) => record,
|
||||
None => {
|
||||
self.create_agent_platform_api_key(
|
||||
workspace_id,
|
||||
agent_id,
|
||||
PlatformApiKeyPayload {
|
||||
name: name.to_owned(),
|
||||
scopes,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.api_key
|
||||
}
|
||||
};
|
||||
|
||||
if revoke && key.api_key.status != PlatformApiKeyStatus::Revoked {
|
||||
self.revoke_agent_platform_api_key(workspace_id, agent_id, &key.api_key.id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: OperationPayload,
|
||||
) -> Result<OperationSummary, ApiError> {
|
||||
if let Some(existing) = self
|
||||
.find_operation_by_name(workspace_id, &payload.name)
|
||||
.await?
|
||||
{
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let operation_name = payload.name.clone();
|
||||
self.create_operation(workspace_id, payload).await?;
|
||||
self.find_operation_by_name(workspace_id, &operation_name)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::internal("demo operation was created but not found"))
|
||||
}
|
||||
|
||||
async fn ensure_operation_published(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
summary: &OperationSummary,
|
||||
) -> Result<(), ApiError> {
|
||||
if summary.latest_published_version.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.publish_operation(workspace_id, &summary.id, summary.current_draft_version)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_operation_archived(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
summary: &OperationSummary,
|
||||
) -> Result<(), ApiError> {
|
||||
if summary.status == OperationStatus::Archived {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.archive_operation(workspace_id, &summary.id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_json_samples(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
input: &Value,
|
||||
output: &Value,
|
||||
) -> Result<(), ApiError> {
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let samples = self
|
||||
.registry
|
||||
.list_sample_metadata(operation_id, summary.current_draft_version)
|
||||
.await?;
|
||||
|
||||
if !samples
|
||||
.iter()
|
||||
.any(|sample| sample.sample_kind == SampleKind::InputJson)
|
||||
{
|
||||
self.save_json_sample(workspace_id, operation_id, SampleKind::InputJson, input)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !samples
|
||||
.iter()
|
||||
.any(|sample| sample.sample_kind == SampleKind::OutputJson)
|
||||
{
|
||||
self.save_json_sample(workspace_id, operation_id, SampleKind::OutputJson, output)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_agent(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
payload: AgentPayload,
|
||||
) -> Result<AgentSummaryView, ApiError> {
|
||||
let summary =
|
||||
if let Some(existing) = self.find_agent_by_slug(workspace_id, &payload.slug).await? {
|
||||
existing
|
||||
} else {
|
||||
self.create_agent(workspace_id, payload.clone()).await?;
|
||||
self.find_agent_by_slug(workspace_id, &payload.slug)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::internal("demo agent was created but not found"))?
|
||||
};
|
||||
|
||||
self.get_agent(workspace_id, &summary.id).await
|
||||
}
|
||||
|
||||
async fn ensure_demo_agent_bindings(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
bindings: Vec<AgentBindingPayload>,
|
||||
publish: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let summary = self.get_agent(workspace_id, agent_id).await?;
|
||||
self.save_agent_bindings(workspace_id, agent_id, bindings)
|
||||
.await?;
|
||||
if publish && summary.latest_published_version.is_none() {
|
||||
self.publish_agent(workspace_id, agent_id, summary.current_draft_version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn seed_demo_invocation_logs(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
revops_agent_id: &AgentId,
|
||||
rest_operation_id: &OperationId,
|
||||
) -> Result<(), ApiError> {
|
||||
if !self
|
||||
.registry
|
||||
.list_invocation_logs(ListInvocationLogsQuery {
|
||||
workspace_id,
|
||||
level: None,
|
||||
search_text: None,
|
||||
source: None,
|
||||
operation_id: None,
|
||||
agent_id: None,
|
||||
created_after: None,
|
||||
limit: 1,
|
||||
})
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rest_operation = self
|
||||
.get_operation_version(
|
||||
workspace_id,
|
||||
rest_operation_id,
|
||||
self.get_operation(workspace_id, rest_operation_id)
|
||||
.await?
|
||||
.current_draft_version,
|
||||
)
|
||||
.await?;
|
||||
self.record_invocation(InvocationRecordRequest {
|
||||
workspace_id,
|
||||
agent_id: Some(revops_agent_id),
|
||||
operation: &rest_operation.snapshot,
|
||||
request_id: None,
|
||||
source: InvocationSource::AgentToolCall,
|
||||
level: InvocationLevel::Info,
|
||||
status: InvocationStatus::Ok,
|
||||
message: "lead created in CRM".to_owned(),
|
||||
status_code: Some(201),
|
||||
error_kind: None,
|
||||
duration_ms: 182,
|
||||
request_preview: json!({
|
||||
"path": {},
|
||||
"query": {},
|
||||
"headers": { "x-demo-source": "crank-seed" },
|
||||
"body": demo_rest_request_sample()
|
||||
}),
|
||||
response_preview: demo_rest_response_sample(),
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_revops_agent_payload() -> AgentPayload {
|
||||
AgentPayload {
|
||||
slug: "revops-copilot".to_owned(),
|
||||
display_name: "RevOps Copilot".to_owned(),
|
||||
description: "Sales operations assistant with CRM tools.".to_owned(),
|
||||
instructions: json!({
|
||||
"system": "Prefer CRM mutations first, then lookup tools for confirmation."
|
||||
}),
|
||||
tool_selection_policy: json!({
|
||||
"max_tools": 8,
|
||||
"prefer_tag": ["sales", "finance"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_rest_operation_payload() -> OperationPayload {
|
||||
let input = demo_rest_input_sample();
|
||||
let request = demo_rest_request_sample();
|
||||
let response = demo_rest_response_sample();
|
||||
let output = demo_rest_output_sample();
|
||||
|
||||
OperationPayload {
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create CRM Lead".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Rest(crank_core::RestTarget {
|
||||
base_url: "https://crm.demo.internal".to_owned(),
|
||||
method: crank_core::HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::from([("x-demo-source".to_owned(), "crank-seed".to_owned())]),
|
||||
}),
|
||||
input_schema: Schema::from_json_sample(&input),
|
||||
output_schema: Schema::from_json_sample(&output),
|
||||
input_mapping: infer_mapping_from_samples(
|
||||
&input,
|
||||
JsonPathRoot::Mcp,
|
||||
&request,
|
||||
JsonPathRoot::RequestBody,
|
||||
),
|
||||
output_mapping: infer_mapping_from_samples(
|
||||
&response,
|
||||
JsonPathRoot::ResponseBody,
|
||||
&output,
|
||||
JsonPathRoot::Output,
|
||||
),
|
||||
execution_config: crank_core::ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
idempotency: None,
|
||||
safety: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Create CRM Lead".to_owned(),
|
||||
description: "Create a lead record in the CRM system.".to_owned(),
|
||||
tags: vec!["sales".to_owned(), "crm".to_owned()],
|
||||
examples: vec![crank_core::ToolExample {
|
||||
input: json!({
|
||||
"email": "sarah.connor@example.com",
|
||||
"company": "Cyberdyne"
|
||||
}),
|
||||
}],
|
||||
},
|
||||
wizard_state: Some(WizardState {
|
||||
input_sample: Some(input),
|
||||
output_sample: Some(output),
|
||||
test_input: Some(demo_rest_input_sample()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_archived_operation_payload() -> OperationPayload {
|
||||
let input = json!({
|
||||
"contactId": "contact_123",
|
||||
"reason": "Duplicate profile"
|
||||
});
|
||||
let request = json!({
|
||||
"contactId": "contact_123",
|
||||
"reason": "Duplicate profile"
|
||||
});
|
||||
let response = json!({
|
||||
"archived": true,
|
||||
"contactId": "contact_123"
|
||||
});
|
||||
|
||||
OperationPayload {
|
||||
name: "marketing_archive_contact".to_owned(),
|
||||
display_name: "Archive Marketing Contact".to_owned(),
|
||||
category: "marketing".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Rest(crank_core::RestTarget {
|
||||
base_url: "https://marketing.demo.internal".to_owned(),
|
||||
method: crank_core::HttpMethod::Patch,
|
||||
path_template: "/v1/contacts/archive".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: Schema::from_json_sample(&input),
|
||||
output_schema: Schema::from_json_sample(&response),
|
||||
input_mapping: infer_mapping_from_samples(
|
||||
&input,
|
||||
JsonPathRoot::Mcp,
|
||||
&request,
|
||||
JsonPathRoot::RequestBody,
|
||||
),
|
||||
output_mapping: infer_mapping_from_samples(
|
||||
&response,
|
||||
JsonPathRoot::ResponseBody,
|
||||
&response,
|
||||
JsonPathRoot::Output,
|
||||
),
|
||||
execution_config: crank_core::ExecutionConfig {
|
||||
timeout_ms: 6_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
idempotency: None,
|
||||
safety: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Archive Marketing Contact".to_owned(),
|
||||
description: "Legacy archived flow kept for audit only.".to_owned(),
|
||||
tags: vec!["marketing".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
wizard_state: Some(WizardState {
|
||||
input_sample: Some(input),
|
||||
output_sample: Some(response),
|
||||
test_input: Some(request),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_rest_input_sample() -> Value {
|
||||
json!({
|
||||
"firstName": "Sarah",
|
||||
"lastName": "Connor",
|
||||
"email": "sarah.connor@example.com",
|
||||
"company": "Cyberdyne",
|
||||
"source": "website"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_request_sample() -> Value {
|
||||
demo_rest_input_sample()
|
||||
}
|
||||
|
||||
fn demo_rest_response_sample() -> Value {
|
||||
json!({
|
||||
"id": "lead_1001",
|
||||
"status": "created",
|
||||
"owner": "revops"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_output_sample() -> Value {
|
||||
demo_rest_response_sample()
|
||||
}
|
||||
Reference in New Issue
Block a user