Refresh community demo and docs
This commit is contained in:
+126
-136
@@ -2,10 +2,11 @@ use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, InvocationLevel, InvocationSource, InvocationStatus, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol,
|
||||
Target, WizardState, WorkspaceId,
|
||||
OperationSecurityLevel, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, Target,
|
||||
WizardState, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, infer_mapping_from_samples};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::{ListInvocationLogsQuery, OperationSummary, SampleKind};
|
||||
use crank_schema::Schema;
|
||||
use serde_json::{Value, json};
|
||||
@@ -28,6 +29,8 @@ impl AdminService {
|
||||
.ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner)
|
||||
.await?;
|
||||
|
||||
self.cleanup_legacy_demo_assets(workspace_id).await?;
|
||||
|
||||
let rest_operation = self
|
||||
.ensure_demo_operation(workspace_id, demo_rest_operation_payload())
|
||||
.await?;
|
||||
@@ -41,25 +44,20 @@ impl AdminService {
|
||||
)
|
||||
.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())
|
||||
let currency_agent = self
|
||||
.ensure_demo_agent(workspace_id, demo_currency_agent_payload())
|
||||
.await?;
|
||||
self.ensure_demo_agent_bindings(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
&AgentId::new(currency_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_name: "frankfurter_latest_rate".to_owned(),
|
||||
tool_title: "Последний курс валюты".to_owned(),
|
||||
tool_description_override: Some(
|
||||
"Create a new CRM lead in the revenue workspace.".to_owned(),
|
||||
"Возвращает последний доступный курс одной валюты к другой через Frankfurter."
|
||||
.to_owned(),
|
||||
),
|
||||
enabled: true,
|
||||
}],
|
||||
@@ -69,8 +67,8 @@ impl AdminService {
|
||||
|
||||
self.ensure_demo_platform_api_key(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id.clone()),
|
||||
"Web Console Demo Key",
|
||||
&AgentId::new(currency_agent.id.clone()),
|
||||
"Frankfurter Demo Key",
|
||||
vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
false,
|
||||
)
|
||||
@@ -78,7 +76,7 @@ impl AdminService {
|
||||
|
||||
self.seed_demo_invocation_logs(
|
||||
workspace_id,
|
||||
&AgentId::new(revops_agent.id),
|
||||
&AgentId::new(currency_agent.id),
|
||||
&rest_operation.id,
|
||||
)
|
||||
.await?;
|
||||
@@ -86,6 +84,46 @@ impl AdminService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cleanup_legacy_demo_assets(&self, workspace_id: &WorkspaceId) -> Result<(), ApiError> {
|
||||
for slug in ["revops-copilot", "support-triage"] {
|
||||
if let Some(agent) = self.find_agent_by_slug(workspace_id, slug).await? {
|
||||
self.delete_agent(workspace_id, &AgentId::new(agent.id.as_str().to_owned()))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let operations = self.registry.list_operations(workspace_id).await?;
|
||||
for operation in operations {
|
||||
if operation.name.starts_with("internal_health_smoke_")
|
||||
|| operation
|
||||
.name
|
||||
.starts_with("weather_current_open_meteo_smoke_")
|
||||
{
|
||||
self.delete_operation(
|
||||
workspace_id,
|
||||
&OperationId::new(operation.id.as_str().to_owned()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
for name in [
|
||||
"crm_create_lead",
|
||||
"marketing_archive_contact",
|
||||
"weather_current_open_meteo",
|
||||
] {
|
||||
if let Some(operation) = self.find_operation_by_name(workspace_id, name).await? {
|
||||
self.delete_operation(
|
||||
workspace_id,
|
||||
&OperationId::new(operation.id.as_str().to_owned()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_demo_platform_api_key(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -157,19 +195,6 @@ impl AdminService {
|
||||
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,
|
||||
@@ -241,7 +266,7 @@ impl AdminService {
|
||||
async fn seed_demo_invocation_logs(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
revops_agent_id: &AgentId,
|
||||
currency_agent_id: &AgentId,
|
||||
rest_operation_id: &OperationId,
|
||||
) -> Result<(), ApiError> {
|
||||
if !self
|
||||
@@ -273,21 +298,21 @@ impl AdminService {
|
||||
.await?;
|
||||
self.record_invocation(InvocationRecordRequest {
|
||||
workspace_id,
|
||||
agent_id: Some(revops_agent_id),
|
||||
agent_id: Some(currency_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),
|
||||
message: "Frankfurter returned latest exchange rate".to_owned(),
|
||||
status_code: Some(200),
|
||||
error_kind: None,
|
||||
duration_ms: 182,
|
||||
duration_ms: 124,
|
||||
request_preview: json!({
|
||||
"path": {},
|
||||
"query": {},
|
||||
"headers": { "x-demo-source": "crank-seed" },
|
||||
"body": demo_rest_request_sample()
|
||||
"query": demo_rest_request_sample(),
|
||||
"headers": { "Accept": "application/json" },
|
||||
"body": null
|
||||
}),
|
||||
response_preview: demo_rest_response_sample(),
|
||||
})
|
||||
@@ -296,47 +321,41 @@ impl AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_revops_agent_payload() -> AgentPayload {
|
||||
fn demo_currency_agent_payload() -> AgentPayload {
|
||||
AgentPayload {
|
||||
slug: "revops-copilot".to_owned(),
|
||||
display_name: "RevOps Copilot".to_owned(),
|
||||
description: "Sales operations assistant with CRM tools.".to_owned(),
|
||||
slug: "currency-rates".to_owned(),
|
||||
display_name: "Курсы валют".to_owned(),
|
||||
description: "Агент с инструментами для получения курсов валют.".to_owned(),
|
||||
instructions: json!({
|
||||
"system": "Prefer CRM mutations first, then lookup tools for confirmation."
|
||||
"system": "Используй инструменты Frankfurter только для запросов о курсах валют."
|
||||
}),
|
||||
tool_selection_policy: json!({
|
||||
"max_tools": 8,
|
||||
"prefer_tag": ["sales", "finance"]
|
||||
"max_tools": 4,
|
||||
"prefer_tag": ["currency", "exchange-rate"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
name: "frankfurter_latest_rate".to_owned(),
|
||||
display_name: "Последний курс валюты".to_owned(),
|
||||
category: "frankfurter_rates".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())]),
|
||||
base_url: "https://api.frankfurter.dev".to_owned(),
|
||||
method: crank_core::HttpMethod::Get,
|
||||
path_template: "/v1/latest".to_owned(),
|
||||
static_headers: BTreeMap::from([("Accept".to_owned(), "application/json".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,
|
||||
),
|
||||
input_mapping: frankfurter_input_mapping(),
|
||||
output_mapping: infer_mapping_from_samples(
|
||||
&response,
|
||||
JsonPathRoot::ResponseBody,
|
||||
@@ -353,13 +372,19 @@ fn demo_rest_operation_payload() -> OperationPayload {
|
||||
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()],
|
||||
title: "Последний курс валюты".to_owned(),
|
||||
description:
|
||||
"Возвращает последний доступный курс одной валюты к другой через Frankfurter."
|
||||
.to_owned(),
|
||||
tags: vec![
|
||||
"frankfurter".to_owned(),
|
||||
"currency".to_owned(),
|
||||
"exchange-rate".to_owned(),
|
||||
],
|
||||
examples: vec![crank_core::ToolExample {
|
||||
input: json!({
|
||||
"email": "sarah.connor@example.com",
|
||||
"company": "Cyberdyne"
|
||||
"base": "USD",
|
||||
"quote": "EUR"
|
||||
}),
|
||||
}],
|
||||
},
|
||||
@@ -371,91 +396,56 @@ fn demo_rest_operation_payload() -> OperationPayload {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"base": "USD",
|
||||
"quote": "EUR"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_request_sample() -> Value {
|
||||
demo_rest_input_sample()
|
||||
json!({
|
||||
"base": "USD",
|
||||
"symbols": "EUR"
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_response_sample() -> Value {
|
||||
json!({
|
||||
"id": "lead_1001",
|
||||
"status": "created",
|
||||
"owner": "revops"
|
||||
"amount": 1.0,
|
||||
"base": "USD",
|
||||
"date": "2026-06-19",
|
||||
"rates": {
|
||||
"EUR": 0.87207
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn demo_rest_output_sample() -> Value {
|
||||
demo_rest_response_sample()
|
||||
}
|
||||
|
||||
fn frankfurter_input_mapping() -> MappingSet {
|
||||
MappingSet {
|
||||
rules: vec![
|
||||
MappingRule {
|
||||
source: "$.mcp.base".to_owned(),
|
||||
target: "$.request.query.base".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
},
|
||||
MappingRule {
|
||||
source: "$.mcp.quote".to_owned(),
|
||||
target: "$.request.query.symbols".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,19 +85,33 @@ impl AdminService {
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<(), ApiError> {
|
||||
let existing = self.registry.list_workspace_upstreams(workspace_id).await?;
|
||||
if existing.iter().any(|item| item.name == "Open Meteo") {
|
||||
if existing.iter().any(|item| item.name == "Frankfurter") {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let existing_open_meteo = existing
|
||||
.iter()
|
||||
.find(|item| {
|
||||
item.name == "Open Meteo"
|
||||
&& item.base_url == "https://api.open-meteo.com"
|
||||
&& item.auth_profile_id.is_none()
|
||||
})
|
||||
.cloned();
|
||||
let upstream = WorkspaceUpstream {
|
||||
id: WorkspaceUpstreamId::new(new_prefixed_id("upstream")),
|
||||
id: existing_open_meteo
|
||||
.as_ref()
|
||||
.map(|item| item.id.clone())
|
||||
.unwrap_or_else(|| WorkspaceUpstreamId::new(new_prefixed_id("upstream"))),
|
||||
workspace_id: workspace_id.clone(),
|
||||
name: "Open Meteo".to_owned(),
|
||||
base_url: "https://api.open-meteo.com".to_owned(),
|
||||
name: "Frankfurter".to_owned(),
|
||||
base_url: "https://api.frankfurter.dev".to_owned(),
|
||||
static_headers: json!({}),
|
||||
auth_profile_id: None,
|
||||
created_at: now,
|
||||
created_at: existing_open_meteo
|
||||
.as_ref()
|
||||
.map(|item| item.created_at)
|
||||
.unwrap_or(now),
|
||||
updated_at: now,
|
||||
};
|
||||
self.registry
|
||||
|
||||
Reference in New Issue
Block a user