diff --git a/cli/src/commands/queue.rs b/cli/src/commands/queue.rs index 09f6c6766..1476333d2 100644 --- a/cli/src/commands/queue.rs +++ b/cli/src/commands/queue.rs @@ -8,8 +8,9 @@ use crate::error::Result; use crate::output::{OutputFormat, OutputFormatter}; use clap::{Args, Subcommand}; use langstar_sdk::{ - AnnotationQueue, CreateAnnotationQueueRequest, LangchainClient, ListAnnotationQueuesParams, - QueueType, UpdateAnnotationQueueRequest, + AnnotationQueue, AnnotationQueueItemStatus, AnnotationQueueListItem, + CreateAnnotationQueueRequest, LangchainClient, ListAnnotationQueueItemsParams, + ListAnnotationQueuesParams, QueueType, UpdateAnnotationQueueRequest, }; use serde::Serialize; use std::io::{BufRead, BufReader}; @@ -38,7 +39,7 @@ pub enum QueueCommands { AddRuns(AddRunsArgs), /// Remove a run from an annotation queue RemoveRun(RemoveRunArgs), - /// List runs in an annotation queue + /// List the items in an annotation queue, one review section at a time Items(ItemsArgs), } @@ -166,6 +167,10 @@ pub struct ItemsArgs { #[arg(short, long, default_value = "100")] pub limit: u32, + /// Review section to list: needs-my-review, needs-others-review or archived (default: all) + #[arg(long, value_parser = ["needs-my-review", "needs-others-review", "archived"])] + pub status: Option, + /// Output as JSON #[arg(long)] pub json: bool, @@ -226,12 +231,12 @@ impl From<&AnnotationQueue> for QueueRow { /// Queue item row for table display #[derive(Debug, Tabled, Serialize)] struct QueueItemRow { - #[tabled(rename = "Index")] - index: u32, + #[tabled(rename = "Item ID")] + id: String, #[tabled(rename = "Run ID")] run_id: String, - #[tabled(rename = "Name")] - name: String, + #[tabled(rename = "Type")] + item_type: String, #[tabled(rename = "Status")] status: String, #[tabled(rename = "Added")] @@ -439,11 +444,11 @@ impl QueueCommands { let auth = config.to_auth_config(); let client = LangchainClient::new(auth)?; - client - .add_runs_to_annotation_queue(args.queue_id, run_ids.clone()) + let items = client + .add_runs_to_annotation_queue(args.queue_id, run_ids) .await?; - println!("Added {} runs to queue {}", run_ids.len(), args.queue_id); + println!("Added {} runs to queue {}", items.len(), args.queue_id); Ok(()) } @@ -452,10 +457,17 @@ impl QueueCommands { let auth = config.to_auth_config(); let client = LangchainClient::new(auth)?; - client + let removed = client .delete_run_from_annotation_queue(args.queue_id, args.run_id) .await?; + if !removed { + return Err(crate::error::CliError::Other(anyhow::anyhow!( + "Run {} is not in queue {}", + args.run_id, + args.queue_id + ))); + } println!("Removed run {} from queue {}", args.run_id, args.queue_id); Ok(()) @@ -471,37 +483,24 @@ impl QueueCommands { let auth = config.to_auth_config(); let client = LangchainClient::new(auth)?; - // Fetch items by index (API only supports get by index) - // Note: Sequential fetching is required as the API only provides get-by-index - let mut items = Vec::new(); - for index in 0..args.limit { - match client - .get_run_from_annotation_queue(args.queue_id, index) - .await - { - Ok(item) => items.push((index, item)), - Err(langstar_sdk::LangstarError::ApiError { status: 404, .. }) => { - // 404 means no more items at this index - end of queue - break; - } - Err(e) => { - // Propagate other errors (auth failures, network issues, etc.) - return Err(e.into()); - } - } - } + let items = collect_items(&client, args).await?; if args.json { - let json_items: Vec<_> = items.iter().map(|(_, item)| item).collect(); - println!("{}", serde_json::to_string_pretty(&json_items)?); + println!("{}", items_json(&items)?); } else { let rows: Vec = items .iter() - .map(|(index, item)| QueueItemRow { - index: *index, - run_id: item.run.id.to_string().chars().take(8).collect(), - name: item.run.name.chars().take(30).collect(), - status: item.run.status.clone(), + .map(|(status, item)| QueueItemRow { + id: item.id.to_string().chars().take(8).collect(), + run_id: item + .run_id + .map(|id| id.to_string().chars().take(8).collect()) + .unwrap_or_else(|| "-".to_string()), + item_type: item + .item_type + .map(|t| t.as_str().to_string()) + .unwrap_or_else(|| "-".to_string()), + status: status.as_str().to_string(), added: item .added_at .map(|t| t.format("%Y-%m-%d").to_string()) @@ -516,6 +515,48 @@ impl QueueCommands { } } +/// An item and the review section it was listed from. +type SectionItem = (AnnotationQueueItemStatus, AnnotationQueueListItem); + +/// Lists the items in the sections `--status` names (every section without it), +/// at most `--limit` in all. +async fn collect_items(client: &LangchainClient, args: &ItemsArgs) -> Result> { + let statuses = match args.status.as_deref() { + Some("needs-my-review") => vec![AnnotationQueueItemStatus::NeedsMyReview], + Some("needs-others-review") => vec![AnnotationQueueItemStatus::NeedsOthersReview], + Some("archived") => vec![AnnotationQueueItemStatus::Archived], + _ => AnnotationQueueItemStatus::ALL.to_vec(), + }; + + let mut items = Vec::new(); + for status in statuses { + let remaining = args.limit as usize - items.len(); + if remaining == 0 { + break; + } + let mut params = ListAnnotationQueueItemsParams::new(status); + params.page_size = Some(args.limit.clamp(1, 100)); + let section = client + .collect_annotation_queue_items(args.queue_id, params, Some(remaining)) + .await?; + items.extend(section.into_iter().map(|item| (status, item))); + } + Ok(items) +} + +/// The `--json` output: each item with a `status` field naming its section. +fn items_json(items: &[SectionItem]) -> Result { + let json_items = items + .iter() + .map(|(status, item)| { + let mut value = serde_json::to_value(item)?; + value["status"] = serde_json::json!(status.as_str()); + Ok(value) + }) + .collect::, serde_json::Error>>()?; + Ok(serde_json::to_string_pretty(&json_items)?) +} + #[cfg(test)] mod tests { use super::*; @@ -525,11 +566,11 @@ mod tests { let json = r#"{ "id": "12345678-1234-1234-1234-123456789012", "name": "Test Queue", - "tenantId": "87654321-4321-4321-4321-210987654321", - "queueType": "single", + "tenant_id": "87654321-4321-4321-4321-210987654321", + "queue_type": "single", "description": "A very long description that should be truncated for display", - "createdAt": "2024-01-01T12:00:00Z", - "updatedAt": "2024-01-01T12:00:00Z" + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z" }"#; let queue: AnnotationQueue = serde_json::from_str(json).unwrap(); @@ -538,4 +579,124 @@ mod tests { assert_eq!(row.id, "12345678"); assert!(row.description.ends_with("...")); } + + const QUEUE: &str = "12345678-1234-1234-1234-123456789012"; + + /// A mock LangSmith that holds one item in each review section and records + /// which sections the CLI asked for. + struct SectionServer { + server: mockito::ServerGuard, + mocks: Vec<(AnnotationQueueItemStatus, Uuid, mockito::Mock)>, + } + + impl SectionServer { + async fn new(listed: &[AnnotationQueueItemStatus]) -> Self { + let mut server = mockito::Server::new_async().await; + let mut mocks = Vec::new(); + for status in AnnotationQueueItemStatus::ALL { + let item_id = Uuid::new_v4(); + let body = serde_json::json!({"items": [{ + "id": item_id, + "queue_id": QUEUE, + "item_type": "RUN", + "run_id": Uuid::new_v4(), + "added_at": "2026-01-01T00:00:00Z" + }]}); + let mock = server + .mock( + "GET", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items").as_str(), + ) + .match_query(mockito::Matcher::UrlEncoded( + "status".into(), + status.as_str().into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(body.to_string()) + .expect(usize::from(listed.contains(&status))) + .create_async() + .await; + mocks.push((status, item_id, mock)); + } + Self { server, mocks } + } + + fn client(&self) -> LangchainClient { + LangchainClient::with_base_urls( + langstar_sdk::AuthConfig::new(Some("test_key".to_string()), None, None), + self.server.url(), + self.server.url(), + self.server.url(), + ) + .unwrap() + } + + fn item_id(&self, status: AnnotationQueueItemStatus) -> String { + let (_, id, _) = self.mocks.iter().find(|(s, _, _)| *s == status).unwrap(); + id.to_string() + } + + async fn assert_sections_listed(&self) { + for (_, _, mock) in &self.mocks { + mock.assert_async().await; + } + } + } + + /// Runs what `queue items [--status ] --json` runs, listing + /// and JSON output, against `server` and returns each printed item's (id, status). + async fn items_output(server: &SectionServer, status: Option<&str>) -> Vec<(String, String)> { + let args = ItemsArgs { + queue_id: Uuid::parse_str(QUEUE).unwrap(), + limit: 100, + status: status.map(str::to_string), + json: true, + }; + let items = collect_items(&server.client(), &args).await.unwrap(); + let printed: Vec = + serde_json::from_str(&items_json(&items).unwrap()).unwrap(); + printed + .iter() + .map(|item| { + ( + item["id"].as_str().unwrap().to_string(), + item["status"].as_str().unwrap().to_string(), + ) + }) + .collect() + } + + #[tokio::test] + async fn test_items_status_lists_only_the_named_section() { + for (flag, status) in [ + ("needs-my-review", AnnotationQueueItemStatus::NeedsMyReview), + ( + "needs-others-review", + AnnotationQueueItemStatus::NeedsOthersReview, + ), + ("archived", AnnotationQueueItemStatus::Archived), + ] { + let server = SectionServer::new(&[status]).await; + let output = items_output(&server, Some(flag)).await; + assert_eq!( + output, + vec![(server.item_id(status), status.as_str().to_string())], + "--status {flag}" + ); + server.assert_sections_listed().await; + } + } + + #[tokio::test] + async fn test_items_without_status_lists_every_section() { + let server = SectionServer::new(&AnnotationQueueItemStatus::ALL).await; + let output = items_output(&server, None).await; + let expected: Vec<(String, String)> = AnnotationQueueItemStatus::ALL + .iter() + .map(|status| (server.item_id(*status), status.as_str().to_string())) + .collect(); + assert_eq!(output, expected); + server.assert_sections_listed().await; + } } diff --git a/cli/tests/queue_command_test.rs b/cli/tests/queue_command_test.rs new file mode 100644 index 000000000..6a6697971 --- /dev/null +++ b/cli/tests/queue_command_test.rs @@ -0,0 +1,360 @@ +//! CLI tests for `langstar queue` run commands (`add-runs`, `items`, `remove-run`). +//! +//! The commands under test call LangSmith's platform items API, +//! `/api/v1/platform/annotation-queues/{queue_id}/items`, so the lifecycle test +//! asserts on the items the CLI and SDK get back, not only on exit codes. +//! +//! **Prerequisites for Integration Tests:** +//! +//! - `LANGSMITH_API_KEY`, `LANGSMITH_ORGANIZATION_ID` and `LANGSMITH_WORKSPACE_ID` +//! +//! Run with: `cargo test --test queue_command_test --features integration-tests` + +#[path = "common/config_dir.rs"] +mod config_dir; + +use assert_cmd::Command; +use chrono::Utc; +use langstar_sdk::{ + AnnotationQueueItemCountStatus, AnnotationQueueItemStatus, AnnotationQueueItemType, AuthConfig, + LangchainClient, ListAnnotationQueueItemsParams, PatchAnnotationQueueItemRequest, + ProjectCreate, +}; +use serde_json::{Value, json}; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +fn langstar_cmd() -> Command { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_langstar")); + cmd.env("LANGSTAR_CONFIG_DIR", config_dir::empty_config_dir()); + cmd +} + +#[test] +fn test_queue_items_help_lists_status_flag() { + let output = langstar_cmd() + .args(["queue", "items", "--help"]) + .output() + .expect("Failed to execute CLI"); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("--status"), + "help should list --status: {stdout}" + ); +} + +#[test] +fn test_queue_items_rejects_unknown_status() { + let output = langstar_cmd() + .args([ + "queue", + "items", + "12345678-1234-1234-1234-123456789012", + "--status", + "bogus", + ]) + .output() + .expect("Failed to execute CLI"); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("bogus")); +} + +/// Deletes the test project, and the runs in it, when the test ends. +struct ProjectCleanup { + client: LangchainClient, + project_id: Uuid, +} + +impl Drop for ProjectCleanup { + fn drop(&mut self) { + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + if let Err(e) = runtime.block_on(self.client.delete_project(self.project_id)) { + eprintln!("[CLEANUP] Failed to delete test project: {}", e); + } + } +} + +/// Deletes the test queue when the test ends, if the test did not already. +struct QueueCleanup { + client: LangchainClient, + queue_id: Uuid, +} + +impl Drop for QueueCleanup { + fn drop(&mut self) { + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + // Ignore the result: the test deletes the queue itself on the success path. + let _ = runtime.block_on(self.client.delete_annotation_queue(self.queue_id)); + } +} + +/// Posts one root run to the ingest endpoint, `POST /api/v1/runs`, and returns its ID. +async fn ingest_run(client: &LangchainClient, project_name: &str, name: &str) -> Uuid { + let id = Uuid::new_v4(); + let start = Utc::now() - chrono::Duration::minutes(5); + let end = start + chrono::Duration::milliseconds(250); + let dotted_order = format!("{}{}", start.format("%Y%m%dT%H%M%S%6fZ"), id); + + let body = json!({ + "id": id, + "trace_id": id, + "dotted_order": dotted_order, + "name": name, + "run_type": "chain", + "session_name": project_name, + "start_time": start.to_rfc3339(), + "end_time": end.to_rfc3339(), + "inputs": {"question": "seed"}, + "outputs": {"answer": "seed"}, + }); + + let response = client + .langsmith_post("/api/v1/runs") + .expect("Failed to build ingest request") + .json(&body) + .send() + .await + .expect("Failed to send ingest request"); + assert!( + response.status().is_success(), + "Ingesting run '{}' failed with HTTP {}", + name, + response.status() + ); + id +} + +/// Runs a CLI command that must succeed and returns its stdout. +fn run_ok(args: &[&str]) -> String { + let output = langstar_cmd() + .args(args) + .output() + .expect("Failed to execute CLI"); + assert!( + output.status.success(), + "langstar {:?} failed: {}", + args, + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// `queue items --json` as a list of items. +fn queue_items_json(queue_id: &str) -> Vec { + let stdout = run_ok(&["queue", "items", queue_id, "--json"]); + serde_json::from_str(&stdout).expect("queue items --json should print a JSON array") +} + +fn run_ids(items: &[Value]) -> Vec<&str> { + let mut ids: Vec<&str> = items + .iter() + .map(|i| i["run_id"].as_str().expect("item should carry a run_id")) + .collect(); + ids.sort(); + ids +} + +/// Pattern: CREATE queue → ADD runs (CLI) → READ items (CLI, SDK) → UPDATE → REMOVE → DELETE +#[test] +#[cfg_attr(not(feature = "integration-tests"), ignore)] +fn test_queue_items_lifecycle() { + let _api_key = std::env::var("LANGSMITH_API_KEY") + .expect("LANGSMITH_API_KEY must be set for integration tests"); + let _org_id = std::env::var("LANGSMITH_ORGANIZATION_ID") + .expect("LANGSMITH_ORGANIZATION_ID must be set for integration tests"); + let _workspace_id = std::env::var("LANGSMITH_WORKSPACE_ID") + .expect("LANGSMITH_WORKSPACE_ID must be set for integration tests"); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let auth = AuthConfig::from_env().expect("Auth config required"); + let client = LangchainClient::new(auth).expect("SDK client required"); + let suffix = &Uuid::new_v4().to_string()[..12]; + + // ── CREATE: a project with two runs, and a queue ── + let project_name = format!("test-queue-items-{suffix}"); + let project = runtime + .block_on(client.create_project(ProjectCreate { + name: Some(project_name.clone()), + description: Some("Test project for queue items lifecycle".to_string()), + ..Default::default() + })) + .expect("Failed to create test project"); + let _project_cleanup = ProjectCleanup { + client: client.clone(), + project_id: project.id, + }; + let (run_a, run_b) = runtime.block_on(async { + ( + ingest_run(&client, &project_name, "queue-run-a").await, + ingest_run(&client, &project_name, "queue-run-b").await, + ) + }); + + let queue_name = format!("test-queue-{suffix}"); + let created: Value = serde_json::from_str(&run_ok(&[ + "queue", + "create", + "--name", + &queue_name, + "--json", + ])) + .expect("queue create --json should print JSON"); + let queue_id = created["id"] + .as_str() + .expect("queue should have an id") + .to_string(); + let queue_uuid = Uuid::parse_str(&queue_id).unwrap(); + let _queue_cleanup = QueueCleanup { + client: client.clone(), + queue_id: queue_uuid, + }; + println!("[CREATE] Created test queue and 2 runs"); + + // ── Wait until both runs are queryable: the items API looks runs up by ID ── + let project_id = project.id.to_string(); + let deadline = Instant::now() + Duration::from_secs(180); + loop { + let stdout = run_ok(&[ + "runs", + "query", + "-p", + &project_id, + "--since", + "1h", + "-o", + "json", + ]); + let runs: Vec = serde_json::from_str(&stdout).expect("runs query should print JSON"); + if runs.len() >= 2 { + break; + } + assert!( + Instant::now() <= deadline, + "only {} of 2 runs visible after 180s", + runs.len() + ); + std::thread::sleep(Duration::from_secs(5)); + } + + // ── ADD (CLI) ── + let (a, b) = (run_a.to_string(), run_b.to_string()); + let stdout = run_ok(&["queue", "add-runs", &queue_id, &a, &b]); + assert!(stdout.contains("Added 2 runs"), "add-runs output: {stdout}"); + + // ── READ (CLI): both runs show up as RUN items, wait for them to be visible ── + let deadline = Instant::now() + Duration::from_secs(120); + let items = loop { + let items = queue_items_json(&queue_id); + if items.len() >= 2 { + break items; + } + assert!( + Instant::now() <= deadline, + "only {} of 2 items visible after 120s", + items.len() + ); + std::thread::sleep(Duration::from_secs(3)); + }; + let mut expected = vec![a.as_str(), b.as_str()]; + expected.sort(); + assert_eq!(run_ids(&items), expected); + for item in &items { + assert_eq!(item["item_type"], "RUN"); + assert!( + ["needs_my_review", "needs_others_review", "archived"] + .contains(&item["status"].as_str().expect("item should carry a status")), + "status should name a review section: {item}" + ); + assert_eq!(item["queue_id"].as_str(), Some(queue_id.as_str())); + assert!(item["id"].is_string(), "item should carry a queue item id"); + } + println!("[READ] queue items returned {} items", items.len()); + + // ── READ (SDK): count and placement agree with the list ── + let first_item_id = items[0]["id"].as_str().unwrap().to_string(); + let counted = runtime + .block_on( + client + .count_annotation_queue_items(queue_uuid, AnnotationQueueItemStatus::NeedsMyReview), + ) + .expect("count failed"); + let others = + runtime + .block_on(client.count_annotation_queue_items( + queue_uuid, + AnnotationQueueItemStatus::NeedsOthersReview, + )) + .expect("count failed"); + assert_eq!(counted + others, 2, "count should match the two items"); + let total = runtime + .block_on( + client.count_annotation_queue_items(queue_uuid, AnnotationQueueItemCountStatus::All), + ) + .expect("count all failed"); + assert_eq!(total, 2, "count with status all should cover every item"); + + let page = runtime + .block_on(client.list_annotation_queue_items( + queue_uuid, + ListAnnotationQueueItemsParams::new(AnnotationQueueItemStatus::NeedsMyReview), + None, + )) + .expect("list failed"); + assert_eq!(page.items.len() as u64, counted); + + let item_uuid = Uuid::parse_str(&first_item_id).unwrap(); + let placement = runtime + .block_on(client.get_annotation_queue_item_placement(queue_uuid, item_uuid)) + .expect("placement failed"); + assert!( + placement.section.is_some(), + "placement should name a section" + ); + assert_eq!(placement.item_type, Some(AnnotationQueueItemType::Run)); + + // ── UPDATE (SDK): mark one item reviewed ── + let reviewed_at = Utc::now(); + let patched = runtime + .block_on(client.update_annotation_queue_item( + queue_uuid, + item_uuid, + PatchAnnotationQueueItemRequest { + last_reviewed_time: Some(reviewed_at), + ..Default::default() + }, + )) + .expect("patch failed"); + assert_eq!(patched.id, item_uuid); + assert!(patched.last_reviewed_time.is_some()); + + // ── REMOVE (CLI): one run, then the same run again ── + let stdout = run_ok(&["queue", "remove-run", &queue_id, &a]); + assert!( + stdout.contains("Removed run"), + "remove-run output: {stdout}" + ); + let items = queue_items_json(&queue_id); + assert_eq!(run_ids(&items), vec![b.as_str()]); + + let output = langstar_cmd() + .args(["queue", "remove-run", &queue_id, &a]) + .output() + .expect("Failed to execute CLI"); + assert!( + !output.status.success(), + "removing a missing run should fail" + ); + assert!(String::from_utf8_lossy(&output.stderr).contains("is not in queue")); + + // ── DELETE ── + run_ok(&["queue", "delete", &queue_id, "--force"]); + let output = langstar_cmd() + .args(["queue", "get", &queue_id]) + .output() + .expect("Failed to execute CLI"); + assert!( + !output.status.success(), + "queue should be gone after delete" + ); +} diff --git a/docs/queues.md b/docs/queues.md index ef53fbb61..433045dfc 100644 --- a/docs/queues.md +++ b/docs/queues.md @@ -229,6 +229,8 @@ langstar queue add-runs 12345678-... run-id-1 run-id-2 run-id-3 langstar queue add-runs 12345678-... --runs-file runs.txt ``` +A run that LangSmith cannot find yet is rejected with a 404, so after ingesting a run, retry until it succeeds. + **runs.txt format:** ``` # Comments are ignored @@ -241,7 +243,7 @@ abcdef03-1234-1234-1234-123456789012 ### `langstar queue remove-run` -Remove a run from an annotation queue. +Remove a run from an annotation queue. The items API deletes by queue item ID, so langstar looks the run up in every review section first. It exits non-zero if the queue holds no item for the run. ```bash langstar queue remove-run @@ -256,7 +258,7 @@ langstar queue remove-run 12345678-... abcdef01-... ### `langstar queue items` -List runs currently in an annotation queue. +List the items currently in an annotation queue, one review section after another. ```bash langstar queue items [OPTIONS] @@ -266,7 +268,8 @@ langstar queue items [OPTIONS] | Option | Description | |--------|-------------| | `-l, --limit ` | Maximum items to return (default: 100) | -| `--json` | Output as JSON | +| `--status
` | One review section: `needs-my-review`, `needs-others-review` or `archived` (default: all three) | +| `--json` | Output as JSON; each item carries a `status` field naming its review section | **Example:** ```bash @@ -275,14 +278,15 @@ langstar queue items 12345678-1234-1234-1234-123456789012 --limit 50 Output: ``` -Index Run ID Name Status Added -0 abcdef01 ChatOpenAI success 2025-11-28 -1 abcdef02 RAGChain error 2025-11-28 -2 abcdef03 AgentExecutor success 2025-11-28 +Item ID Run ID Type Status Added +1a2b3c4d abcdef01 RUN needs_my_review 2026-10-13 +5e6f7a8b abcdef02 RUN needs_my_review 2026-10-13 -Found 3 items in queue +Found 2 items in queue ``` +The items API returns the run ID, not the run's name or status. Look a run up with `langstar runs query`. + --- ## CI/CD Integration @@ -520,11 +524,13 @@ async fn main() -> Result<(), Box> { client.add_runs_to_annotation_queue(queue.base.id, run_ids).await?; // List queue items - for index in 0..10 { - match client.get_run_from_annotation_queue(queue.base.id, index).await { - Ok(item) => println!("Item {}: {}", index, item.run.name), - Err(_) => break, // No more items - } + use langstar_sdk::{AnnotationQueueItemStatus, ListAnnotationQueueItemsParams}; + let params = ListAnnotationQueueItemsParams::new(AnnotationQueueItemStatus::NeedsMyReview); + let page = client + .list_annotation_queue_items(queue.base.id, params, None) + .await?; + for item in page.items { + println!("Item {}: run {:?}", item.id, item.run_id); } // Clean up @@ -562,8 +568,9 @@ langstar config ### Empty queue items -The `items` command fetches runs sequentially by index. If you see fewer items than expected: -- Some runs may have been reviewed and removed +If you see fewer items than expected: +- If you passed `--status`, items in the other two review sections are left out; drop `--status` to see all three +- Raise `--limit` (default 100) - The queue may be empty --- @@ -579,8 +586,10 @@ The annotation queue commands use the LangSmith REST API: | `queue get` | GET | `/api/v1/annotation-queues/{id}` | | `queue update` | PATCH | `/api/v1/annotation-queues/{id}` | | `queue delete` | DELETE | `/api/v1/annotation-queues/{id}` | -| `queue add-runs` | POST | `/api/v1/annotation-queues/{id}/runs` | -| `queue remove-run` | DELETE | `/api/v1/annotation-queues/{id}/runs/{run_id}` | -| `queue items` | GET | `/api/v1/annotation-queues/{id}/run/{index}` | +| `queue add-runs` | POST | `/api/v1/platform/annotation-queues/{id}/items` | +| `queue remove-run` | GET, then POST | `/api/v1/platform/annotation-queues/{id}/items`, then `.../items/delete` | +| `queue items` | GET | `/api/v1/platform/annotation-queues/{id}/items` | + +LangSmith deprecated the `/api/v1/annotation-queues/{id}/runs*` and `/run/{index}` endpoints. `add-runs` sends each run as a `RUN` item with only its `run_id`. For complete API documentation, see the [LangSmith OpenAPI spec](https://api.smith.langchain.com/redoc). diff --git a/openspec/specs/queue/spec.md b/openspec/specs/queue/spec.md index 64a83c2bc..d2a0ab0d2 100644 --- a/openspec/specs/queue/spec.md +++ b/openspec/specs/queue/spec.md @@ -87,18 +87,28 @@ The `langstar queue` subcommands SHALL print a table or a short summary by defau ### Requirement: Remove a run from a queue -`langstar queue remove-run ` SHALL remove one run from the queue and print the run and queue IDs. +`langstar queue remove-run ` SHALL remove one run from the queue and print the run and queue IDs. If the queue holds no item for the run, it SHALL exit non-zero and say the run is not in the queue. #### Scenario: Remove a run - **WHEN** a user runs `langstar queue remove-run ` - **THEN** the run leaves the queue -### Requirement: List the runs in a queue +#### Scenario: Remove a run that is not in the queue -`langstar queue items ` SHALL list the runs in the queue in order, up to `--limit` (default 100), stopping at the end of the queue. The table SHALL show each run's position in the queue, short run ID, name, status and the date it was added, followed by how many items it found. +- **WHEN** a user runs `langstar queue remove-run ` for a run the queue does not hold +- **THEN** langstar exits non-zero and says the run is not in the queue + +### Requirement: List the items in a queue + +`langstar queue items ` SHALL list the items in the queue, up to `--limit` (default 100), one review section after another, or only the section `--status` names (`needs-my-review`, `needs-others-review` or `archived`). The table and the JSON `status` field name each item's section with underscores: `needs_my_review`, `needs_others_review`, `archived`. The table SHALL show each item's short ID, short run ID, type, review section and the date it was added, followed by how many items it found. #### Scenario: Queue shorter than the limit -- **WHEN** a queue holds 5 runs and the user runs `langstar queue items ` -- **THEN** langstar lists the 5 runs and says it found 5 items +- **WHEN** a queue holds 5 items and the user runs `langstar queue items ` +- **THEN** langstar lists the 5 items and says it found 5 items + +#### Scenario: One review section + +- **WHEN** a queue holds 3 archived items and 2 items that need review, and the user runs `langstar queue items --status archived` +- **THEN** langstar lists only the 3 archived items and says it found 3 items diff --git a/reference/api-specs/LANGSMITH_APIS_DETAILS.md b/reference/api-specs/LANGSMITH_APIS_DETAILS.md index e1ebd1710..20f7cba3c 100644 --- a/reference/api-specs/LANGSMITH_APIS_DETAILS.md +++ b/reference/api-specs/LANGSMITH_APIS_DETAILS.md @@ -21,8 +21,8 @@ See [langsmith/FRAGMENTS.md](./langsmith/FRAGMENTS.md) for extraction commands. | Schema | File Path | Size | Purpose | |--------|-----------|------|---------| -| Annotation Queue Endpoints | `langsmith/annotation-queue-endpoints.json` | 2.3K | HITL annotation queue API endpoints | -| Annotation Queue Schemas | `langsmith/annotation-queue-schemas.json` | 45K | Data types for annotation queues | +| Annotation Queue Endpoints | `langsmith/annotation-queue-endpoints.json` | 64K | Annotation queue endpoints, including the platform items endpoints | +| Annotation Queue Schemas | `langsmith/annotation-queue-schemas.json` | 47K | Data types for annotation queues | | Run Schema | `langsmith/run-schema.json` | 9.8K | Run/trace data structures | | Runs Query Endpoint | `langsmith/runs-query-endpoint.json` | 1.0K | POST /runs/query endpoint spec | | Runs Query Request | `langsmith/runs-query-request-schema.json` | 5.5K | Request payload for runs query | diff --git a/reference/api-specs/langsmith/FRAGMENTS.md b/reference/api-specs/langsmith/FRAGMENTS.md index ab0108243..2435bd826 100644 --- a/reference/api-specs/langsmith/FRAGMENTS.md +++ b/reference/api-specs/langsmith/FRAGMENTS.md @@ -16,8 +16,8 @@ These fragments are extracted subsets of the full OpenAPI spec, optimized for: | File | Size | Purpose | jq Query | Last Updated | |------|------|---------|----------|--------------| -| `annotation-queue-endpoints.json` | 2.3K | Annotation queue API endpoints | See below | 2025-11-26 | -| `annotation-queue-schemas.json` | 45K | Annotation queue data types | See below | 2025-11-26 | +| `annotation-queue-endpoints.json` | 64K | Annotation queue API endpoints | See below | 2026-10-13 | +| `annotation-queue-schemas.json` | 47K | Annotation queue data types | See below | 2026-10-13 | | `run-schema.json` | 9.8K | Run/trace data structures | See below | 2025-11-26 | | `runs-query-endpoint.json` | 1.0K | POST /runs/query endpoint | See below | 2025-11-26 | | `runs-query-request-schema.json` | 5.5K | Runs query request payload | See below | 2025-11-26 | diff --git a/reference/api-specs/langsmith/annotation-queue-endpoints.json b/reference/api-specs/langsmith/annotation-queue-endpoints.json index c341d073b..ecf33de66 100644 --- a/reference/api-specs/langsmith/annotation-queue-endpoints.json +++ b/reference/api-specs/langsmith/annotation-queue-endpoints.json @@ -1,135 +1,2585 @@ -[ - { - "path": "/api/v1/annotation-queues", - "methods": [ - "get", - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/populate", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}", - "methods": [ - "delete", - "get", - "patch" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/runs", - "methods": [ - "get", - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/export", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/run/{index}", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/{run_id}/queues", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/runs/{queue_run_id}", - "methods": [ - "delete", - "patch" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/runs/delete", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/total_size", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/total_archived", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/{queue_id}/size", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/status/{annotation_queue_run_id}", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise", - "methods": [ - "get", - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/{queue_id}", - "methods": [ - "get", - "patch" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/populate", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/{queue_id}/populate", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/{queue_id}/entries", - "methods": [ - "get" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/{queue_id}/entries/delete", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/status/{queue_entry_id}", - "methods": [ - "post" - ] - }, - { - "path": "/api/v1/annotation-queues/pairwise/{queue_id}/size", - "methods": [ - "get" - ] +{ + "/api/v1/annotation-queues": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Annotation Queues", + "operationId": "get_annotation_queues_api_v1_annotation_queues_get", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + { + "type": "null" + } + ], + "title": "Ids" + } + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + }, + { + "name": "name_contains", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name Contains" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 100, + "title": "Limit" + } + }, + { + "name": "tag_value_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + { + "type": "null" + } + ], + "title": "Tag Value Id" + } + }, + { + "name": "dataset_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Dataset Id" + } + }, + { + "name": "queue_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "single", + "pairwise" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Queue Type" + } + }, + { + "name": "assigned_to_me", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Assigned To Me" + } + }, + { + "name": "sort_by", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort By" + } + }, + { + "name": "sort_by_desc", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true, + "title": "Sort By Desc" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnnotationQueueSchemaWithSize" + }, + "title": "Response Get Annotation Queues Api V1 Annotation Queues Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Create Annotation Queue", + "operationId": "create_annotation_queue_api_v1_annotation_queues_post", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueCreateSchema" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueSchema" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "delete": { + "tags": [ + "annotation-queues" + ], + "summary": "Delete Annotation Queues", + "description": "Delete multiple annotation queues with partial success support.\n\nReturns:\n - 200: All queues deleted successfully\n - 207: Some queues deleted successfully, some failed", + "operationId": "delete_annotation_queues_api_v1_annotation_queues_delete", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_ids", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "maxItems": 100, + "title": "Queue Ids" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/populate": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Populate Annotation Queue", + "description": "Populate annotation queue with runs from an experiment.", + "operationId": "populate_annotation_queue_api_v1_annotation_queues_populate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PopulateAnnotationQueueSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}": { + "delete": { + "tags": [ + "annotation-queues" + ], + "summary": "Delete Annotation Queue", + "operationId": "delete_annotation_queue_api_v1_annotation_queues__queue_id__delete", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "patch": { + "tags": [ + "annotation-queues" + ], + "summary": "Update Annotation Queue", + "operationId": "update_annotation_queue_api_v1_annotation_queues__queue_id__patch", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueUpdateSchema" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Annotation Queue", + "operationId": "get_annotation_queue_api_v1_annotation_queues__queue_id__get", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueSchemaWithRubric" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/runs": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Add Runs To Annotation Queue", + "operationId": "add_runs_to_annotation_queue_api_v1_annotation_queues__queue_id__runs_post", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "extend_trace_retention", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Extend Trace Retention" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddRunToQueueRequest" + } + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnnotationQueueRunAddSchema" + } + } + ], + "title": "Runs" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnnotationQueueRunSchema" + }, + "title": "Response Add Runs To Annotation Queue Api V1 Annotation Queues Queue Id Runs Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Runs From Annotation Queue", + "operationId": "get_runs_from_annotation_queue_api_v1_annotation_queues__queue_id__runs_get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 100, + "title": "Limit" + } + }, + { + "name": "archived", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Archived" + } + }, + { + "name": "include_stats", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Include Stats" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "needs_my_review", + "needs_others_review", + "completed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunSchemaWithAnnotationQueueInfo" + }, + "title": "Response Get Runs From Annotation Queue Api V1 Annotation Queues Queue Id Runs Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/runs/by-key": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Add Runs To Annotation Queue By Key", + "description": "Self-hosted deployments require LangSmith `v0.16` or later.", + "operationId": "add_runs_to_annotation_queue_by_key_api_v1_annotation_queues__queue_id__runs_by_key_post", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "extend_trace_retention", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Extend Trace Retention" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AddRunToQueueByKeyRequest" + }, + "title": "Runs" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnnotationQueueRunSchema" + }, + "title": "Response Add Runs To Annotation Queue By Key Api V1 Annotation Queues Queue Id Runs By Key Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/export": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Export Annotation Queue Archived Runs", + "operationId": "export_annotation_queue_archived_runs_api_v1_annotation_queues__queue_id__export_post", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportAnnotationQueueRunsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/run/{index}": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Run From Annotation Queue", + "description": "Get a run from an annotation queue", + "operationId": "get_run_from_annotation_queue_api_v1_annotation_queues__queue_id__run__index__get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "index", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Index" + } + }, + { + "name": "include_extra", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Extra" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunSchemaWithAnnotationQueueInfo" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{run_id}/queues": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Annotation Queues For Run", + "operationId": "get_annotation_queues_for_run_api_v1_annotation_queues__run_id__queues_get", + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnnotationQueueSchema" + }, + "title": "Response Get Annotation Queues For Run Api V1 Annotation Queues Run Id Queues Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/runs/{queue_run_id}": { + "patch": { + "tags": [ + "annotation-queues" + ], + "summary": "Update Run In Annotation Queue", + "operationId": "update_run_in_annotation_queue_api_v1_annotation_queues__queue_id__runs__queue_run_id__patch", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "queue_run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Run Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueRunUpdateSchema" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + }, + "delete": { + "tags": [ + "annotation-queues" + ], + "summary": "Delete Run From Annotation Queue", + "operationId": "delete_run_from_annotation_queue_api_v1_annotation_queues__queue_id__runs__queue_run_id__delete", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "queue_run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/runs/delete": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Delete Runs From Annotation Queue", + "operationId": "delete_runs_from_annotation_queue_api_v1_annotation_queues__queue_id__runs_delete_post", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueBulkDeleteRunsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/total_size": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Total Size From Annotation Queue", + "operationId": "get_total_size_from_annotation_queue_api_v1_annotation_queues__queue_id__total_size_get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueSizeSchema" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/total_archived": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Total Archived From Annotation Queue", + "operationId": "get_total_archived_from_annotation_queue_api_v1_annotation_queues__queue_id__total_archived_get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "start_time", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" + } + }, + { + "name": "end_time", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueSizeSchema" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/size": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Get Size From Annotation Queue", + "operationId": "get_size_from_annotation_queue_api_v1_annotation_queues__queue_id__size_get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "needs_my_review", + "needs_others_review", + "completed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnnotationQueueSizeSchema" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/status/{annotation_queue_run_id}": { + "post": { + "tags": [ + "annotation-queues" + ], + "summary": "Create Identity Annotation Queue Run Status", + "operationId": "create_identity_annotation_queue_run_status_api_v1_annotation_queues_status__annotation_queue_run_id__post", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "annotation_queue_run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Annotation Queue Run Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityAnnotationQueueRunStatusCreateSchema" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/annotation-queues/{queue_id}/runs/resolve/{queue_run_id}": { + "get": { + "tags": [ + "annotation-queues" + ], + "summary": "Resolve Annotation Queue Run", + "description": "Resolve a queue run ID to its section and run data for deep linking.", + "operationId": "resolve_annotation_queue_run_api_v1_annotation_queues__queue_id__runs_resolve__queue_run_id__get", + "deprecated": true, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Id" + } + }, + { + "name": "queue_run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Queue Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolvedAnnotationQueueRunSchema" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "x-public": true + } + }, + "/api/v1/platform/annotation-queues/items/{queue_item_id}/status": { + "post": { + "description": "Log the caller's reviewer status for a RUN or THREAD annotation queue item. A null status re-shows the item for this reviewer.", + "tags": [ + "annotation_queues" + ], + "summary": "Create annotation queue item status", + "parameters": [ + { + "description": "Annotation queue item ID", + "name": "queue_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.CreateAnnotationQueueItemStatusResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.CreateAnnotationQueueItemStatusRequest" + } + } + } + } + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/items": { + "get": { + "description": "List RUN and THREAD items in a single annotation queue for one review status section, with opaque cursor pagination. Optional item_type=RUN|THREAD filters the page. Optional min_start_time/max_start_time bound the item's trace start time; items with no start time are excluded when either bound is set. direction=backward returns items before the supplied cursor. The response contains item metadata only, not expanded run or thread payloads. status=archived returns items whose queue review requirements have been satisfied, not merely items the caller personally marked completed.", + "tags": [ + "annotation_queues" + ], + "summary": "List annotation queue items", + "parameters": [ + { + "description": "Annotation queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Review section: needs_my_review, needs_others_review, or archived", + "name": "status", + "in": "query", + "required": true, + "schema": { + "type": "string", + "enum": [ + "needs_my_review", + "needs_others_review", + "archived" + ], + "title": "Status" + } + }, + { + "description": "Page size (max 100)", + "name": "page_size", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "title": "Page Size" + } + }, + { + "description": "Opaque pagination cursor", + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "title": "Cursor" + } + }, + { + "description": "Filter to RUN or THREAD", + "name": "item_type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "RUN", + "THREAD" + ], + "title": "Item Type" + } + }, + { + "description": "Only items whose trace start time is at or after this timestamp. Omit or send the zero time for no bound", + "name": "min_start_time", + "in": "query", + "schema": { + "format": "date-time", + "type": "string", + "title": "Min Start Time" + } + }, + { + "description": "Only items whose trace start time is at or before this timestamp. Omit or send the zero time for no bound", + "name": "max_start_time", + "in": "query", + "schema": { + "format": "date-time", + "type": "string", + "title": "Max Start Time" + } + }, + { + "description": "Pagination direction. backward requires cursor", + "name": "direction", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "forward", + "backward" + ], + "default": "forward", + "title": "Direction" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.ListAnnotationQueueItemsResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true + }, + "post": { + "description": "Add RUN or THREAD items to a single annotation queue. RUN items require run_id unless they are created from a suggested example. THREAD items require thread_id and project_id.", + "tags": [ + "annotation_queues" + ], + "summary": "Add annotation queue items", + "parameters": [ + { + "description": "Annotation queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Extend trace retention for added run items", + "name": "extend_trace_retention", + "in": "query", + "schema": { + "type": "boolean", + "title": "Extend Trace Retention" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AddAnnotationQueueItemsResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AddAnnotationQueueItemsRequest" + } + } + } + } + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/items/count": { + "get": { + "description": "Returns the number of annotation queue items in one status bucket. The two time windows are independent: start_time/end_time bound when an item was archived, min_start_time/max_start_time bound when its trace ran. Items with no trace start time are excluded when either of the latter is set.", + "tags": [ + "annotation_queues" + ], + "summary": "Get the annotation queue item count", + "parameters": [ + { + "description": "Queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Count bucket: all, needs_my_review, needs_others_review, or archived.", + "name": "status", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Status" + } + }, + { + "description": "Archived strictly after this time. Only used when status=archived", + "name": "start_time", + "in": "query", + "schema": { + "type": "string", + "title": "Start Time" + } + }, + { + "description": "Archived strictly before this time. Only used when status=archived", + "name": "end_time", + "in": "query", + "schema": { + "type": "string", + "title": "End Time" + } + }, + { + "description": "Trace started at or after this time", + "name": "min_start_time", + "in": "query", + "schema": { + "format": "date-time", + "type": "string", + "title": "Min Start Time" + } + }, + { + "description": "Trace started at or before this time", + "name": "max_start_time", + "in": "query", + "schema": { + "format": "date-time", + "type": "string", + "title": "Max Start Time" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemCountResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/items/delete": { + "post": { + "description": "Remove RUN or THREAD items from a single annotation queue by item ID. Both active and completed items can be removed.", + "tags": [ + "annotation_queues" + ], + "summary": "Delete annotation queue items", + "parameters": [ + { + "description": "Annotation queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.DeleteAnnotationQueueItemsRequest" + } + } + } + } + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/items/{item_id}": { + "patch": { + "description": "Partially update mutable timestamps (added_at, last_reviewed_time) for a RUN or THREAD annotation queue item. Omit a field, or pass JSON null, to leave it unchanged.", + "tags": [ + "annotation_queues" + ], + "summary": "Update an annotation queue item", + "parameters": [ + { + "description": "Annotation queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Annotation queue item ID", + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItem" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.PatchAnnotationQueueItemRequest" + } + } + } + } + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/items/{item_id}/placement": { + "get": { + "description": "Resolve a RUN or THREAD item to its current review section and zero-based position for deep linking. The returned cursor counts RUN and THREAD items together, so it is only valid for a list request with no item_type or start-time filter.", + "tags": [ + "annotation_queues" + ], + "summary": "Get annotation queue item placement", + "parameters": [ + { + "description": "Annotation queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Annotation queue item ID", + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemPlacementResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/shared.ProblemDetails" + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/reviewers": { + "post": { + "description": "Assigns a single identity as a reviewer for the queue. Idempotent.", + "tags": [ + "annotation_queues" + ], + "summary": "Add a reviewer to an annotation queue", + "parameters": [ + { + "description": "Queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AddReviewerResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/annotationqueues.AddReviewerRequest" + } + } + } + } + } + }, + "/api/v1/platform/annotation-queues/{queue_id}/reviewers/{identity_id}": { + "delete": { + "description": "Unassigns an identity as a reviewer for the queue. Idempotent.", + "tags": [ + "annotation_queues" + ], + "summary": "Remove a reviewer from an annotation queue", + "parameters": [ + { + "description": "Queue ID", + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Identity ID of the reviewer to remove", + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "security": [ + { + "API Key": [] + }, + { + "Tenant ID": [] + }, + { + "Bearer Auth": [] + } + ], + "x-public": true + } } -] +} diff --git a/reference/api-specs/langsmith/annotation-queue-schemas.json b/reference/api-specs/langsmith/annotation-queue-schemas.json index 9473995fc..2a0d5007f 100644 --- a/reference/api-specs/langsmith/annotation-queue-schemas.json +++ b/reference/api-specs/langsmith/annotation-queue-schemas.json @@ -1,4 +1,70 @@ { + "AddRunToQueueByKeyRequest": { + "properties": { + "run_id": { + "type": "string", + "format": "uuid", + "title": "Run Id" + }, + "session_id": { + "type": "string", + "format": "uuid", + "title": "Session Id" + }, + "start_time": { + "type": "string", + "format": "date-time", + "title": "Start Time" + }, + "source_proposed_example_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Source Proposed Example Id" + } + }, + "type": "object", + "required": [ + "run_id", + "session_id", + "start_time" + ], + "title": "AddRunToQueueByKeyRequest", + "description": "Add run to AQ by SmithDB key. is_root derived server-side (LSAQ-141)." + }, + "AddRunToQueueRequest": { + "properties": { + "run_id": { + "type": "string", + "format": "uuid", + "title": "Run Id" + }, + "source_proposed_example_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Source Proposed Example Id" + } + }, + "type": "object", + "required": [ + "run_id" + ], + "title": "AddRunToQueueRequest", + "description": "Add a single run to AQ (CH path) with an optional back-pointer to the\nissues-agent proposal that seeded this add. Use when bulk-adding runs\nthat come from different proposals — each row carries its own\nsource_proposed_example_id. For unrelated bulk adds, prefer plain\nList[UUID] on the same endpoint." + }, "AnnotationQueueBulkDeleteRunsRequest": { "properties": { "delete_all": { @@ -99,6 +165,11 @@ "title": "Reservation Minutes", "default": 1 }, + "reviewer_access_mode": { + "type": "string", + "title": "Reviewer Access Mode", + "default": "any" + }, "name": { "type": "string", "title": "Name" @@ -163,6 +234,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -223,6 +295,62 @@ } ], "title": "Score Descriptions" + }, + "is_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Required" + }, + "is_assertion": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Assertion" + }, + "regex_validator": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Missing" + } + ], + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Regex Validator", + "default": { + "__missing__": "__missing__" + } + }, + "feedback_config_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Feedback Config Id" } }, "type": "object", @@ -231,6 +359,85 @@ ], "title": "AnnotationQueueRubricItemSchema" }, + "AnnotationQueueRunAddSchema": { + "properties": { + "run_id": { + "type": "string", + "format": "uuid", + "title": "Run Id", + "deprecated": true + }, + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time", + "deprecated": true + }, + "session_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "deprecated": true + }, + "trace_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Trace Id", + "deprecated": true + }, + "parent_run_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Parent Run Id", + "deprecated": true + }, + "trace_tier": { + "anyOf": [ + { + "$ref": "#/components/schemas/TraceTier" + }, + { + "type": "null" + } + ], + "deprecated": true + } + }, + "type": "object", + "required": [ + "run_id" + ], + "title": "AnnotationQueueRunAddSchema", + "description": "Deprecated: use plain UUID list or AddRunToQueueByKeyRequest instead." + }, "AnnotationQueueRunSchema": { "properties": { "run_id": { @@ -260,6 +467,18 @@ "format": "date-time", "title": "Added At" }, + "source_proposed_example_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Source Proposed Example Id" + }, "id": { "type": "string", "format": "uuid", @@ -363,6 +582,11 @@ "title": "Reservation Minutes", "default": 1 }, + "reviewer_access_mode": { + "type": "string", + "title": "Reviewer Access Mode", + "default": "any" + }, "name": { "type": "string", "title": "Name" @@ -424,6 +648,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -431,6 +656,14 @@ } ], "title": "Metadata" + }, + "assigned_reviewers": { + "items": { + "$ref": "#/components/schemas/AssignedReviewerSchema" + }, + "type": "array", + "title": "Assigned Reviewers", + "default": [] } }, "type": "object", @@ -502,6 +735,11 @@ "title": "Reservation Minutes", "default": 1 }, + "reviewer_access_mode": { + "type": "string", + "title": "Reviewer Access Mode", + "default": "any" + }, "name": { "type": "string", "title": "Name" @@ -563,6 +801,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -571,6 +810,14 @@ ], "title": "Metadata" }, + "assigned_reviewers": { + "items": { + "$ref": "#/components/schemas/AssignedReviewerSchema" + }, + "type": "array", + "title": "Assigned Reviewers", + "default": [] + }, "rubric_items": { "anyOf": [ { @@ -666,6 +913,11 @@ "title": "Reservation Minutes", "default": 1 }, + "reviewer_access_mode": { + "type": "string", + "title": "Reviewer Access Mode", + "default": "any" + }, "name": { "type": "string", "title": "Name" @@ -727,6 +979,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -735,6 +988,14 @@ ], "title": "Metadata" }, + "assigned_reviewers": { + "items": { + "$ref": "#/components/schemas/AssignedReviewerSchema" + }, + "type": "array", + "title": "Assigned Reviewers", + "default": [] + }, "total_runs": { "type": "integer", "title": "Total Runs" @@ -806,6 +1067,9 @@ { "type": "integer" }, + { + "$ref": "#/components/schemas/Missing" + }, { "type": "null" } @@ -857,6 +1121,7 @@ "metadata": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -870,65 +1135,66 @@ "default": { "__missing__": "__missing__" } + }, + "reviewer_access_mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "any", + "assigned" + ] + }, + { + "type": "null" + } + ], + "title": "Reviewer Access Mode" } }, "type": "object", "title": "AnnotationQueueUpdateSchema", "description": "AnnotationQueue update schema." }, - "ComparativeAnnotationQueueBulkDeleteEntriesRequest": { + "ExportAnnotationQueueRunsRequest": { "properties": { - "delete_all": { - "type": "boolean", - "title": "Delete All", - "default": false - }, - "example_ids": { + "start_time": { "anyOf": [ { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Example Ids" + "title": "Start Time" }, - "exclude_example_ids": { + "end_time": { "anyOf": [ { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Exclude Example Ids" + "title": "End Time" }, - "comparative_experiment_id": { - "type": "string", - "format": "uuid", - "title": "Comparative Experiment Id" + "include_annotator_detail": { + "type": "boolean", + "title": "Include Annotator Detail", + "default": false } }, "type": "object", - "required": [ - "comparative_experiment_id" - ], - "title": "ComparativeAnnotationQueueBulkDeleteEntriesRequest", - "description": "Request to bulk delete runs from comparative annotation queue." + "title": "ExportAnnotationQueueRunsRequest", + "description": "Export annotation queue runs request schema." }, - "ComparativeAnnotationQueueCreateRequest": { + "IdentityAnnotationQueueRunStatusCreateSchema": { "properties": { - "description": { + "status": { "anyOf": [ { "type": "string" @@ -937,606 +1203,13 @@ "type": "null" } ], - "title": "Description" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" + "title": "Status" }, - "num_reviewers_per_item": { + "override_added_at": { "anyOf": [ { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Num Reviewers Per Item", - "default": 1 - }, - "enable_reservations": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Enable Reservations", - "default": true - }, - "reservation_minutes": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Reservation Minutes", - "default": 1 - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "rubric_items": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/AnnotationQueueRubricItemSchema" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Rubric Items" - }, - "rubric_instructions": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Rubric Instructions" - }, - "experiment_ids": { - "anyOf": [ - { - "prefixItems": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "string", - "format": "uuid" - } - ], - "type": "array", - "maxItems": 2, - "minItems": 2 - }, - { - "type": "null" - } - ], - "title": "Experiment Ids" - }, - "pairwise_experiment_ids": { - "anyOf": [ - { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array", - "maxItems": 1 - }, - { - "type": "null" - } - ], - "title": "Pairwise Experiment Ids" - }, - "metadata": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Metadata" - } - }, - "type": "object", - "title": "ComparativeAnnotationQueueCreateRequest", - "description": "Create comparative annotation queue request." - }, - "ComparativeAnnotationQueueEntry": { - "properties": { - "queue_entry_id": { - "type": "string", - "format": "uuid", - "title": "Queue Entry Id" - }, - "example_id": { - "type": "string", - "format": "uuid", - "title": "Example Id" - }, - "comparative_experiment_id": { - "type": "string", - "format": "uuid", - "title": "Comparative Experiment Id" - }, - "last_reviewed_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Last Reviewed Time" - }, - "added_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Added At" - }, - "effective_added_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Effective Added At" - }, - "runs": { - "items": { - "$ref": "#/components/schemas/RunSchemaComparisonView" - }, - "type": "array", - "title": "Runs" - } - }, - "type": "object", - "required": [ - "queue_entry_id", - "example_id", - "comparative_experiment_id", - "runs" - ], - "title": "ComparativeAnnotationQueueEntry", - "description": "One comparison task in a pairwise annotation queue." - }, - "ComparativeAnnotationQueuePopulateBody": { - "properties": { - "experiment_ids": { - "anyOf": [ - { - "prefixItems": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "string", - "format": "uuid" - } - ], - "type": "array", - "maxItems": 2, - "minItems": 2 - }, - { - "type": "null" - } - ], - "title": "Experiment Ids" - }, - "pairwise_experiment_ids": { - "anyOf": [ - { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array", - "maxItems": 10 - }, - { - "type": "null" - } - ], - "title": "Pairwise Experiment Ids" - } - }, - "type": "object", - "title": "ComparativeAnnotationQueuePopulateBody" - }, - "ComparativeAnnotationQueuePopulateRequest": { - "properties": { - "queue_id": { - "type": "string", - "format": "uuid", - "title": "Queue Id" - }, - "experiment_ids": { - "anyOf": [ - { - "prefixItems": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "string", - "format": "uuid" - } - ], - "type": "array", - "maxItems": 2, - "minItems": 2 - }, - { - "type": "null" - } - ], - "title": "Experiment Ids" - }, - "pairwise_experiment_ids": { - "anyOf": [ - { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array", - "maxItems": 1 - }, - { - "type": "null" - } - ], - "title": "Pairwise Experiment Ids" - } - }, - "type": "object", - "required": [ - "queue_id" - ], - "title": "ComparativeAnnotationQueuePopulateRequest" - }, - "ComparativeAnnotationQueueSchema": { - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" - }, - "num_reviewers_per_item": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Num Reviewers Per Item", - "default": 1 - }, - "enable_reservations": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Enable Reservations", - "default": true - }, - "reservation_minutes": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Reservation Minutes", - "default": 1 - }, - "name": { - "type": "string", - "title": "Name" - }, - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "tenant_id": { - "type": "string", - "format": "uuid", - "title": "Tenant Id" - }, - "source_rule_id": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "Source Rule Id" - }, - "run_rule_id": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "Run Rule Id" - }, - "default_dataset": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "Default Dataset" - }, - "queue_type": { - "type": "string", - "enum": [ - "single", - "pairwise" - ], - "title": "Queue Type" - }, - "metadata": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Metadata" - } - }, - "type": "object", - "required": [ - "name", - "id", - "tenant_id", - "queue_type" - ], - "title": "ComparativeAnnotationQueueSchema", - "description": "Comparative annotation queue schema." - }, - "ComparativeAnnotationQueueUpdateSchema": { - "properties": { - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "num_reviewers_per_item": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Num Reviewers Per Item" - }, - "enable_reservations": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Enable Reservations" - }, - "reservation_minutes": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Reservation Minutes" - }, - "rubric_items": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/AnnotationQueueRubricItemSchema" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Rubric Items" - }, - "rubric_instructions": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Rubric Instructions" - }, - "metadata": { - "anyOf": [ - { - "type": "object" - }, - { - "$ref": "#/components/schemas/Missing" - }, - { - "type": "null" - } - ], - "title": "Metadata", - "default": { - "__missing__": "__missing__" - } - } - }, - "type": "object", - "title": "ComparativeAnnotationQueueUpdateSchema", - "description": "Update schema for comparative annotation queue." - }, - "ExportAnnotationQueueRunsRequest": { - "properties": { - "start_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Start Time" - }, - "end_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "End Time" - } - }, - "type": "object", - "title": "ExportAnnotationQueueRunsRequest", - "description": "Export annotation queue runs request schema." - }, - "IdentityAnnotationQueueRunStatusCreateSchema": { - "properties": { - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" - }, - "override_added_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" + "type": "string", + "format": "date-time" }, { "type": "null" @@ -1549,36 +1222,6 @@ "title": "IdentityAnnotationQueueRunStatusCreateSchema", "description": "Identity annotation queue run status create schema." }, - "IdentityComparativeAnnotationQueueEntryStatusCreateSchema": { - "properties": { - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" - }, - "override_added_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Override Added At" - } - }, - "type": "object", - "title": "IdentityComparativeAnnotationQueueEntryStatusCreateSchema", - "description": "Identity comparative annotation queue run status create schema." - }, "PopulateAnnotationQueueSchema": { "properties": { "queue_id": { @@ -1593,6 +1236,11 @@ }, "type": "array", "title": "Session Ids" + }, + "extend_trace_retention": { + "type": "boolean", + "title": "Extend Trace Retention", + "default": false } }, "type": "object", @@ -1602,6 +1250,54 @@ ], "title": "PopulateAnnotationQueueSchema" }, + "QueueInfoResponse": { + "properties": { + "queued": { + "type": "integer", + "title": "Queued" + }, + "active": { + "type": "integer", + "title": "Active" + }, + "scheduled": { + "type": "integer", + "title": "Scheduled" + } + }, + "type": "object", + "required": [ + "queued", + "active", + "scheduled" + ], + "title": "QueueInfoResponse", + "description": "Short summary of queue counts." + }, + "ResolvedAnnotationQueueRunSchema": { + "properties": { + "section": { + "type": "string", + "enum": [ + "needs_my_review", + "needs_others_review", + "completed" + ], + "title": "Section" + }, + "position": { + "type": "integer", + "title": "Position" + } + }, + "type": "object", + "required": [ + "section", + "position" + ], + "title": "ResolvedAnnotationQueueRunSchema", + "description": "Resolved annotation queue run position for deep linking." + }, "RunSchemaWithAnnotationQueueInfo": { "properties": { "name": { @@ -1611,6 +1307,7 @@ "inputs": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1653,6 +1350,7 @@ "extra": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1674,13 +1372,14 @@ }, "execution_order": { "type": "integer", - "minimum": 1, + "minimum": 1.0, "title": "Execution Order", "default": 1 }, "serialized": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1692,6 +1391,7 @@ "outputs": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1751,6 +1451,7 @@ "anyOf": [ { "items": { + "additionalProperties": true, "type": "object" }, "type": "array" @@ -1778,6 +1479,7 @@ "inputs_s3_urls": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1789,6 +1491,7 @@ "outputs_s3_urls": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1800,6 +1503,7 @@ "s3_urls": { "anyOf": [ { + "additionalProperties": true, "type": "object" }, { @@ -1899,6 +1603,7 @@ "anyOf": [ { "additionalProperties": { + "additionalProperties": true, "type": "object" }, "type": "object" @@ -1967,7 +1672,8 @@ "total_cost": { "anyOf": [ { - "type": "string" + "type": "string", + "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$" }, { "type": "null" @@ -1978,7 +1684,8 @@ "prompt_cost": { "anyOf": [ { - "type": "string" + "type": "string", + "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$" }, { "type": "null" @@ -1989,7 +1696,8 @@ "completion_cost": { "anyOf": [ { - "type": "string" + "type": "string", + "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$" }, { "type": "null" @@ -2001,7 +1709,8 @@ "anyOf": [ { "additionalProperties": { - "type": "string" + "type": "string", + "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$" }, "type": "object" }, @@ -2015,7 +1724,8 @@ "anyOf": [ { "additionalProperties": { - "type": "string" + "type": "string", + "pattern": "^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$" }, "type": "object" }, @@ -2049,6 +1759,21 @@ ], "title": "First Token Time" }, + "messages": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages" + }, "session_id": { "type": "string", "format": "uuid", @@ -2194,6 +1919,36 @@ } ], "title": "Effective Added At" + }, + "reserved_by": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Reserved By", + "default": [] + }, + "completed_by": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Completed By", + "default": [] + }, + "source_proposed_example_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Source Proposed Example Id" } }, "type": "object", @@ -2210,5 +1965,293 @@ ], "title": "RunSchemaWithAnnotationQueueInfo", "description": "Run schema with annotation queue info." + }, + "annotationqueues.AddAnnotationQueueItemsRequest": { + "type": "object", + "properties": { + "items": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemInput" + } + } + } + }, + "annotationqueues.AddAnnotationQueueItemsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItem" + } + } + } + }, + "annotationqueues.AddReviewerRequest": { + "type": "object", + "required": [ + "identity_id" + ], + "properties": { + "identity_id": { + "type": "string" + } + } + }, + "annotationqueues.AddReviewerResponse": { + "type": "object", + "properties": { + "identity_id": { + "type": "string" + } + } + }, + "annotationqueues.AnnotationQueueItem": { + "type": "object", + "properties": { + "added_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "item_type": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemType" + }, + "last_reviewed_time": { + "description": "LastReviewedTime is always present on the wire (null until reviewed).", + "type": "string" + }, + "project_id": { + "type": "string" + }, + "queue_id": { + "type": "string" + }, + "run_id": { + "type": "string" + }, + "source_proposed_example_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "thread_id": { + "type": "string" + } + } + }, + "annotationqueues.AnnotationQueueItemCountResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + } + } + }, + "annotationqueues.AnnotationQueueItemInput": { + "type": "object", + "properties": { + "item_type": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemType" + }, + "project_id": { + "type": "string" + }, + "run_id": { + "description": "RUN fields", + "type": "string" + }, + "session_id": { + "description": "SessionID is an alias for project_id.", + "type": "string" + }, + "source_proposed_example_id": { + "description": "SourceProposedExampleID links the queue item to the suggested example\nit was created from, when applicable.", + "type": "string" + }, + "start_time": { + "type": "string", + "format": "date-time" + }, + "thread_id": { + "type": "string" + } + } + }, + "annotationqueues.AnnotationQueueItemListStatus": { + "type": "string", + "enum": [ + "needs_my_review", + "needs_others_review", + "archived" + ], + "x-enum-varnames": [ + "AnnotationQueueItemStatusNeedsMyReview", + "AnnotationQueueItemStatusNeedsOthersReview", + "AnnotationQueueItemStatusArchived" + ] + }, + "annotationqueues.AnnotationQueueItemPlacementResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "item_type": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemType" + }, + "position": { + "type": "integer" + }, + "section": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemListStatus" + } + } + }, + "annotationqueues.AnnotationQueueItemType": { + "type": "string", + "enum": [ + "RUN", + "THREAD" + ], + "x-enum-varnames": [ + "AnnotationQueueItemTypeRun", + "AnnotationQueueItemTypeThread" + ] + }, + "annotationqueues.AnnotationQueueListItem": { + "type": "object", + "properties": { + "added_at": { + "type": "string" + }, + "completed_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "effective_added_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "item_type": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueItemType" + }, + "last_reviewed_time": { + "description": "LastReviewedTime is always present on the wire (null until reviewed).", + "type": "string" + }, + "project_id": { + "type": "string" + }, + "queue_id": { + "type": "string" + }, + "reserved_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "run_id": { + "type": "string" + }, + "source_proposed_example_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "thread_id": { + "type": "string" + } + } + }, + "annotationqueues.AnnotationQueueReviewStatus": { + "type": "string", + "enum": [ + "viewed", + "completed" + ], + "x-enum-varnames": [ + "AnnotationQueueReviewStatusViewed", + "AnnotationQueueReviewStatusCompleted" + ] + }, + "annotationqueues.CreateAnnotationQueueItemStatusRequest": { + "type": "object", + "properties": { + "override_added_at": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueReviewStatus" + } + } + }, + "annotationqueues.CreateAnnotationQueueItemStatusResponse": { + "type": "object", + "properties": { + "is_archived": { + "type": "boolean" + }, + "override_added_at": { + "type": "string" + }, + "queue_item_id": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueReviewStatus" + } + } + }, + "annotationqueues.DeleteAnnotationQueueItemsRequest": { + "type": "object", + "properties": { + "item_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "annotationqueues.ListAnnotationQueueItemsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/annotationqueues.AnnotationQueueListItem" + } + }, + "next_cursor": { + "type": "string" + }, + "previous_cursor": { + "type": "string" + } + } + }, + "annotationqueues.PatchAnnotationQueueItemRequest": { + "type": "object", + "properties": { + "added_at": { + "type": "string", + "format": "date-time" + }, + "last_reviewed_time": { + "type": "string", + "format": "date-time" + } + } } } diff --git a/sdk/src/annotation_queues.rs b/sdk/src/annotation_queues.rs index 7768fa259..d0f0402e8 100644 --- a/sdk/src/annotation_queues.rs +++ b/sdk/src/annotation_queues.rs @@ -5,7 +5,8 @@ //! //! # API Reference //! -//! - Base endpoint: `/api/v1/annotation-queues` +//! - Queue endpoints: `/api/v1/annotation-queues` +//! - Item endpoints: `/api/v1/platform/annotation-queues/{queue_id}/items` //! - OpenAPI spec: //! //! # Example @@ -51,7 +52,6 @@ pub enum QueueType { /// /// Maps to `AnnotationQueueRubricItemSchema` in OpenAPI spec. #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct AnnotationQueueRubricItem { /// Feedback key identifier pub feedback_key: String, @@ -78,7 +78,6 @@ pub struct AnnotationQueueRubricItem { /// Maps to `AnnotationQueueSchema` in OpenAPI spec. /// Returned by `GET /annotation-queues` and `GET /annotation-queues/{id}` #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct AnnotationQueue { /// Unique identifier for the queue pub id: Uuid, @@ -142,7 +141,6 @@ pub struct AnnotationQueue { /// Maps to `AnnotationQueueSchemaWithRubric` in OpenAPI spec. /// Returned by `GET /annotation-queues/{id}` and `POST /annotation-queues` #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct AnnotationQueueWithDetails { /// Base queue fields #[serde(flatten)] @@ -178,7 +176,6 @@ pub struct AnnotationQueueWithDetails { /// }; /// ``` #[derive(Debug, Clone, Serialize, Default)] -#[serde(rename_all = "camelCase")] pub struct CreateAnnotationQueueRequest { /// Name of the queue (required) pub name: String, @@ -255,7 +252,6 @@ pub struct CreateAnnotationQueueRequest { /// /// All fields are optional for partial updates. #[derive(Debug, Clone, Serialize, Default)] -#[serde(rename_all = "camelCase")] pub struct UpdateAnnotationQueueRequest { /// New name #[serde(skip_serializing_if = "Option::is_none")] @@ -300,7 +296,6 @@ pub struct UpdateAnnotationQueueRequest { /// /// Query parameters for `GET /annotation-queues` #[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] pub struct ListAnnotationQueuesParams { /// Filter by specific queue IDs #[serde(skip_serializing_if = "Option::is_none")] @@ -319,39 +314,310 @@ pub struct ListAnnotationQueuesParams { pub limit: Option, } -/// Run with annotation queue metadata. +/// Kind of thing an annotation queue item points at. /// -/// Extends the base Run with queue-specific fields like when it was added -/// and when it was last reviewed. -/// -/// # API Reference +/// Maps to `annotationqueues.AnnotationQueueItemType` in the OpenAPI spec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum AnnotationQueueItemType { + /// A single run (trace) + Run, + /// A conversation thread + Thread, +} + +impl AnnotationQueueItemType { + /// The wire value LangSmith expects in the `item_type` query parameter. + pub fn as_str(&self) -> &'static str { + match self { + Self::Run => "RUN", + Self::Thread => "THREAD", + } + } +} + +/// Review section of an annotation queue, as the list and count endpoints take it. /// -/// Maps to `RunSchemaWithAnnotationQueueInfo` in OpenAPI spec. -/// Returned by `GET /annotation-queues/{queue_id}/run/{index}` +/// Maps to `annotationqueues.AnnotationQueueItemListStatus` in the OpenAPI spec. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AnnotationQueueItemStatus { + /// Waiting for the calling reviewer + NeedsMyReview, + /// Waiting for other reviewers + NeedsOthersReview, + /// Fully reviewed + Archived, +} + +impl AnnotationQueueItemStatus { + /// Every section, in the order a queue shows them. + pub const ALL: [Self; 3] = [Self::NeedsMyReview, Self::NeedsOthersReview, Self::Archived]; + + /// The wire value LangSmith expects in the `status` query parameter. + pub fn as_str(&self) -> &'static str { + match self { + Self::NeedsMyReview => "needs_my_review", + Self::NeedsOthersReview => "needs_others_review", + Self::Archived => "archived", + } + } +} + +/// Bucket to count with `GET .../items/count`: one review section, or every item. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnnotationQueueItemCountStatus { + /// Items in one review section + Section(AnnotationQueueItemStatus), + /// Every item in the queue + All, +} + +impl AnnotationQueueItemCountStatus { + /// The wire value LangSmith expects in the `status` query parameter. + pub fn as_str(&self) -> &'static str { + match self { + Self::Section(status) => status.as_str(), + Self::All => "all", + } + } +} + +impl From for AnnotationQueueItemCountStatus { + fn from(status: AnnotationQueueItemStatus) -> Self { + Self::Section(status) + } +} + +/// One item to add to an annotation queue. /// -/// # Note +/// Maps to `annotationqueues.AnnotationQueueItemInput`. A `RUN` item needs `run_id`, unless +/// `source_proposed_example_id` creates it from a suggested example; `project_id` and +/// `start_time` locate the run directly, as langsmith-sdk's `RunKey` does. +#[derive(Debug, Clone, Default, Serialize)] +pub struct AnnotationQueueItemInput { + /// What the item points at + #[serde(skip_serializing_if = "Option::is_none")] + pub item_type: Option, + + /// Run to review (for `RUN` items) + #[serde(skip_serializing_if = "Option::is_none")] + pub run_id: Option, + + /// Tracing project the run belongs to + #[serde(skip_serializing_if = "Option::is_none")] + pub project_id: Option, + + /// When the run started + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option>, + + /// Thread to review (for `THREAD` items) + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + + /// Suggested example the item was created from + #[serde(skip_serializing_if = "Option::is_none")] + pub source_proposed_example_id: Option, +} + +/// An item as the add and patch endpoints return it. /// -/// Uses snake_case field names to match existing Run struct pattern. +/// Maps to `annotationqueues.AnnotationQueueItem`. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RunWithAnnotationQueueInfo { - /// Base run fields - #[serde(flatten)] - pub run: super::runs::Run, +pub struct AnnotationQueueItem { + /// Queue item ID (not the run ID) + pub id: Uuid, + + /// Queue the item belongs to + #[serde(default)] + pub queue_id: Option, + + /// What the item points at + #[serde(default)] + pub item_type: Option, - /// Queue run ID (junction table ID) - pub queue_run_id: Uuid, + /// Run under review, for `RUN` items + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub run_id: Option, - /// When the run was last reviewed + /// Tracing project of the run + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub project_id: Option, + + /// Thread under review, for `THREAD` items + #[serde(default)] + pub thread_id: Option, + + /// Suggested example the item was created from + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub source_proposed_example_id: Option, + + /// When the run started + #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] + pub start_time: Option>, + + /// When the item was added to the queue + #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] + pub added_at: Option>, + + /// When the item was last reviewed; null until reviewed #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] pub last_reviewed_time: Option>, +} + +/// An item as the list endpoint returns it, with reviewer state. +/// +/// Maps to `annotationqueues.AnnotationQueueListItem`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnnotationQueueListItem { + /// Queue item ID (not the run ID) + pub id: Uuid, + + /// Queue the item belongs to + #[serde(default)] + pub queue_id: Option, + + /// What the item points at + #[serde(default)] + pub item_type: Option, + + /// Run under review, for `RUN` items + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub run_id: Option, + + /// Tracing project of the run + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub project_id: Option, + + /// Thread under review, for `THREAD` items + #[serde(default)] + pub thread_id: Option, + + /// Suggested example the item was created from + #[serde(default, deserialize_with = "deserialize_nonempty_uuid_opt")] + pub source_proposed_example_id: Option, - /// When the run was added to the queue + /// When the run started + #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] + pub start_time: Option>, + + /// When the item was added to the queue #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] pub added_at: Option>, - /// Effective added time (for sorting/display) + /// Added time used for ordering #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] pub effective_added_at: Option>, + + /// When the item was last reviewed; null until reviewed + #[serde(default, deserialize_with = "deserialize_flexible_datetime_opt")] + pub last_reviewed_time: Option>, + + /// Identities currently reviewing the item + #[serde(default)] + pub reserved_by: Option>, + + /// Identities that finished reviewing the item + #[serde(default)] + pub completed_by: Option>, +} + +/// One page of queue items. +/// +/// Maps to `annotationqueues.ListAnnotationQueueItemsResponse`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ListAnnotationQueueItemsResponse { + /// Items on this page + #[serde(default)] + pub items: Vec, + + /// Cursor for the next page, if there is one + #[serde(default)] + pub next_cursor: Option, + + /// Cursor for the previous page, if there is one + #[serde(default)] + pub previous_cursor: Option, +} + +/// Query parameters for `GET /api/v1/platform/annotation-queues/{queue_id}/items`, which +/// lists the items of one review section of an annotation queue. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ListAnnotationQueueItemsParams { + /// Review section to list; the API requires one + pub status: AnnotationQueueItemStatus, + + /// Items per page (the API defaults to 20) + pub page_size: Option, + + /// Only items of this type + pub item_type: Option, +} + +impl ListAnnotationQueueItemsParams { + /// Params for one review section, with the API's defaults for everything else. + pub fn new(status: AnnotationQueueItemStatus) -> Self { + Self { + status, + page_size: None, + item_type: None, + } + } +} + +/// Body for `PATCH /api/v1/platform/annotation-queues/{queue_id}/items/{item_id}`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct PatchAnnotationQueueItemRequest { + /// Move the item to this added time + #[serde(skip_serializing_if = "Option::is_none")] + pub added_at: Option>, + + /// Set the last reviewed time + #[serde(skip_serializing_if = "Option::is_none")] + pub last_reviewed_time: Option>, +} + +/// Where an item sits in its queue. +/// +/// Maps to `annotationqueues.AnnotationQueueItemPlacementResponse`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnnotationQueueItemPlacement { + /// Section the item is in + #[serde(default)] + pub section: Option, + + /// Zero-based position within the section + #[serde(default)] + pub position: Option, + + /// Cursor that lists the page holding the item + #[serde(default)] + pub cursor: Option, + + /// What the item points at + #[serde(default)] + pub item_type: Option, +} + +#[derive(Deserialize)] +pub(crate) struct AnnotationQueueItemCountResponse { + #[serde(default)] + pub(crate) count: u64, +} + +/// The items endpoints send `""` for an id that does not apply to the item type. +fn deserialize_nonempty_uuid_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value: Option = Option::deserialize(deserializer)?; + match value.as_deref() { + None | Some("") => Ok(None), + Some(s) => Uuid::parse_str(s) + .map(Some) + .map_err(serde::de::Error::custom), + } } #[cfg(test)] @@ -383,14 +649,14 @@ mod tests { let json = r#"{ "id": "12345678-1234-1234-1234-123456789012", "name": "Review Queue", - "tenantId": "87654321-4321-4321-4321-210987654321", - "queueType": "single", + "tenant_id": "87654321-4321-4321-4321-210987654321", + "queue_type": "single", "description": "Queue for reviews", - "createdAt": "2024-01-01T12:00:00Z", - "updatedAt": "2024-01-01T12:00:00Z", - "numReviewersPerItem": 2, - "enableReservations": true, - "reservationMinutes": 5 + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + "num_reviewers_per_item": 2, + "enable_reservations": true, + "reservation_minutes": 5 }"#; let queue: AnnotationQueue = serde_json::from_str(json).unwrap(); @@ -410,8 +676,8 @@ mod tests { let json = r#"{ "id": "12345678-1234-1234-1234-123456789012", "name": "Minimal Queue", - "tenantId": "87654321-4321-4321-4321-210987654321", - "queueType": "single" + "tenant_id": "87654321-4321-4321-4321-210987654321", + "queue_type": "single" }"#; let queue: AnnotationQueue = serde_json::from_str(json).unwrap(); @@ -426,14 +692,14 @@ mod tests { let json = r#"{ "id": "12345678-1234-1234-1234-123456789012", "name": "Review Queue", - "tenantId": "87654321-4321-4321-4321-210987654321", - "queueType": "pairwise", - "createdAt": "2024-01-01T12:00:00Z", - "updatedAt": "2024-01-01T12:00:00Z", - "rubricInstructions": "Rate on accuracy and helpfulness", - "rubricItems": [ + "tenant_id": "87654321-4321-4321-4321-210987654321", + "queue_type": "pairwise", + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + "rubric_instructions": "Rate on accuracy and helpfulness", + "rubric_items": [ { - "feedbackKey": "accuracy", + "feedback_key": "accuracy", "description": "How accurate is the response?" } ] @@ -463,8 +729,8 @@ mod tests { let json = serde_json::to_string(&request).unwrap(); assert!(json.contains("\"name\":\"Test Queue\"")); assert!(json.contains("\"description\":\"Test description\"")); - assert!(json.contains("\"queueType\":\"single\"")); - assert!(json.contains("\"rubricInstructions\":\"Test rubric\"")); + assert!(json.contains("\"queue_type\":\"single\"")); + assert!(json.contains("\"rubric_instructions\":\"Test rubric\"")); } #[test] @@ -502,7 +768,7 @@ mod tests { }; let json = serde_json::to_string(¶ms).unwrap(); - assert!(json.contains("\"nameContains\":\"review\"")); + assert!(json.contains("\"name_contains\":\"review\"")); assert!(json.contains("\"limit\":50")); } @@ -516,31 +782,51 @@ mod tests { }; let json = serde_json::to_string(&item).unwrap(); - assert!(json.contains("\"feedbackKey\":\"accuracy\"")); + assert!(json.contains("\"feedback_key\":\"accuracy\"")); assert!(json.contains("\"description\":\"Rate accuracy\"")); } #[test] - fn test_run_with_annotation_queue_info_deserialization() { + fn test_queue_item_deserialization_blank_ids() { let json = r#"{ "id": "123e4567-e89b-12d3-a456-426614174000", - "name": "ChatOpenAI", - "run_type": "llm", - "trace_id": "223e4567-e89b-12d3-a456-426614174001", - "dotted_order": "20240101T000000000000Z123e4567-e89b-12d3-a456-426614174000", - "status": "success", - "session_id": "323e4567-e89b-12d3-a456-426614174002", - "app_path": "/chat", - "queue_run_id": "423e4567-e89b-12d3-a456-426614174003", - "last_reviewed_time": "2024-01-01T13:00:00Z", + "queue_id": "223e4567-e89b-12d3-a456-426614174001", + "item_type": "THREAD", + "run_id": "", + "thread_id": "thread-1", "added_at": "2024-01-01T12:00:00Z", - "effective_added_at": "2024-01-01T12:00:00Z" + "last_reviewed_time": null }"#; - let run: RunWithAnnotationQueueInfo = serde_json::from_str(json).unwrap(); - assert_eq!(run.run.name, "ChatOpenAI"); - assert!(run.last_reviewed_time.is_some()); - assert!(run.added_at.is_some()); - assert!(run.effective_added_at.is_some()); + let item: AnnotationQueueItem = serde_json::from_str(json).unwrap(); + assert_eq!(item.item_type, Some(AnnotationQueueItemType::Thread)); + assert!(item.run_id.is_none()); + assert!(item.added_at.is_some()); + assert!(item.last_reviewed_time.is_none()); + } + + #[test] + fn test_queue_item_input_serialization() { + let run_id = Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let input = AnnotationQueueItemInput { + item_type: Some(AnnotationQueueItemType::Run), + run_id: Some(run_id), + ..Default::default() + }; + let value = serde_json::to_value(&input).unwrap(); + assert_eq!( + value, + serde_json::json!({"item_type": "RUN", "run_id": run_id.to_string()}) + ); + } + + #[test] + fn test_item_status_wire_values() { + for status in AnnotationQueueItemStatus::ALL { + assert_eq!( + serde_json::to_string(&status).unwrap(), + format!("\"{}\"", status.as_str()) + ); + } } } diff --git a/sdk/src/client.rs b/sdk/src/client.rs index 473e8edf9..c846a5a9f 100644 --- a/sdk/src/client.rs +++ b/sdk/src/client.rs @@ -904,12 +904,46 @@ impl LangchainClient { self.execute_status_only_request(request).await } - /// Add runs to an annotation queue. + /// Add items to an annotation queue. /// - /// # Arguments + /// The API takes at most 100 items per request, so a longer list is sent in + /// several requests. Returns the created items, each with its queue item ID. /// - /// * `queue_id` - The UUID of the annotation queue - /// * `run_ids` - Vector of run UUIDs to add to the queue + /// # API Reference + /// + /// - Endpoint: `POST /api/v1/platform/annotation-queues/{queue_id}/items` + /// - Request body: `{"items": [AnnotationQueueItemInput]}`, at most 100 per request + pub async fn add_items_to_annotation_queue( + &self, + queue_id: uuid::Uuid, + items: Vec, + ) -> Result> { + #[derive(Deserialize)] + struct AddResponse { + #[serde(default)] + items: Vec, + } + + let path = format!("/api/v1/platform/annotation-queues/{}/items", queue_id); + let mut created = Vec::with_capacity(items.len()); + for chunk in items.chunks(100) { + let request = self + .langsmith_post(&path)? + .json(&serde_json::json!({ "items": chunk })); + let response: AddResponse = self.execute(request).await?; + created.extend(response.items); + } + Ok(created) + } + + /// Add runs to an annotation queue by run ID. + /// + /// Each run becomes a `RUN` item that carries only its `run_id`. To locate a run + /// directly, as langsmith-sdk's `RunKey` does, build [`AnnotationQueueItemInput`]s with + /// `project_id` and `start_time` and call + /// [`add_items_to_annotation_queue`](Self::add_items_to_annotation_queue). + /// + /// [`AnnotationQueueItemInput`]: crate::annotation_queues::AnnotationQueueItemInput /// /// # Example /// @@ -921,114 +955,243 @@ impl LangchainClient { /// let client = LangchainClient::new(auth)?; /// /// let queue_id = Uuid::parse_str("12345678-1234-1234-1234-123456789012").unwrap(); - /// let run_ids = vec![ - /// Uuid::parse_str("abcdef01-1234-1234-1234-123456789012").unwrap(), - /// Uuid::parse_str("abcdef02-1234-1234-1234-123456789012").unwrap(), - /// ]; + /// let run_ids = vec![Uuid::parse_str("abcdef01-1234-1234-1234-123456789012").unwrap()]; /// - /// client.add_runs_to_annotation_queue(queue_id, run_ids).await?; - /// println!("Runs added successfully"); + /// let items = client.add_runs_to_annotation_queue(queue_id, run_ids).await?; + /// println!("Added {} items", items.len()); /// # Ok(()) /// # } /// ``` /// /// # API Reference /// - /// - Endpoint: `POST /api/v1/annotation-queues/{queue_id}/runs` - /// - Request body: JSON array of UUID strings - /// - OpenAPI spec: + /// - Endpoint: `POST /api/v1/platform/annotation-queues/{queue_id}/items` pub async fn add_runs_to_annotation_queue( &self, queue_id: uuid::Uuid, run_ids: Vec, - ) -> Result<()> { - let path = format!("/api/v1/annotation-queues/{}/runs", queue_id); - - // Convert UUIDs to strings for JSON serialization - let run_id_strings: Vec = run_ids.iter().map(|id| id.to_string()).collect(); - - let request_builder = self.langsmith_post(&path)?.json(&run_id_strings); - self.execute_status_only_request(request_builder).await + ) -> Result> { + let items = run_ids + .into_iter() + .map( + |run_id| crate::annotation_queues::AnnotationQueueItemInput { + item_type: Some(crate::annotation_queues::AnnotationQueueItemType::Run), + run_id: Some(run_id), + ..Default::default() + }, + ) + .collect(); + self.add_items_to_annotation_queue(queue_id, items).await } - /// Remove a run from an annotation queue. + /// List one page of the items in one review section of an annotation queue. /// - /// # Arguments + /// Pass the returned `next_cursor` as `cursor` to get the next page. /// - /// * `queue_id` - The UUID of the annotation queue - /// * `run_id` - The UUID of the run to remove + /// # API Reference /// - /// # Example + /// - Endpoint: `GET /api/v1/platform/annotation-queues/{queue_id}/items` + pub async fn list_annotation_queue_items( + &self, + queue_id: uuid::Uuid, + params: crate::annotation_queues::ListAnnotationQueueItemsParams, + cursor: Option<&str>, + ) -> Result { + let path = format!("/api/v1/platform/annotation-queues/{}/items", queue_id); + let mut request = self + .langsmith_get(&path)? + .query(&[("status", params.status.as_str())]); + if let Some(page_size) = params.page_size { + request = request.query(&[("page_size", page_size)]); + } + if let Some(item_type) = params.item_type { + request = request.query(&[("item_type", item_type.as_str())]); + } + if let Some(cursor) = cursor { + request = request.query(&[("cursor", cursor)]); + } + self.execute(request).await + } + + /// List the items of one review section across pages. /// - /// ```no_run - /// # use langstar_sdk::{AuthConfig, LangchainClient}; - /// # use uuid::Uuid; - /// # async fn example() -> langstar_sdk::Result<()> { - /// let auth = AuthConfig::from_env()?; - /// let client = LangchainClient::new(auth)?; + /// Returns up to `limit` items, or every item in the section when `limit` is `None`. + /// The call requests the next page whenever `next_cursor` is non-empty, even after an + /// empty page, and stops if the API sends a cursor the call has already used. + pub async fn collect_annotation_queue_items( + &self, + queue_id: uuid::Uuid, + params: crate::annotation_queues::ListAnnotationQueueItemsParams, + limit: Option, + ) -> Result> { + let mut items = Vec::new(); + if limit == Some(0) { + return Ok(items); + } + let mut cursor: Option = None; + let mut seen_cursors = std::collections::HashSet::new(); + loop { + let page = self + .list_annotation_queue_items(queue_id, params, cursor.as_deref()) + .await?; + for item in page.items { + items.push(item); + if limit.is_some_and(|limit| items.len() >= limit) { + return Ok(items); + } + } + match page.next_cursor { + Some(next) if !next.is_empty() && seen_cursors.insert(next.clone()) => { + cursor = Some(next) + } + _ => return Ok(items), + } + } + } + + /// Count the items in one review section of an annotation queue, or every item with + /// [`AnnotationQueueItemCountStatus::All`]. /// - /// let queue_id = Uuid::parse_str("12345678-1234-1234-1234-123456789012").unwrap(); - /// let run_id = Uuid::parse_str("abcdef01-1234-1234-1234-123456789012").unwrap(); + /// [`AnnotationQueueItemCountStatus::All`]: crate::annotation_queues::AnnotationQueueItemCountStatus::All /// - /// client.delete_run_from_annotation_queue(queue_id, run_id).await?; - /// println!("Run removed successfully"); - /// # Ok(()) - /// # } - /// ``` + /// # API Reference + /// + /// - Endpoint: `GET /api/v1/platform/annotation-queues/{queue_id}/items/count` + pub async fn count_annotation_queue_items( + &self, + queue_id: uuid::Uuid, + status: impl Into, + ) -> Result { + let status = status.into(); + let path = format!( + "/api/v1/platform/annotation-queues/{}/items/count", + queue_id + ); + let request = self + .langsmith_get(&path)? + .query(&[("status", status.as_str())]); + let response: crate::annotation_queues::AnnotationQueueItemCountResponse = + self.execute(request).await?; + Ok(response.count) + } + + /// Update an item of an annotation queue. /// /// # API Reference /// - /// - Endpoint: `DELETE /api/v1/annotation-queues/{queue_id}/runs/{run_id}` - /// - OpenAPI spec: - pub async fn delete_run_from_annotation_queue( + /// - Endpoint: `PATCH /api/v1/platform/annotation-queues/{queue_id}/items/{item_id}` + pub async fn update_annotation_queue_item( &self, queue_id: uuid::Uuid, - run_id: uuid::Uuid, - ) -> Result<()> { - let path = format!("/api/v1/annotation-queues/{}/runs/{}", queue_id, run_id); - let request = self.langsmith_delete(&path)?; - self.execute_status_only_request(request).await + item_id: uuid::Uuid, + request: crate::annotation_queues::PatchAnnotationQueueItemRequest, + ) -> Result { + let path = format!( + "/api/v1/platform/annotation-queues/{}/items/{}", + queue_id, item_id + ); + let request = self.langsmith_patch(&path)?.json(&request); + self.execute(request).await } - /// Get a run from an annotation queue at the specified index. + /// Get where an item sits in its annotation queue. /// - /// # Arguments + /// # API Reference /// - /// * `queue_id` - The UUID of the annotation queue - /// * `index` - Zero-based index of the run in the queue + /// - Endpoint: `GET /api/v1/platform/annotation-queues/{queue_id}/items/{item_id}/placement` + pub async fn get_annotation_queue_item_placement( + &self, + queue_id: uuid::Uuid, + item_id: uuid::Uuid, + ) -> Result { + let path = format!( + "/api/v1/platform/annotation-queues/{}/items/{}/placement", + queue_id, item_id + ); + let request = self.langsmith_get(&path)?; + self.execute(request).await + } + + /// Remove items from an annotation queue by queue item ID. /// - /// # Returns + /// The IDs are the `id` of each [`AnnotationQueueItem`] or + /// [`AnnotationQueueListItem`], not run IDs; to remove a run by its run ID, use + /// [`delete_run_from_annotation_queue`](Self::delete_run_from_annotation_queue). /// - /// A `RunWithAnnotationQueueInfo` containing the run data and queue-specific metadata. + /// [`AnnotationQueueItem`]: crate::annotation_queues::AnnotationQueueItem + /// [`AnnotationQueueListItem`]: crate::annotation_queues::AnnotationQueueListItem /// - /// # Example + /// # API Reference /// - /// ```no_run - /// # use langstar_sdk::{AuthConfig, LangchainClient}; - /// # use uuid::Uuid; - /// # async fn example() -> langstar_sdk::Result<()> { - /// let auth = AuthConfig::from_env()?; - /// let client = LangchainClient::new(auth)?; + /// - Endpoint: `POST /api/v1/platform/annotation-queues/{queue_id}/items/delete` + pub async fn delete_items_from_annotation_queue( + &self, + queue_id: uuid::Uuid, + item_ids: Vec, + ) -> Result<()> { + let path = format!( + "/api/v1/platform/annotation-queues/{}/items/delete", + queue_id + ); + let request = self + .langsmith_post(&path)? + .json(&serde_json::json!({ "item_ids": item_ids })); + self.execute_status_only_request(request).await + } + + /// Remove a run from an annotation queue by run ID. /// - /// let queue_id = Uuid::parse_str("12345678-1234-1234-1234-123456789012").unwrap(); - /// let run = client.get_run_from_annotation_queue(queue_id, 0).await?; - /// println!("Run: {} (added at: {:?})", run.run.name, run.added_at); - /// # Ok(()) - /// # } - /// ``` + /// The items API deletes by queue item ID, so this lists every review section for the + /// item whose `run_id` matches, page by page, and deletes it. Returns `Ok(false)` when the queue + /// holds no item for the run. /// /// # API Reference /// - /// - Endpoint: `GET /api/v1/annotation-queues/{queue_id}/run/{index}` - /// - OpenAPI spec: - pub async fn get_run_from_annotation_queue( + /// - Endpoints: `GET /api/v1/platform/annotation-queues/{queue_id}/items`, then + /// `POST /api/v1/platform/annotation-queues/{queue_id}/items/delete` + pub async fn delete_run_from_annotation_queue( &self, queue_id: uuid::Uuid, - index: u32, - ) -> Result { - let path = format!("/api/v1/annotation-queues/{}/run/{}", queue_id, index); - let request = self.langsmith_get(&path)?; - self.execute(request).await + run_id: uuid::Uuid, + ) -> Result { + use crate::annotation_queues::{ + AnnotationQueueItemStatus, AnnotationQueueItemType, ListAnnotationQueueItemsParams, + }; + + let mut matches = Vec::new(); + for status in AnnotationQueueItemStatus::ALL { + let mut params = ListAnnotationQueueItemsParams::new(status); + params.item_type = Some(AnnotationQueueItemType::Run); + params.page_size = Some(100); + // Walk the pages and keep only the matching item IDs, so a large queue is + // never held in memory whole. + let mut cursor: Option = None; + let mut seen_cursors = std::collections::HashSet::new(); + loop { + let page = self + .list_annotation_queue_items(queue_id, params, cursor.as_deref()) + .await?; + matches.extend( + page.items + .iter() + .filter(|item| item.run_id == Some(run_id)) + .map(|item| item.id), + ); + match page.next_cursor { + Some(next) if !next.is_empty() && seen_cursors.insert(next.clone()) => { + cursor = Some(next) + } + _ => break, + } + } + } + + if matches.is_empty() { + return Ok(false); + } + self.delete_items_from_annotation_queue(queue_id, matches) + .await?; + Ok(true) } // ═══════════════════════════════════════════════════════════════════════ diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index b6715dcc2..73b81fddc 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -55,9 +55,12 @@ pub mod test_utils; // Re-export commonly used types pub use annotation_queues::{ - AnnotationQueue, AnnotationQueueRubricItem, AnnotationQueueWithDetails, - CreateAnnotationQueueRequest, ListAnnotationQueuesParams, QueueType, - RunWithAnnotationQueueInfo, UpdateAnnotationQueueRequest, + AnnotationQueue, AnnotationQueueItem, AnnotationQueueItemCountStatus, AnnotationQueueItemInput, + AnnotationQueueItemPlacement, AnnotationQueueItemStatus, AnnotationQueueItemType, + AnnotationQueueListItem, AnnotationQueueRubricItem, AnnotationQueueWithDetails, + CreateAnnotationQueueRequest, ListAnnotationQueueItemsParams, ListAnnotationQueueItemsResponse, + ListAnnotationQueuesParams, PatchAnnotationQueueItemRequest, QueueType, + UpdateAnnotationQueueRequest, }; pub use assistants::{ Assistant, AssistantClient, AssistantSearchRequest, CreateAssistantRequest, diff --git a/sdk/tests/annotation_queue_items_test.rs b/sdk/tests/annotation_queue_items_test.rs new file mode 100644 index 000000000..bb3f197fa --- /dev/null +++ b/sdk/tests/annotation_queue_items_test.rs @@ -0,0 +1,429 @@ +//! HTTP-mocked tests for the annotation queue items methods, which call +//! `/api/v1/platform/annotation-queues/{queue_id}/items`. + +use langstar_sdk::{ + AnnotationQueueItemInput, AnnotationQueueItemStatus, AnnotationQueueItemType, AuthConfig, + LangchainClient, ListAnnotationQueueItemsParams, +}; +use mockito::{Matcher, Server}; +use serde_json::json; +use uuid::Uuid; + +const QUEUE: &str = "12345678-1234-1234-1234-123456789012"; + +fn create_test_client(server_url: &str) -> LangchainClient { + let auth = AuthConfig::new(Some("test_langsmith_key".to_string()), None, None); + LangchainClient::with_base_urls( + auth, + server_url.to_string(), + "https://api.langgraph.cloud".to_string(), + "https://api.host.langchain.com".to_string(), + ) + .expect("Failed to create test client") +} + +fn list_item(id: Uuid, run_id: Uuid) -> serde_json::Value { + json!({ + "id": id, + "queue_id": QUEUE, + "item_type": "RUN", + "run_id": run_id, + "added_at": "2026-01-01T00:00:00Z", + "last_reviewed_time": null + }) +} + +#[tokio::test] +async fn test_add_runs_sends_run_items_and_returns_created_items() { + let mut server = Server::new_async().await; + let run_id = Uuid::new_v4(); + let item_id = Uuid::new_v4(); + + let mock = server + .mock( + "POST", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items").as_str(), + ) + .match_body(Matcher::Json(json!({ + "items": [{"item_type": "RUN", "run_id": run_id}] + }))) + .with_status(201) + .with_header("content-type", "application/json") + .with_body(json!({"items": [list_item(item_id, run_id)]}).to_string()) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let items = client + .add_runs_to_annotation_queue(Uuid::parse_str(QUEUE).unwrap(), vec![run_id]) + .await + .expect("add failed"); + + assert_eq!(items.len(), 1); + assert_eq!(items[0].id, item_id); + assert_eq!(items[0].run_id, Some(run_id)); + assert_eq!(items[0].item_type, Some(AnnotationQueueItemType::Run)); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_add_items_splits_requests_at_100() { + let mut server = Server::new_async().await; + let mock = server + .mock( + "POST", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items").as_str(), + ) + .with_status(201) + .with_header("content-type", "application/json") + .with_body(json!({"items": []}).to_string()) + .expect(2) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let items = (0..101) + .map(|_| AnnotationQueueItemInput { + item_type: Some(AnnotationQueueItemType::Run), + run_id: Some(Uuid::new_v4()), + ..Default::default() + }) + .collect(); + client + .add_items_to_annotation_queue(Uuid::parse_str(QUEUE).unwrap(), items) + .await + .expect("add failed"); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_list_items_sends_status_and_cursor() { + let mut server = Server::new_async().await; + let (item_id, run_id) = (Uuid::new_v4(), Uuid::new_v4()); + + let mock = server + .mock( + "GET", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items").as_str(), + ) + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("status".into(), "archived".into()), + Matcher::UrlEncoded("cursor".into(), "abc".into()), + Matcher::UrlEncoded("page_size".into(), "5".into()), + ])) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": [list_item(item_id, run_id)], "next_cursor": "def"}).to_string()) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let mut params = ListAnnotationQueueItemsParams::new(AnnotationQueueItemStatus::Archived); + params.page_size = Some(5); + let page = client + .list_annotation_queue_items(Uuid::parse_str(QUEUE).unwrap(), params, Some("abc")) + .await + .expect("list failed"); + + assert_eq!(page.items.len(), 1); + assert_eq!(page.items[0].run_id, Some(run_id)); + assert_eq!(page.next_cursor.as_deref(), Some("def")); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_count_items_returns_count() { + let mut server = Server::new_async().await; + let mock = server + .mock( + "GET", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items/count").as_str(), + ) + .match_query(Matcher::UrlEncoded( + "status".into(), + "needs_my_review".into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"count": 7}).to_string()) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let count = client + .count_annotation_queue_items( + Uuid::parse_str(QUEUE).unwrap(), + AnnotationQueueItemStatus::NeedsMyReview, + ) + .await + .expect("count failed"); + assert_eq!(count, 7); + mock.assert_async().await; +} + +#[tokio::test] +async fn test_delete_run_finds_item_by_run_id_across_sections() { + let mut server = Server::new_async().await; + let (item_id, run_id) = (Uuid::new_v4(), Uuid::new_v4()); + let list_path = format!("/api/v1/platform/annotation-queues/{QUEUE}/items"); + + // The run sits in the archived section; the other two sections hold another run. + let mut list_mocks = Vec::new(); + for status in ["needs_my_review", "needs_others_review"] { + list_mocks.push( + server + .mock("GET", list_path.as_str()) + .match_query(Matcher::UrlEncoded("status".into(), status.into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({"items": [list_item(Uuid::new_v4(), Uuid::new_v4())]}).to_string(), + ) + .create_async() + .await, + ); + } + list_mocks.push( + server + .mock("GET", list_path.as_str()) + .match_query(Matcher::UrlEncoded("status".into(), "archived".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": [list_item(item_id, run_id)]}).to_string()) + .create_async() + .await, + ); + let delete = server + .mock("POST", format!("{list_path}/delete").as_str()) + .match_body(Matcher::Json(json!({"item_ids": [item_id]}))) + .with_status(200) + .with_header("content-type", "application/json") + .with_body("{}") + .create_async() + .await; + + let client = create_test_client(&server.url()); + let removed = client + .delete_run_from_annotation_queue(Uuid::parse_str(QUEUE).unwrap(), run_id) + .await + .expect("delete failed"); + + assert!(removed); + delete.assert_async().await; + for mock in list_mocks { + mock.assert_async().await; + } +} + +#[tokio::test] +async fn test_delete_run_returns_false_when_run_not_in_queue() { + let mut server = Server::new_async().await; + let list_path = format!("/api/v1/platform/annotation-queues/{QUEUE}/items"); + let list = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::Any) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": []}).to_string()) + .expect(3) + .create_async() + .await; + let delete = server + .mock("POST", format!("{list_path}/delete").as_str()) + .expect(0) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let removed = client + .delete_run_from_annotation_queue(Uuid::parse_str(QUEUE).unwrap(), Uuid::new_v4()) + .await + .expect("delete failed"); + + assert!(!removed); + list.assert_async().await; + delete.assert_async().await; +} + +#[tokio::test] +async fn test_collect_items_follows_cursor_and_stops_at_limit() { + let mut server = Server::new_async().await; + let list_path = format!("/api/v1/platform/annotation-queues/{QUEUE}/items"); + let ids: Vec = (0..4).map(|_| Uuid::new_v4()).collect(); + + let page_two = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("status".into(), "archived".into()), + Matcher::UrlEncoded("cursor".into(), "page2".into()), + ])) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({"items": [list_item(ids[2], ids[2]), list_item(ids[3], ids[3])]}).to_string(), + ) + .expect(2) + .create_async() + .await; + let page_one = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("status".into(), "archived".into()), + Matcher::Regex("^status=archived$".into()), + ])) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({ + "items": [list_item(ids[0], ids[0]), list_item(ids[1], ids[1])], + "next_cursor": "page2" + }) + .to_string(), + ) + .expect(2) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let queue = Uuid::parse_str(QUEUE).unwrap(); + let params = ListAnnotationQueueItemsParams::new(AnnotationQueueItemStatus::Archived); + + let all = client + .collect_annotation_queue_items(queue, params, None) + .await + .expect("collect failed"); + assert_eq!(all.iter().map(|i| i.id).collect::>(), ids); + + let limited = client + .collect_annotation_queue_items(queue, params, Some(3)) + .await + .expect("collect failed"); + assert_eq!(limited.iter().map(|i| i.id).collect::>(), ids[..3]); + + page_one.assert_async().await; + page_two.assert_async().await; +} + +#[tokio::test] +async fn test_collect_follows_cursor_past_empty_page_and_stops_on_repeat() { + let mut server = Server::new_async().await; + let list_path = format!("/api/v1/platform/annotation-queues/{QUEUE}/items"); + let item_id = Uuid::new_v4(); + + let _last = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::UrlEncoded("cursor".into(), "page3".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({"items": [list_item(item_id, item_id)], "next_cursor": "page3"}).to_string(), + ) + .expect(1) + .create_async() + .await; + let _empty = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::UrlEncoded("cursor".into(), "page2".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": [], "next_cursor": "page3"}).to_string()) + .expect(1) + .create_async() + .await; + let _first = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::Regex("^status=archived$".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": [], "next_cursor": "page2"}).to_string()) + .expect(1) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let all = client + .collect_annotation_queue_items( + Uuid::parse_str(QUEUE).unwrap(), + ListAnnotationQueueItemsParams::new(AnnotationQueueItemStatus::Archived), + None, + ) + .await + .expect("collect failed"); + assert_eq!(all.iter().map(|i| i.id).collect::>(), vec![item_id]); +} + +#[tokio::test] +async fn test_delete_run_finds_item_on_second_page() { + let mut server = Server::new_async().await; + let list_path = format!("/api/v1/platform/annotation-queues/{QUEUE}/items"); + let (item_id, run_id) = (Uuid::new_v4(), Uuid::new_v4()); + + let _page_two = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::UrlEncoded("cursor".into(), "page2".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"items": [list_item(item_id, run_id)]}).to_string()) + .create_async() + .await; + let _first_pages = server + .mock("GET", list_path.as_str()) + .match_query(Matcher::Regex( + "^status=(needs_my_review|needs_others_review|archived)&page_size=100&item_type=RUN$" + .into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({ + "items": [list_item(Uuid::new_v4(), Uuid::new_v4())], + "next_cursor": "page2" + }) + .to_string(), + ) + .create_async() + .await; + let delete = server + .mock("POST", format!("{list_path}/delete").as_str()) + .match_body(Matcher::PartialJson(json!({"item_ids": [item_id]}))) + .with_status(200) + .with_header("content-type", "application/json") + .with_body("{}") + .create_async() + .await; + + let client = create_test_client(&server.url()); + let removed = client + .delete_run_from_annotation_queue(Uuid::parse_str(QUEUE).unwrap(), run_id) + .await + .expect("delete failed"); + assert!(removed); + delete.assert_async().await; +} + +#[tokio::test] +async fn test_count_items_all_sends_status_all() { + let mut server = Server::new_async().await; + let mock = server + .mock( + "GET", + format!("/api/v1/platform/annotation-queues/{QUEUE}/items/count").as_str(), + ) + .match_query(Matcher::UrlEncoded("status".into(), "all".into())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(json!({"count": 9}).to_string()) + .create_async() + .await; + + let client = create_test_client(&server.url()); + let count = client + .count_annotation_queue_items( + Uuid::parse_str(QUEUE).unwrap(), + langstar_sdk::AnnotationQueueItemCountStatus::All, + ) + .await + .expect("count failed"); + assert_eq!(count, 9); + mock.assert_async().await; +}