Skip to content

Commit 15828dd

Browse files
Merge pull request #728 from gsvigruha/gergely.svigruha/annotation-pup-cli
feat(llm-obs): add annotation queue label schema and annotation commands
2 parents 591a121 + f6d1ffc commit 15828dd

4 files changed

Lines changed: 301 additions & 7 deletions

File tree

docs/COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pup <domain> <subgroup> <action> [options] # Nested commands
6666
| data-deletion | requests (list, create, cancel) | src/commands/data_deletion.rs ||
6767
| data-governance | scanner-rules (list) | src/commands/data_governance.rs ||
6868
| obs-pipelines | list, get, create, update, delete, validate | src/commands/obs_pipelines.rs ||
69-
| llm-obs | projects (create, list), experiments (create, list, update, delete, summary, events (list, get, submit), metric-values, dimension-values), datasets (create, list, batch-update, clone, restore, records, records-add, records-all, records-full), spans (search), patterns (configs (list, get), runs (list, status), topics, topics-with-points, points), agent-insights (list, get, update-status, submit-feedback), model-pricing | src/commands/llm_obs.rs ||
69+
| llm-obs | projects (create, list), experiments (create, list, update, delete, summary, events (list, get, submit), metric-values, dimension-values), datasets (create, list, batch-update, clone, restore, records, records-add, records-all, records-full), spans (search), patterns (configs (list, get), runs (list, status), topics, topics-with-points, points), agent-insights (list, get, update-status, submit-feedback), annotation-queues (create, list, update, delete, interactions (add, delete, list), schema (get, update), annotations (upsert, delete)), model-pricing | src/commands/llm_obs.rs ||
7070
| reference-tables | list, get, create, batch-query | src/commands/reference_tables.rs ||
7171
| network | flows list, devices (list, get, interfaces, tags), interfaces (list, update) | src/commands/network.rs ||
7272
| cloud | aws, gcp, azure, oci | src/commands/cloud.rs ||

src/client.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ static UNSTABLE_OPS: &[&str] = &[
301301
"v2.delete_aws_cloud_auth_persona_mapping",
302302
"v2.get_aws_cloud_auth_persona_mapping",
303303
"v2.list_aws_cloud_auth_persona_mappings",
304-
// LLM Observability (21)
304+
// LLM Observability (25)
305305
"v2.create_llm_obs_project",
306306
"v2.list_llm_obs_projects",
307307
"v2.create_llm_obs_experiment",
@@ -320,6 +320,10 @@ static UNSTABLE_OPS: &[&str] = &[
320320
"v2.create_llm_obs_annotation_queue_interactions",
321321
"v2.delete_llm_obs_annotation_queue_interactions",
322322
"v2.get_llm_obs_annotated_interactions",
323+
"v2.get_llm_obs_annotation_queue_label_schema",
324+
"v2.update_llm_obs_annotation_queue_label_schema",
325+
"v2.upsert_llm_obs_annotations",
326+
"v2.delete_llm_obs_annotations",
323327
"v2.get_llm_obs_custom_eval_config",
324328
"v2.update_llm_obs_custom_eval_config",
325329
"v2.delete_llm_obs_custom_eval_config",
@@ -516,7 +520,7 @@ mod tests {
516520

517521
#[test]
518522
fn test_unstable_ops_count() {
519-
assert_eq!(UNSTABLE_OPS.len(), 186);
523+
assert_eq!(UNSTABLE_OPS.len(), 190);
520524
}
521525

522526
#[test]

src/commands/llm_obs.rs

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use datadog_api_client::datadogV2::api_llm_observability::{
33
LLMObservabilityAPI, ListLLMObsAnnotationQueuesOptionalParams,
44
};
55
use datadog_api_client::datadogV2::model::{
6-
LLMObsAnnotationQueueInteractionsRequest, LLMObsAnnotationQueueRequest,
7-
LLMObsAnnotationQueueUpdateRequest, LLMObsCustomEvalConfigUpdateRequest,
8-
LLMObsDatasetBatchUpdateRequest, LLMObsDatasetCloneRequest, LLMObsDatasetRequest,
9-
LLMObsDatasetRestoreVersionRequest, LLMObsDeleteAnnotationQueueInteractionsRequest,
6+
LLMObsAnnotationQueueInteractionsRequest, LLMObsAnnotationQueueLabelSchemaUpdateRequest,
7+
LLMObsAnnotationQueueRequest, LLMObsAnnotationQueueUpdateRequest, LLMObsAnnotationsRequest,
8+
LLMObsCustomEvalConfigUpdateRequest, LLMObsDatasetBatchUpdateRequest,
9+
LLMObsDatasetCloneRequest, LLMObsDatasetRequest, LLMObsDatasetRestoreVersionRequest,
10+
LLMObsDeleteAnnotationQueueInteractionsRequest, LLMObsDeleteAnnotationsRequest,
1011
LLMObsDeleteExperimentsRequest, LLMObsProjectRequest,
1112
};
1213

@@ -596,6 +597,57 @@ pub async fn annotation_queue_interactions_list(cfg: &Config, queue_id: &str) ->
596597
formatter::output(cfg, &resp)
597598
}
598599

600+
pub async fn annotation_queue_schema_get(cfg: &Config, queue_id: &str) -> Result<()> {
601+
let api = make_api(cfg);
602+
let resp = api
603+
.get_llm_obs_annotation_queue_label_schema(queue_id.to_string())
604+
.await
605+
.map_err(|e| anyhow::anyhow!("failed to get annotation queue label schema: {e:?}"))?;
606+
formatter::output(cfg, &resp)
607+
}
608+
609+
pub async fn annotation_queue_schema_update(
610+
cfg: &Config,
611+
queue_id: &str,
612+
file: &str,
613+
) -> Result<()> {
614+
let body: LLMObsAnnotationQueueLabelSchemaUpdateRequest = util::read_json_file(file)?;
615+
let api = make_api(cfg);
616+
let resp = api
617+
.update_llm_obs_annotation_queue_label_schema(queue_id.to_string(), body)
618+
.await
619+
.map_err(|e| anyhow::anyhow!("failed to update annotation queue label schema: {e:?}"))?;
620+
formatter::output(cfg, &resp)
621+
}
622+
623+
pub async fn annotation_queue_annotations_upsert(
624+
cfg: &Config,
625+
queue_id: &str,
626+
file: &str,
627+
) -> Result<()> {
628+
let body: LLMObsAnnotationsRequest = util::read_json_file(file)?;
629+
let api = make_api(cfg);
630+
let resp = api
631+
.upsert_llm_obs_annotations(queue_id.to_string(), body)
632+
.await
633+
.map_err(|e| anyhow::anyhow!("failed to upsert annotations: {e:?}"))?;
634+
formatter::output(cfg, &resp)
635+
}
636+
637+
pub async fn annotation_queue_annotations_delete(
638+
cfg: &Config,
639+
queue_id: &str,
640+
file: &str,
641+
) -> Result<()> {
642+
let body: LLMObsDeleteAnnotationsRequest = util::read_json_file(file)?;
643+
let api = make_api(cfg);
644+
let resp = api
645+
.delete_llm_obs_annotations(queue_id.to_string(), body)
646+
.await
647+
.map_err(|e| anyhow::anyhow!("failed to delete annotations: {e:?}"))?;
648+
formatter::output(cfg, &resp)
649+
}
650+
599651
// ---- Custom Evaluator Configs ----
600652

601653
pub async fn eval_config_get(cfg: &Config, eval_name: &str) -> Result<()> {
@@ -4961,4 +5013,173 @@ mod tests {
49615013
assert!(result.is_err(), "should fail on 500");
49625014
cleanup_env();
49635015
}
5016+
5017+
// ---- Annotation queue label schemas ----
5018+
5019+
const LABEL_SCHEMA_BODY: &str = r#"{"data":{"id":"queue-1","type":"queues","attributes":{"annotation_schema":{"label_schemas":[{"id":"ls-1","name":"quality","type":"score","min":0.0,"max":5.0,"is_required":true}]}}}}"#;
5020+
5021+
#[tokio::test]
5022+
async fn test_annotation_queue_schema_get() {
5023+
let _lock = lock_env().await;
5024+
let mut server = mockito::Server::new_async().await;
5025+
let cfg = test_config(&server.url());
5026+
let _mock = mock_any(&mut server, "GET", LABEL_SCHEMA_BODY).await;
5027+
5028+
let result = super::annotation_queue_schema_get(&cfg, "queue-1").await;
5029+
assert!(result.is_ok(), "schema_get failed: {:?}", result.err());
5030+
cleanup_env();
5031+
}
5032+
5033+
#[tokio::test]
5034+
async fn test_annotation_queue_schema_get_404() {
5035+
let _lock = lock_env().await;
5036+
let mut server = mockito::Server::new_async().await;
5037+
let cfg = test_config(&server.url());
5038+
let _mock = server
5039+
.mock("GET", mockito::Matcher::Any)
5040+
.match_query(mockito::Matcher::Any)
5041+
.with_status(404)
5042+
.with_header("content-type", "application/json")
5043+
.with_body(r#"{"errors":["queue not found"]}"#)
5044+
.create_async()
5045+
.await;
5046+
5047+
let result = super::annotation_queue_schema_get(&cfg, "missing-queue").await;
5048+
assert!(result.is_err(), "should fail on 404");
5049+
cleanup_env();
5050+
}
5051+
5052+
#[tokio::test]
5053+
async fn test_annotation_queue_schema_update() {
5054+
let _lock = lock_env().await;
5055+
let mut server = mockito::Server::new_async().await;
5056+
let cfg = test_config(&server.url());
5057+
let _mock = mock_any(&mut server, "PUT", LABEL_SCHEMA_BODY).await;
5058+
5059+
let path = write_temp_json(
5060+
"pup_aq_schema_update.json",
5061+
r#"{"data":{"type":"queues","attributes":{"annotation_schema":{"label_schemas":[{"name":"quality","type":"score","min":0.0,"max":5.0}]}}}}"#,
5062+
);
5063+
let result =
5064+
super::annotation_queue_schema_update(&cfg, "queue-1", path.to_str().unwrap()).await;
5065+
assert!(result.is_ok(), "schema_update failed: {:?}", result.err());
5066+
let _ = std::fs::remove_file(&path);
5067+
cleanup_env();
5068+
}
5069+
5070+
#[tokio::test]
5071+
async fn test_annotation_queue_schema_update_missing_file() {
5072+
let _lock = lock_env().await;
5073+
let server = mockito::Server::new_async().await;
5074+
let cfg = test_config(&server.url());
5075+
5076+
let result =
5077+
super::annotation_queue_schema_update(&cfg, "queue-1", "/nonexistent/schema.json")
5078+
.await;
5079+
let err = result
5080+
.expect_err("should fail on unreadable file")
5081+
.to_string();
5082+
assert!(err.contains("failed to read file"), "unexpected: {err}");
5083+
cleanup_env();
5084+
}
5085+
5086+
#[tokio::test]
5087+
async fn test_annotation_queue_schema_update_malformed_json() {
5088+
let _lock = lock_env().await;
5089+
let server = mockito::Server::new_async().await;
5090+
let cfg = test_config(&server.url());
5091+
5092+
// Valid JSON but missing the required `data` member.
5093+
let path = write_temp_json("pup_aq_schema_bad.json", r#"{"nope":true}"#);
5094+
let result =
5095+
super::annotation_queue_schema_update(&cfg, "queue-1", path.to_str().unwrap()).await;
5096+
let err = result.expect_err("should reject bad body").to_string();
5097+
assert!(err.contains("failed to parse JSON"), "unexpected: {err}");
5098+
let _ = std::fs::remove_file(&path);
5099+
cleanup_env();
5100+
}
5101+
5102+
// ---- Annotations on queue interactions ----
5103+
5104+
#[tokio::test]
5105+
async fn test_annotation_queue_annotations_upsert() {
5106+
let _lock = lock_env().await;
5107+
let mut server = mockito::Server::new_async().await;
5108+
let cfg = test_config(&server.url());
5109+
let body = r#"{"data":{"id":"upsert-1","type":"annotations","attributes":{"annotations":[{"id":"a-1","interaction_id":"i-1","created_at":"2024-01-01T00:00:00Z","created_by":"user-1","modified_at":"2024-01-01T00:00:00Z","modified_by":"user-1","label_values":[{"label_schema_id":"ls-1","value":4.0}]}]}}}"#;
5110+
let _mock = mock_any(&mut server, "POST", body).await;
5111+
5112+
let path = write_temp_json(
5113+
"pup_aq_annotations_upsert.json",
5114+
r#"{"data":{"type":"annotations","attributes":{"annotations":[{"interaction_id":"i-1","label_values":[{"label_schema_id":"ls-1","value":4.0}]}]}}}"#,
5115+
);
5116+
let result =
5117+
super::annotation_queue_annotations_upsert(&cfg, "queue-1", path.to_str().unwrap())
5118+
.await;
5119+
assert!(result.is_ok(), "upsert failed: {:?}", result.err());
5120+
let _ = std::fs::remove_file(&path);
5121+
cleanup_env();
5122+
}
5123+
5124+
#[tokio::test]
5125+
async fn test_annotation_queue_annotations_upsert_400() {
5126+
let _lock = lock_env().await;
5127+
let mut server = mockito::Server::new_async().await;
5128+
let cfg = test_config(&server.url());
5129+
let _mock = server
5130+
.mock("POST", mockito::Matcher::Any)
5131+
.match_query(mockito::Matcher::Any)
5132+
.with_status(400)
5133+
.with_header("content-type", "application/json")
5134+
.with_body(r#"{"errors":["unknown label_schema_id"]}"#)
5135+
.create_async()
5136+
.await;
5137+
5138+
let path = write_temp_json(
5139+
"pup_aq_annotations_upsert_400.json",
5140+
r#"{"data":{"type":"annotations","attributes":{"annotations":[{"interaction_id":"i-1","label_values":[{"label_schema_id":"bogus","value":4.0}]}]}}}"#,
5141+
);
5142+
let result =
5143+
super::annotation_queue_annotations_upsert(&cfg, "queue-1", path.to_str().unwrap())
5144+
.await;
5145+
assert!(result.is_err(), "should fail on 400");
5146+
let _ = std::fs::remove_file(&path);
5147+
cleanup_env();
5148+
}
5149+
5150+
#[tokio::test]
5151+
async fn test_annotation_queue_annotations_delete() {
5152+
let _lock = lock_env().await;
5153+
let mut server = mockito::Server::new_async().await;
5154+
let cfg = test_config(&server.url());
5155+
let body = r#"{"data":{"id":"delete-1","type":"annotations","attributes":{"annotation_ids":["a-1"],"errors":[]}}}"#;
5156+
let _mock = mock_any(&mut server, "POST", body).await;
5157+
5158+
let path = write_temp_json(
5159+
"pup_aq_annotations_delete.json",
5160+
r#"{"data":{"type":"annotations","attributes":{"annotation_ids":["a-1"]}}}"#,
5161+
);
5162+
let result =
5163+
super::annotation_queue_annotations_delete(&cfg, "queue-1", path.to_str().unwrap())
5164+
.await;
5165+
assert!(result.is_ok(), "delete failed: {:?}", result.err());
5166+
let _ = std::fs::remove_file(&path);
5167+
cleanup_env();
5168+
}
5169+
5170+
#[tokio::test]
5171+
async fn test_annotation_queue_annotations_delete_malformed_json() {
5172+
let _lock = lock_env().await;
5173+
let server = mockito::Server::new_async().await;
5174+
let cfg = test_config(&server.url());
5175+
5176+
let path = write_temp_json("pup_aq_annotations_delete_bad.json", r#"{ not json"#);
5177+
let result =
5178+
super::annotation_queue_annotations_delete(&cfg, "queue-1", path.to_str().unwrap())
5179+
.await;
5180+
let err = result.expect_err("should reject bad JSON").to_string();
5181+
assert!(err.contains("failed to parse JSON"), "unexpected: {err}");
5182+
let _ = std::fs::remove_file(&path);
5183+
cleanup_env();
5184+
}
49645185
}

src/main.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9886,6 +9886,50 @@ enum LlmObsAnnotationQueuesActions {
98869886
#[command(subcommand)]
98879887
action: LlmObsAnnotationQueueInteractionsActions,
98889888
},
9889+
/// Manage the label schema of an annotation queue
9890+
Schema {
9891+
#[command(subcommand)]
9892+
action: LlmObsAnnotationQueueSchemaActions,
9893+
},
9894+
/// Manage annotations on interactions in an annotation queue
9895+
Annotations {
9896+
#[command(subcommand)]
9897+
action: LlmObsAnnotationQueueAnnotationsActions,
9898+
},
9899+
}
9900+
9901+
#[derive(Subcommand)]
9902+
enum LlmObsAnnotationQueueSchemaActions {
9903+
/// Get the label schema for an annotation queue
9904+
Get {
9905+
#[arg(help = "Annotation queue ID")]
9906+
queue_id: String,
9907+
},
9908+
/// Replace the label schema for an annotation queue
9909+
Update {
9910+
#[arg(help = "Annotation queue ID")]
9911+
queue_id: String,
9912+
#[arg(long, help = "JSON file with label schema update body (required)")]
9913+
file: String,
9914+
},
9915+
}
9916+
9917+
#[derive(Subcommand)]
9918+
enum LlmObsAnnotationQueueAnnotationsActions {
9919+
/// Create or update annotations on interactions in an annotation queue
9920+
Upsert {
9921+
#[arg(help = "Annotation queue ID")]
9922+
queue_id: String,
9923+
#[arg(long, help = "JSON file with annotations body (required)")]
9924+
file: String,
9925+
},
9926+
/// Delete annotations from interactions in an annotation queue
9927+
Delete {
9928+
#[arg(help = "Annotation queue ID")]
9929+
queue_id: String,
9930+
#[arg(long, help = "JSON file with annotations to delete (required)")]
9931+
file: String,
9932+
},
98899933
}
98909934

98919935
#[derive(Subcommand)]
@@ -17032,6 +17076,31 @@ async fn main_inner() -> anyhow::Result<()> {
1703217076
.await?;
1703317077
}
1703417078
},
17079+
LlmObsAnnotationQueuesActions::Schema { action } => match action {
17080+
LlmObsAnnotationQueueSchemaActions::Get { queue_id } => {
17081+
commands::llm_obs::annotation_queue_schema_get(&cfg, &queue_id).await?;
17082+
}
17083+
LlmObsAnnotationQueueSchemaActions::Update { queue_id, file } => {
17084+
commands::llm_obs::annotation_queue_schema_update(
17085+
&cfg, &queue_id, &file,
17086+
)
17087+
.await?;
17088+
}
17089+
},
17090+
LlmObsAnnotationQueuesActions::Annotations { action } => match action {
17091+
LlmObsAnnotationQueueAnnotationsActions::Upsert { queue_id, file } => {
17092+
commands::llm_obs::annotation_queue_annotations_upsert(
17093+
&cfg, &queue_id, &file,
17094+
)
17095+
.await?;
17096+
}
17097+
LlmObsAnnotationQueueAnnotationsActions::Delete { queue_id, file } => {
17098+
commands::llm_obs::annotation_queue_annotations_delete(
17099+
&cfg, &queue_id, &file,
17100+
)
17101+
.await?;
17102+
}
17103+
},
1703517104
},
1703617105
LlmObsActions::EvalConfig { action } => match action {
1703717106
LlmObsEvalConfigActions::Get { eval_name } => {

0 commit comments

Comments
 (0)