Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions rust/crates/sift_cli/assets/skills/sift/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ errors. Read the tool schema instead of guessing. This map only tells you what
exists.

- **Discovery:** `list_assets`, `list_runs`, `list_channels`, `list_reports`,
`list_rules`, `list_rule_versions`, `list_annotations`.
`list_report_templates`, `list_rules`, `list_rule_versions`, `list_annotations`.
- **People:** `list_users`.
- **Report detail:** `list_report_rule_summaries`.
- **Test results:** `list_test_reports`, `list_test_steps`,
Expand All @@ -58,8 +58,8 @@ exists.
- **Docs:** `search_docs`.
- **Writes:** `create_rule`, `update_rule`, `archive_rule`, `unarchive_rule`,
`create_annotation`, `update_annotation`, `create_report`, `update_report`,
`create_test_report`, `append_test_measurements`, `update_asset`,
`update_run`.
`create_report_template`, `update_report_template`, `create_test_report`,
`append_test_measurements`, `update_asset`, `update_run`.

## Workflows that span tools

Expand All @@ -76,6 +76,14 @@ exists.
chart and numbers, do both and give the user both.
- **Answer a question about how Sift works.** Call `search_docs`. Do not answer
from memory, and cite the page you used.
- **Run rules against a run.** Find or author rules with `list_rules` /
`create_rule`. To reuse the same rule set across many runs, bundle standard
rules (`is_external: false`) into a template with `create_report_template`,
then call `create_report` with `report_template_id`. For a one-off — or for
ad-hoc rules (`is_external: true`, which the API also calls "external" but
cannot be attached to a template) — skip the template and pass `rule_ids`,
`rule_client_keys`, or `rule_version_ids` directly to `create_report`. Track
progress via `list_report_rule_summaries`.
Comment thread
evan-sift marked this conversation as resolved.
Outdated

## Rules that always apply

Expand Down
9 changes: 7 additions & 2 deletions rust/crates/sift_mcp/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::policy::RetryPolicy;
use crate::service::{
annotations::AnnotationService, assets::AssetService, channels::ChannelService,
data::DataService, docs::DocsService, ingest::IngestService, ping::PingService,
reports::ReportService, rules::RuleService, runs::RunService, test_reports::TestReportService,
url::UrlService, users::UserService,
report_templates::ReportTemplateService, reports::ReportService, rules::RuleService,
runs::RunService, test_reports::TestReportService, url::UrlService, users::UserService,
};

#[derive(Clone)]
Expand All @@ -31,6 +31,7 @@ pub struct SiftMcpServer {
pub ping_service: PingService,
pub run_service: RunService,
pub report_service: ReportService,
pub report_template_service: ReportTemplateService,
pub rule_service: RuleService,
pub test_report_service: TestReportService,
pub docs_service: DocsService,
Expand Down Expand Up @@ -86,6 +87,7 @@ impl SiftMcpServer {
tool_router.merge(Self::runs_router());
tool_router.merge(Self::channels_router());
tool_router.merge(Self::reports_router());
tool_router.merge(Self::report_templates_router());
tool_router.merge(Self::data_router());
tool_router.merge(Self::explore_router());
tool_router.merge(Self::ping_router());
Expand All @@ -108,6 +110,8 @@ impl SiftMcpServer {
let ping_service = PingService::new(channel.clone(), retry_policy.clone());
let run_service = RunService::new(channel.clone(), retry_policy.clone());
let report_service = ReportService::new(channel.clone(), retry_policy.clone());
let report_template_service =
ReportTemplateService::new(channel.clone(), retry_policy.clone());
let rule_service = RuleService::new(channel.clone(), retry_policy.clone());
let test_report_service = TestReportService::new(channel.clone(), retry_policy.clone());
let docs_service = DocsService::new(channel.clone(), retry_policy.clone());
Expand All @@ -123,6 +127,7 @@ impl SiftMcpServer {
ping_service,
run_service,
report_service,
report_template_service,
rule_service,
test_report_service,
docs_service,
Expand Down
1 change: 1 addition & 0 deletions rust/crates/sift_mcp/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod data;
pub mod docs;
pub mod ingest;
pub mod ping;
pub mod report_templates;
pub mod reports;
pub mod rules;
pub mod runs;
Expand Down
254 changes: 254 additions & 0 deletions rust/crates/sift_mcp/src/service/report_templates/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use crate::policy::{RetryPolicy, with_retry};
use crate::service::common;
use anyhow::{Context, Result, anyhow};
use pbjson_types::FieldMask;
use sift_rs::{
SiftChannel,
metadata::v1::MetadataValue,
report_templates::v1::{
CreateReportTemplateRequest, CreateReportTemplateRequestClientKeys,
CreateReportTemplateRequestRuleIds, ListReportTemplatesRequest,
ListReportTemplatesResponse, ReportTemplate, ReportTemplateRule, ReportTemplateTag,
UpdateReportTemplateRequest, create_report_template_request,
report_template_service_client::ReportTemplateServiceClient,
},
};

#[cfg(test)]
mod test;

#[allow(clippy::enum_variant_names)]
pub enum TemplateRuleIdentifier {
RuleIds(Vec<String>),
RuleClientKeys(Vec<String>),
}

#[derive(Default)]
pub struct ReportTemplateUpdate {
pub name: Option<String>,
pub description: Option<String>,
pub tag_names: Option<Vec<String>>,
pub rules: Option<TemplateRuleIdentifier>,
pub metadata: Option<Vec<MetadataValue>>,
pub is_archived: Option<bool>,
}

#[derive(Clone)]
pub struct ReportTemplateService {
channel: SiftChannel,
policy: RetryPolicy,
}

impl ReportTemplateService {
pub fn new(channel: SiftChannel, policy: RetryPolicy) -> Self {
Self { channel, policy }
}

pub async fn list_report_templates(
&self,
filter: String,
order_by: Option<String>,
limit: Option<u32>,
organization_id: Option<String>,
) -> Result<Vec<ReportTemplate>> {
let (page_size, record_limit) = common::paging(limit);

let mut page_token = String::new();
let mut results = Vec::new();

let order_by = order_by.unwrap_or_default();
let organization_id = organization_id.unwrap_or_default();

loop {
let channel = self.channel.clone();
let filter = filter.clone();
let order_by = order_by.clone();
let organization_id = organization_id.clone();
let token = page_token.clone();

let resp = with_retry(&self.policy, move || {
let channel = channel.clone();
let filter = filter.clone();
let order_by = order_by.clone();
let organization_id = organization_id.clone();
let token = token.clone();
async move {
let mut client = ReportTemplateServiceClient::new(channel);
client
.list_report_templates(ListReportTemplatesRequest {
page_size,
page_token: token,
filter,
organization_id,
order_by,
..Default::default()
})
.await
.map(|resp| resp.into_inner())
}
})
.await
.context("failed to query report templates")?;

let ListReportTemplatesResponse {
report_templates,
next_page_token,
} = resp;
if report_templates.is_empty() {
break;
}
results.extend(report_templates);

if results.len() >= record_limit || next_page_token.is_empty() {
break;
}
page_token = next_page_token;
}

results.truncate(record_limit);

Ok(results)
}

pub async fn create_report_template(
&self,
organization_id: Option<String>,
name: String,
client_key: Option<String>,
description: Option<String>,
tag_names: Vec<String>,
rules: TemplateRuleIdentifier,
metadata: Vec<MetadataValue>,
) -> Result<ReportTemplate> {
let rule_identifiers = match rules {
TemplateRuleIdentifier::RuleIds(rule_ids) => {
create_report_template_request::RuleIdentifiers::RuleIds(
CreateReportTemplateRequestRuleIds { rule_ids },
)
}
TemplateRuleIdentifier::RuleClientKeys(rule_client_keys) => {
create_report_template_request::RuleIdentifiers::RuleClientKeys(
CreateReportTemplateRequestClientKeys { rule_client_keys },
)
}
};

let request = CreateReportTemplateRequest {
name,
client_key,
description,
tag_names,
organization_id: organization_id.unwrap_or_default(),
rule_identifiers: Some(rule_identifiers),
metadata,
};

let channel = self.channel.clone();
let resp = with_retry(&self.policy, move || {
let channel = channel.clone();
let request = request.clone();
async move {
let mut client = ReportTemplateServiceClient::new(channel);
client
.create_report_template(request)
.await
.map(|resp| resp.into_inner())
}
})
.await
.context("failed to create report template")?;

resp.report_template
.ok_or_else(|| anyhow!("create_report_template response missing report_template"))
}

pub async fn update_report_template(
&self,
report_template_id: String,
changes: ReportTemplateUpdate,
) -> Result<ReportTemplate> {
let ReportTemplateUpdate {
name,
description,
tag_names,
rules,
metadata,
is_archived,
} = changes;

let mut template = ReportTemplate {
report_template_id,
..Default::default()
};
let mut paths = Vec::new();

if let Some(name) = name {
template.name = name;
paths.push("name".to_string());
}
if let Some(description) = description {
template.description = Some(description);
paths.push("description".to_string());
}
if let Some(tag_names) = tag_names {
template.tags = tag_names
.into_iter()
.map(|tag_name| ReportTemplateTag { tag_name })
.collect();
paths.push("tags".to_string());
}
if let Some(rules) = rules {
template.rules = template_rules_from_identifier(rules);
paths.push("rules".to_string());
}
if let Some(metadata) = metadata {
template.metadata = metadata;
paths.push("metadata".to_string());
}
if let Some(is_archived) = is_archived {
template.is_archived = is_archived;
paths.push("is_archived".to_string());
}

let channel = self.channel.clone();
let resp = with_retry(&self.policy, move || {
let channel = channel.clone();
let template = template.clone();
let paths = paths.clone();
async move {
let mut client = ReportTemplateServiceClient::new(channel);
client
.update_report_template(UpdateReportTemplateRequest {
report_template: Some(template),
update_mask: Some(FieldMask { paths }),
})
.await
.map(|resp| resp.into_inner())
}
})
.await
.context("failed to update report template")?;

resp.report_template
.ok_or_else(|| anyhow!("update_report_template response missing report_template"))
}
}

fn template_rules_from_identifier(rules: TemplateRuleIdentifier) -> Vec<ReportTemplateRule> {
match rules {
TemplateRuleIdentifier::RuleIds(rule_ids) => rule_ids
.into_iter()
.map(|rule_id| ReportTemplateRule {
rule_id,
..Default::default()
})
.collect(),
TemplateRuleIdentifier::RuleClientKeys(client_keys) => client_keys
.into_iter()
.map(|client_key| ReportTemplateRule {
client_key,
..Default::default()
})
.collect(),
}
}
Loading
Loading