Refresh community demo and docs
Deploy / deploy (push) Successful in 1m42s
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Successful in 4m42s

This commit is contained in:
github-ops
2026-06-21 11:20:12 +00:00
parent cab9282c50
commit c77065756d
24 changed files with 688 additions and 807 deletions
+126 -136
View File
@@ -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,
},
],
}
}
+19 -5
View File
@@ -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
@@ -421,10 +421,25 @@ async fn seeds_demo_assets_for_live_ui() {
.list_operations(&default_workspace_id)
.await
.unwrap();
assert!(operations.len() >= 2);
assert!(
operations
.iter()
.any(|operation| operation.name == "frankfurter_latest_rate")
);
assert!(
!operations
.iter()
.any(|operation| operation.name == "crm_create_lead")
);
assert!(
!operations
.iter()
.any(|operation| operation.name.starts_with("internal_health_smoke_"))
);
let agents = service.list_agents(&default_workspace_id).await.unwrap();
assert!(!agents.is_empty());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].slug, "currency-rates");
assert!(agents.iter().any(|agent| agent.key_count > 0));