Skip to content

Commit 74c0bb6

Browse files
committed
docs: properly document the new aggregation pipeline endpoint
1 parent 9f53cd2 commit 74c0bb6

2 files changed

Lines changed: 100 additions & 4 deletions

File tree

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ result = response.json()
4545
### Query endpoints
4646

4747
- `GET /upgrade-query` — Build a query using the LLM query builder.
48-
- `POST /retrieve-records` — Run a query against the metadata store.
48+
- `POST /retrieve-records` — Run a filter query or aggregation pipeline against the metadata store.
4949

50-
Optional query parameters for `/retrieve-records`:
50+
To run a **filter query**, send a JSON object as the body:
51+
52+
Optional query parameters:
5153
- `names_only=true` — return only asset names
5254
- `limit=<int>` — limit number of results (default 0 = no limit)
5355
- `projection=<json>` — JSON object specifying which fields to include/exclude
@@ -61,6 +63,19 @@ response = requests.post(
6163
print(response.json())
6264
```
6365

66+
To run an **aggregation pipeline**, send a JSON array of pipeline stage dicts as the body. The pipeline is always executed against DocumentDB.
67+
68+
```python
69+
response = requests.post(
70+
"https://metadata-portal.allenneuraldynamics-test.org/retrieve-records",
71+
json=[
72+
{"$match": {"data_description.project_name": "MyProject"}},
73+
{"$group": {"_id": "$subject.subject_id", "count": {"$sum": 1}}},
74+
],
75+
)
76+
print(response.json())
77+
```
78+
6479
### Chat endpoint
6580

6681
`POST /chat` — Ask a natural-language question about the metadata store. The agent can query records, look up schema info, and summarize results.
@@ -250,9 +265,11 @@ Fields that rename across schema versions (e.g. `session` → `acquisition`, `ri
250265
### Query endpoints
251266

252267
- `GET /upgrade-query` — Build a query using the LLM query builder. Pass query string parameters as needed.
253-
- `POST /retrieve-records` — Run a query. Accepts a JSON object as the request body.
268+
- `POST /retrieve-records` — Run a filter query or aggregation pipeline against the metadata store.
254269

255-
Optional query parameters for `/retrieve-records`:
270+
To run a **filter query**, send a JSON object as the body:
271+
272+
Optional query parameters:
256273
- `names_only=true` — return only asset names
257274
- `limit=<int>` — limit number of results (default 0 = no limit)
258275
- `projection=<json>` — JSON object specifying which fields to include/exclude (e.g. `{"subject.subject_id": 1}`)
@@ -266,6 +283,19 @@ response = requests.post(
266283
print(response.json())
267284
```
268285

286+
To run an **aggregation pipeline**, send a JSON array of pipeline stage dicts as the body. The pipeline is always executed against DocumentDB.
287+
288+
```python
289+
response = requests.post(
290+
"https://metadata-portal.allenneuraldynamics-test.org/retrieve-records",
291+
json=[
292+
{"$match": {"data_description.project_name": "MyProject"}},
293+
{"$group": {"_id": "$subject.subject_id", "count": {"$sum": 1}}},
294+
],
295+
)
296+
print(response.json())
297+
```
298+
269299
### Chat endpoint
270300

271301
`POST /chat` — Ask a natural-language question about the metadata store. The agent can query records, look up schema info, and summarize results.

tests/test_validation.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,5 +369,71 @@ def test_upgrade_by_asset_name_fetch_error(self, mock_retrieve):
369369
self.assertIn("Failed to fetch record", response.json()["error"])
370370

371371

372+
class TestRetrieveRecordsEndpoint(unittest.TestCase):
373+
374+
@patch("aind_metadata_viz.endpoints.retrieve_records")
375+
def test_filter_query(self, mock_retrieve):
376+
mock_result = Mock()
377+
mock_result.backend = "cache"
378+
mock_result.elapsed_seconds = 0.1
379+
mock_result.asset_names = ["asset-1"]
380+
mock_result.records = [{"name": "asset-1"}]
381+
mock_retrieve.return_value = mock_result
382+
383+
response = client.post("/retrieve-records", json={"subject.subject_id": "123456"})
384+
self.assertEqual(response.status_code, 200)
385+
body = response.json()
386+
self.assertEqual(body["asset_names"], ["asset-1"])
387+
self.assertEqual(body["records"], [{"name": "asset-1"}])
388+
mock_retrieve.assert_called_once()
389+
390+
@patch("aind_metadata_viz.endpoints.retrieve_aggregation")
391+
def test_aggregation_pipeline(self, mock_aggregate):
392+
mock_result = Mock()
393+
mock_result.backend = "docdb"
394+
mock_result.elapsed_seconds = 0.2
395+
mock_result.asset_names = ["asset-2"]
396+
mock_result.records = [{"name": "asset-2", "count": 5}]
397+
mock_aggregate.return_value = mock_result
398+
399+
pipeline = [{"$match": {"subject.subject_id": "123456"}}, {"$limit": 5}]
400+
response = client.post("/retrieve-records", json=pipeline)
401+
self.assertEqual(response.status_code, 200)
402+
body = response.json()
403+
self.assertEqual(body["backend"], "docdb")
404+
self.assertEqual(body["asset_names"], ["asset-2"])
405+
self.assertEqual(body["records"], [{"name": "asset-2", "count": 5}])
406+
mock_aggregate.assert_called_once_with(pipeline)
407+
408+
@patch("aind_metadata_viz.endpoints.retrieve_aggregation")
409+
def test_aggregation_pipeline_error(self, mock_aggregate):
410+
mock_aggregate.side_effect = Exception("pipeline error")
411+
412+
response = client.post("/retrieve-records", json=[{"$match": {}}])
413+
self.assertEqual(response.status_code, 500)
414+
self.assertIn("Aggregation execution failed", response.json()["error"])
415+
416+
@patch("aind_metadata_viz.endpoints.retrieve_records")
417+
def test_filter_query_error(self, mock_retrieve):
418+
mock_retrieve.side_effect = Exception("connection error")
419+
420+
response = client.post("/retrieve-records", json={"name": "test"})
421+
self.assertEqual(response.status_code, 500)
422+
self.assertIn("Query execution failed", response.json()["error"])
423+
424+
def test_invalid_json(self):
425+
response = client.post(
426+
"/retrieve-records",
427+
content=b"not json",
428+
headers={"Content-Type": "application/json"},
429+
)
430+
self.assertEqual(response.status_code, 400)
431+
self.assertIn("Invalid JSON format", response.json()["error"])
432+
433+
def test_invalid_body_type(self):
434+
response = client.post("/retrieve-records", json="a string")
435+
self.assertEqual(response.status_code, 400)
436+
437+
372438
if __name__ == "__main__":
373439
unittest.main()

0 commit comments

Comments
 (0)