@@ -3,10 +3,11 @@ use datadog_api_client::datadogV2::api_llm_observability::{
33 LLMObservabilityAPI , ListLLMObsAnnotationQueuesOptionalParams ,
44} ;
55use 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
601653pub 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}
0 commit comments