feat: add tags filtering and q description fix for list documents API (#468)

* feat: add Pydantic AI integration to CI, release pipeline, and docs

- Add test-pydantic-ai-integration job to CI (test.yml)
- Add build, publish, and artifact steps to release workflow (release.yml)
- Add hindsight-integrations/pydantic-ai to release.sh version bumping
- Add Pydantic AI documentation page (sdks/integrations/pydantic-ai.md)
- Add Pydantic AI entry to sidebar with icon

* docs: remove Requirements section from pydantic-ai integration page

* feat: add tags filtering and fix offset pagination docs for list documents API

- Add `tags` and `tags_match` query params to GET /banks/{bank_id}/documents
- Supports any, all, any_strict, all_strict matching modes (default: any_strict)
- Fix `q` param description — it's a case-insensitive substring match on document ID only
- Add tests for offset pagination and all tags_match modes
- Regenerate OpenAPI spec and Python/TypeScript/Go clients
- Document the new filtering options in docs/developer/api/documents.mdx

* fix(cli): pass new tags/tags_match args to list_documents
This commit is contained in:
Nicolò Boschi 2026-03-02 17:03:16 +01:00 committed by GitHub
parent ecf609c8aa
commit 1d70abfe85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 490 additions and 17 deletions

View file

@ -3064,7 +3064,13 @@ def _register_routes(app: FastAPI):
) )
async def api_list_documents( async def api_list_documents(
bank_id: str, bank_id: str,
q: str | None = None, q: str | None = Query(
None, description="Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')"
),
tags: list[str] | None = Query(None, description="Filter documents by tags"),
tags_match: str = Query(
"any_strict", description="How to match tags: 'any', 'all', 'any_strict', 'all_strict'"
),
limit: int = 100, limit: int = 100,
offset: int = 0, offset: int = 0,
request_context: RequestContext = Depends(get_request_context), request_context: RequestContext = Depends(get_request_context),
@ -3074,13 +3080,21 @@ def _register_routes(app: FastAPI):
Args: Args:
bank_id: Memory Bank ID (from path) bank_id: Memory Bank ID (from path)
q: Search query (searches document ID and metadata) q: Case-insensitive substring filter on document ID
tags: Filter documents by tags
tags_match: How to match tags (any, all, any_strict, all_strict)
limit: Maximum number of results (default: 100) limit: Maximum number of results (default: 100)
offset: Offset for pagination (default: 0) offset: Offset for pagination (default: 0)
""" """
try: try:
data = await app.state.memory.list_documents( data = await app.state.memory.list_documents(
bank_id=bank_id, search_query=q, limit=limit, offset=offset, request_context=request_context bank_id=bank_id,
search_query=q,
tags=tags,
tags_match=tags_match,
limit=limit,
offset=offset,
request_context=request_context,
) )
return data return data
except OperationValidationError as e: except OperationValidationError as e:

View file

@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING: if TYPE_CHECKING:
from hindsight_api.engine.memory_engine import Budget from hindsight_api.engine.memory_engine import Budget
from hindsight_api.engine.response_models import RecallResult, ReflectResult from hindsight_api.engine.response_models import RecallResult, ReflectResult
from hindsight_api.engine.search.tags import TagsMatch
from hindsight_api.models import RequestContext from hindsight_api.models import RequestContext
@ -337,6 +338,8 @@ class MemoryEngineInterface(ABC):
bank_id: str, bank_id: str,
*, *,
search_query: str | None = None, search_query: str | None = None,
tags: list[str] | None = None,
tags_match: "TagsMatch" = "any_strict",
limit: int = 100, limit: int = 100,
offset: int = 0, offset: int = 0,
request_context: "RequestContext", request_context: "RequestContext",
@ -346,7 +349,9 @@ class MemoryEngineInterface(ABC):
Args: Args:
bank_id: The memory bank ID. bank_id: The memory bank ID.
search_query: Search query. search_query: Case-insensitive substring filter on document ID.
tags: Filter by tags.
tags_match: How to match tags (any, all, any_strict, all_strict).
limit: Maximum results. limit: Maximum results.
offset: Pagination offset. offset: Pagination offset.
request_context: Request context for authentication. request_context: Request context for authentication.

View file

@ -184,7 +184,7 @@ from .retain import bank_utils, embedding_utils
from .retain.types import RetainContentDict from .retain.types import RetainContentDict
from .search import think_utils from .search import think_utils
from .search.reranking import CrossEncoderReranker from .search.reranking import CrossEncoderReranker
from .search.tags import TagsMatch from .search.tags import TagsMatch, build_tags_where_clause
from .task_backend import BrokerTaskBackend, SyncTaskBackend, TaskBackend from .task_backend import BrokerTaskBackend, SyncTaskBackend, TaskBackend
@ -4142,6 +4142,8 @@ class MemoryEngine(MemoryEngineInterface):
bank_id: str, bank_id: str,
*, *,
search_query: str | None = None, search_query: str | None = None,
tags: list[str] | None = None,
tags_match: "TagsMatch" = "any_strict",
limit: int = 100, limit: int = 100,
offset: int = 0, offset: int = 0,
request_context: "RequestContext", request_context: "RequestContext",
@ -4152,6 +4154,8 @@ class MemoryEngine(MemoryEngineInterface):
Args: Args:
bank_id: bank ID (required) bank_id: bank ID (required)
search_query: Search in document ID search_query: Search in document ID
tags: Filter by tags
tags_match: How to match tags (any, all, any_strict, all_strict)
limit: Maximum number of results limit: Maximum number of results
offset: Offset for pagination offset: Offset for pagination
request_context: Request context for authentication. request_context: Request context for authentication.
@ -4182,7 +4186,16 @@ class MemoryEngine(MemoryEngineInterface):
query_conditions.append(f"id ILIKE ${param_count}") query_conditions.append(f"id ILIKE ${param_count}")
query_params.append(f"%{search_query}%") query_params.append(f"%{search_query}%")
tags_clause, tags_params, next_param = build_tags_where_clause(
tags, param_offset=param_count + 1, match=tags_match
)
query_params.extend(tags_params)
param_count = next_param - 1 # next_param is next available; convert to last used
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else "" where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
if tags_clause:
# tags_clause starts with "AND", append after WHERE conditions
where_clause = where_clause + " " + tags_clause if where_clause else "WHERE " + tags_clause[4:].lstrip()
# Get total count # Get total count
count_query = f""" count_query = f"""
@ -6038,8 +6051,6 @@ class MemoryEngine(MemoryEngineInterface):
async with acquire_with_retry(pool) as conn: async with acquire_with_retry(pool) as conn:
# Build filters # Build filters
from .search.tags import build_tags_where_clause
filters = ["bank_id = $1"] filters = ["bank_id = $1"]
params: list[Any] = [bank_id] params: list[Any] = [bank_id]
param_idx = 2 param_idx = 2

View file

@ -0,0 +1,174 @@
"""
Tests for list_documents pagination and tags filtering.
"""
from datetime import datetime, timezone
import pytest
async def _retain_doc(memory, bank_id, document_id, tags, request_context):
"""Helper to retain a document with given tags. Uses gibberish content to avoid LLM
fact extraction (documents are persisted even with zero facts)."""
await memory.retain_batch_async(
bank_id=bank_id,
contents=[{"content": f"xyzabc123 !@# $$$ {document_id}"}],
document_id=document_id,
document_tags=tags or None,
request_context=request_context,
)
@pytest.mark.asyncio
async def test_list_documents_offset_pagination(memory, request_context):
"""offset parameter returns the correct slice of documents."""
bank_id = f"test_list_docs_offset_{datetime.now(timezone.utc).timestamp()}"
try:
for i in range(4):
await _retain_doc(memory, bank_id, f"doc-{i:02d}", [], request_context)
# All documents, ordered by created_at DESC → doc-03, doc-02, doc-01, doc-00
all_docs = await memory.list_documents(
bank_id=bank_id, limit=10, offset=0, request_context=request_context
)
assert all_docs["total"] == 4
assert len(all_docs["items"]) == 4
all_ids = [d["id"] for d in all_docs["items"]]
# offset=2 should skip the first two and return the remaining two
page2 = await memory.list_documents(
bank_id=bank_id, limit=10, offset=2, request_context=request_context
)
assert page2["total"] == 4 # total is always the full count
assert len(page2["items"]) == 2
assert [d["id"] for d in page2["items"]] == all_ids[2:]
# offset beyond total returns empty items but correct total
beyond = await memory.list_documents(
bank_id=bank_id, limit=10, offset=10, request_context=request_context
)
assert beyond["total"] == 4
assert beyond["items"] == []
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_documents_tags_filter_any_strict(memory, request_context):
"""tags filter with any_strict returns only tagged documents that match."""
bank_id = f"test_list_docs_tags_{datetime.now(timezone.utc).timestamp()}"
try:
await _retain_doc(memory, bank_id, "doc-alpha", ["team-a"], request_context)
await _retain_doc(memory, bank_id, "doc-beta", ["team-b"], request_context)
await _retain_doc(memory, bank_id, "doc-both", ["team-a", "team-b"], request_context)
await _retain_doc(memory, bank_id, "doc-untagged", [], request_context)
# any_strict: only docs with at least one of the given tags, untagged excluded
result = await memory.list_documents(
bank_id=bank_id,
tags=["team-a"],
tags_match="any_strict",
request_context=request_context,
)
ids = {d["id"] for d in result["items"]}
assert ids == {"doc-alpha", "doc-both"}
assert result["total"] == 2
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_documents_tags_filter_any_includes_untagged(memory, request_context):
"""tags filter with 'any' mode includes untagged documents."""
bank_id = f"test_list_docs_tags_any_{datetime.now(timezone.utc).timestamp()}"
try:
await _retain_doc(memory, bank_id, "doc-tagged", ["team-a"], request_context)
await _retain_doc(memory, bank_id, "doc-other", ["team-b"], request_context)
await _retain_doc(memory, bank_id, "doc-untagged", [], request_context)
result = await memory.list_documents(
bank_id=bank_id,
tags=["team-a"],
tags_match="any",
request_context=request_context,
)
ids = {d["id"] for d in result["items"]}
# "any" includes untagged + matching tagged
assert "doc-tagged" in ids
assert "doc-untagged" in ids
assert "doc-other" not in ids
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_documents_tags_filter_all_strict(memory, request_context):
"""tags filter with all_strict returns only docs that have ALL the specified tags."""
bank_id = f"test_list_docs_tags_all_{datetime.now(timezone.utc).timestamp()}"
try:
await _retain_doc(memory, bank_id, "doc-a-only", ["team-a"], request_context)
await _retain_doc(memory, bank_id, "doc-a-and-b", ["team-a", "team-b"], request_context)
await _retain_doc(memory, bank_id, "doc-untagged", [], request_context)
result = await memory.list_documents(
bank_id=bank_id,
tags=["team-a", "team-b"],
tags_match="all_strict",
request_context=request_context,
)
ids = {d["id"] for d in result["items"]}
assert ids == {"doc-a-and-b"}
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_documents_no_tags_filter_returns_all(memory, request_context):
"""When no tags filter is specified, all documents are returned."""
bank_id = f"test_list_docs_no_tags_{datetime.now(timezone.utc).timestamp()}"
try:
await _retain_doc(memory, bank_id, "doc-tagged", ["team-a"], request_context)
await _retain_doc(memory, bank_id, "doc-untagged", [], request_context)
result = await memory.list_documents(
bank_id=bank_id,
tags=None,
request_context=request_context,
)
ids = {d["id"] for d in result["items"]}
assert ids == {"doc-tagged", "doc-untagged"}
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_documents_tags_and_search_query_combined(memory, request_context):
"""tags filter and q (search_query) can be combined."""
bank_id = f"test_list_docs_tags_q_{datetime.now(timezone.utc).timestamp()}"
try:
await _retain_doc(memory, bank_id, "report-2024", ["team-a"], request_context)
await _retain_doc(memory, bank_id, "report-2025", ["team-b"], request_context)
await _retain_doc(memory, bank_id, "summary-2024", ["team-a"], request_context)
result = await memory.list_documents(
bank_id=bank_id,
search_query="report",
tags=["team-a"],
tags_match="any_strict",
request_context=request_context,
)
ids = {d["id"] for d in result["items"]}
assert ids == {"report-2024"}
finally:
await memory.delete_bank(bank_id, request_context=request_context)

View file

@ -300,6 +300,8 @@ impl ApiClient {
offset.map(|o| o as i64), offset.map(|o| o as i64),
q, q,
None, None,
None,
None,
).await?; ).await?;
Ok(response.into_inner()) Ok(response.into_inner())
}) })

View file

@ -1173,7 +1173,9 @@ paths:
title: Bank Id title: Bank Id
type: string type: string
style: simple style: simple
- explode: true - description: Case-insensitive substring filter on document ID (e.g. 'report'
matches 'report-2024')
explode: true
in: query in: query
name: q name: q
required: false required: false
@ -1181,6 +1183,28 @@ paths:
nullable: true nullable: true
type: string type: string
style: form style: form
- description: Filter documents by tags
explode: true
in: query
name: tags
required: false
schema:
items:
type: string
nullable: true
type: array
style: form
- description: "How to match tags: 'any', 'all', 'any_strict', 'all_strict'"
explode: true
in: query
name: tags_match
required: false
schema:
default: any_strict
description: "How to match tags: 'any', 'all', 'any_strict', 'all_strict'"
title: Tags Match
type: string
style: form
- explode: true - explode: true
in: query in: query
name: limit name: limit

View file

@ -17,6 +17,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"reflect"
) )
@ -409,16 +410,31 @@ type ApiListDocumentsRequest struct {
ApiService *DocumentsAPIService ApiService *DocumentsAPIService
bankId string bankId string
q *string q *string
tags *[]string
tagsMatch *string
limit *int32 limit *int32
offset *int32 offset *int32
authorization *string authorization *string
} }
// Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')
func (r ApiListDocumentsRequest) Q(q string) ApiListDocumentsRequest { func (r ApiListDocumentsRequest) Q(q string) ApiListDocumentsRequest {
r.q = &q r.q = &q
return r return r
} }
// Filter documents by tags
func (r ApiListDocumentsRequest) Tags(tags []string) ApiListDocumentsRequest {
r.tags = &tags
return r
}
// How to match tags: 'any', 'all', 'any_strict', 'all_strict'
func (r ApiListDocumentsRequest) TagsMatch(tagsMatch string) ApiListDocumentsRequest {
r.tagsMatch = &tagsMatch
return r
}
func (r ApiListDocumentsRequest) Limit(limit int32) ApiListDocumentsRequest { func (r ApiListDocumentsRequest) Limit(limit int32) ApiListDocumentsRequest {
r.limit = &limit r.limit = &limit
return r return r
@ -480,6 +496,23 @@ func (a *DocumentsAPIService) ListDocumentsExecute(r ApiListDocumentsRequest) (*
if r.q != nil { if r.q != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "q", r.q, "form", "") parameterAddToHeaderOrQuery(localVarQueryParams, "q", r.q, "form", "")
} }
if r.tags != nil {
t := *r.tags
if reflect.TypeOf(t).Kind() == reflect.Slice {
s := reflect.ValueOf(t)
for i := 0; i < s.Len(); i++ {
parameterAddToHeaderOrQuery(localVarQueryParams, "tags", s.Index(i).Interface(), "form", "multi")
}
} else {
parameterAddToHeaderOrQuery(localVarQueryParams, "tags", t, "form", "multi")
}
}
if r.tagsMatch != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "tags_match", r.tagsMatch, "form", "")
} else {
var defaultValue string = "any_strict"
r.tagsMatch = &defaultValue
}
if r.limit != nil { if r.limit != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "") parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "")
} else { } else {

View file

@ -16,8 +16,9 @@ from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated from typing_extensions import Annotated
from pydantic import StrictInt, StrictStr from pydantic import Field, StrictInt, StrictStr
from typing import Optional from typing import List, Optional
from typing_extensions import Annotated
from hindsight_client_api.models.chunk_response import ChunkResponse from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.document_response import DocumentResponse from hindsight_client_api.models.document_response import DocumentResponse
@ -909,7 +910,9 @@ class DocumentsApi:
async def list_documents( async def list_documents(
self, self,
bank_id: StrictStr, bank_id: StrictStr,
q: Optional[StrictStr] = None, q: Annotated[Optional[StrictStr], Field(description="Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')")] = None,
tags: Annotated[Optional[List[StrictStr]], Field(description="Filter documents by tags")] = None,
tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags: 'any', 'all', 'any_strict', 'all_strict'")] = None,
limit: Optional[StrictInt] = None, limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None, offset: Optional[StrictInt] = None,
authorization: Optional[StrictStr] = None, authorization: Optional[StrictStr] = None,
@ -932,8 +935,12 @@ class DocumentsApi:
:param bank_id: (required) :param bank_id: (required)
:type bank_id: str :type bank_id: str
:param q: :param q: Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')
:type q: str :type q: str
:param tags: Filter documents by tags
:type tags: List[str]
:param tags_match: How to match tags: 'any', 'all', 'any_strict', 'all_strict'
:type tags_match: str
:param limit: :param limit:
:type limit: int :type limit: int
:param offset: :param offset:
@ -965,6 +972,8 @@ class DocumentsApi:
_param = self._list_documents_serialize( _param = self._list_documents_serialize(
bank_id=bank_id, bank_id=bank_id,
q=q, q=q,
tags=tags,
tags_match=tags_match,
limit=limit, limit=limit,
offset=offset, offset=offset,
authorization=authorization, authorization=authorization,
@ -993,7 +1002,9 @@ class DocumentsApi:
async def list_documents_with_http_info( async def list_documents_with_http_info(
self, self,
bank_id: StrictStr, bank_id: StrictStr,
q: Optional[StrictStr] = None, q: Annotated[Optional[StrictStr], Field(description="Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')")] = None,
tags: Annotated[Optional[List[StrictStr]], Field(description="Filter documents by tags")] = None,
tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags: 'any', 'all', 'any_strict', 'all_strict'")] = None,
limit: Optional[StrictInt] = None, limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None, offset: Optional[StrictInt] = None,
authorization: Optional[StrictStr] = None, authorization: Optional[StrictStr] = None,
@ -1016,8 +1027,12 @@ class DocumentsApi:
:param bank_id: (required) :param bank_id: (required)
:type bank_id: str :type bank_id: str
:param q: :param q: Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')
:type q: str :type q: str
:param tags: Filter documents by tags
:type tags: List[str]
:param tags_match: How to match tags: 'any', 'all', 'any_strict', 'all_strict'
:type tags_match: str
:param limit: :param limit:
:type limit: int :type limit: int
:param offset: :param offset:
@ -1049,6 +1064,8 @@ class DocumentsApi:
_param = self._list_documents_serialize( _param = self._list_documents_serialize(
bank_id=bank_id, bank_id=bank_id,
q=q, q=q,
tags=tags,
tags_match=tags_match,
limit=limit, limit=limit,
offset=offset, offset=offset,
authorization=authorization, authorization=authorization,
@ -1077,7 +1094,9 @@ class DocumentsApi:
async def list_documents_without_preload_content( async def list_documents_without_preload_content(
self, self,
bank_id: StrictStr, bank_id: StrictStr,
q: Optional[StrictStr] = None, q: Annotated[Optional[StrictStr], Field(description="Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')")] = None,
tags: Annotated[Optional[List[StrictStr]], Field(description="Filter documents by tags")] = None,
tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags: 'any', 'all', 'any_strict', 'all_strict'")] = None,
limit: Optional[StrictInt] = None, limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None, offset: Optional[StrictInt] = None,
authorization: Optional[StrictStr] = None, authorization: Optional[StrictStr] = None,
@ -1100,8 +1119,12 @@ class DocumentsApi:
:param bank_id: (required) :param bank_id: (required)
:type bank_id: str :type bank_id: str
:param q: :param q: Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')
:type q: str :type q: str
:param tags: Filter documents by tags
:type tags: List[str]
:param tags_match: How to match tags: 'any', 'all', 'any_strict', 'all_strict'
:type tags_match: str
:param limit: :param limit:
:type limit: int :type limit: int
:param offset: :param offset:
@ -1133,6 +1156,8 @@ class DocumentsApi:
_param = self._list_documents_serialize( _param = self._list_documents_serialize(
bank_id=bank_id, bank_id=bank_id,
q=q, q=q,
tags=tags,
tags_match=tags_match,
limit=limit, limit=limit,
offset=offset, offset=offset,
authorization=authorization, authorization=authorization,
@ -1157,6 +1182,8 @@ class DocumentsApi:
self, self,
bank_id, bank_id,
q, q,
tags,
tags_match,
limit, limit,
offset, offset,
authorization, authorization,
@ -1169,6 +1196,7 @@ class DocumentsApi:
_host = None _host = None
_collection_formats: Dict[str, str] = { _collection_formats: Dict[str, str] = {
'tags': 'multi',
} }
_path_params: Dict[str, str] = {} _path_params: Dict[str, str] = {}
@ -1188,6 +1216,14 @@ class DocumentsApi:
_query_params.append(('q', q)) _query_params.append(('q', q))
if tags is not None:
_query_params.append(('tags', tags))
if tags_match is not None:
_query_params.append(('tags_match', tags_match))
if limit is not None: if limit is not None:
_query_params.append(('limit', limit)) _query_params.append(('limit', limit))

View file

@ -3082,8 +3082,22 @@ export type ListDocumentsData = {
query?: { query?: {
/** /**
* Q * Q
*
* Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')
*/ */
q?: string | null; q?: string | null;
/**
* Tags
*
* Filter documents by tags
*/
tags?: Array<string> | null;
/**
* Tags Match
*
* How to match tags: 'any', 'all', 'any_strict', 'all_strict'
*/
tags_match?: string;
/** /**
* Limit * Limit
*/ */

View file

@ -131,6 +131,51 @@ hindsight document delete my-bank meeting-2024-03-15
Deleting a document permanently removes all memories extracted from it. This action cannot be undone. Deleting a document permanently removes all memories extracted from it. This action cannot be undone.
::: :::
## List Documents
List documents in a bank with optional filtering by ID and tags.
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={documentsPy} section="document-list" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={documentsMjs} section="document-list" language="javascript" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
# List all documents
hindsight document list my-bank
# Filter by ID substring
hindsight document list my-bank --q report
# Filter by tags
hindsight document list my-bank --tags team-a --tags team-b
```
</TabItem>
</Tabs>
### Filtering Options
| Parameter | Description |
|---|---|
| `q` | Case-insensitive substring match on document ID. `report` matches `report-2024`, `annual-report`, etc. |
| `tags` | Filter by document tags. Accepts multiple values. |
| `tags_match` | How to match tags (default: `any_strict`). See below. |
| `limit` / `offset` | Pagination. Default limit is 100. |
**`tags_match` modes:**
| Mode | Behaviour |
|---|---|
| `any_strict` *(default)* | Document must have **at least one** of the specified tags. Untagged docs excluded. |
| `any` | Same as `any_strict` but also includes untagged documents. |
| `all_strict` | Document must have **all** specified tags. Untagged docs excluded. |
| `all` | Same as `all_strict` but also includes untagged documents. |
## Document Response Format ## Document Response Format
```json ```json

View file

@ -44,6 +44,47 @@ await client.retain('my-bank', 'Project deadline: April 15 (extended)', {
// [/docs:document-update] // [/docs:document-update]
// [docs:document-list]
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
// List all documents
const { data: allDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' }
});
console.log(`Total documents: ${allDocs.total}`);
// Filter by document ID substring
const { data: reportDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { q: 'report' }
});
// Filter by tags — only docs tagged with "team-a" (untagged excluded)
const { data: taggedDocs } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { tags: ['team-a'], tags_match: 'any_strict' }
});
// Combine ID search and tags
const { data: filtered } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { q: 'meeting', tags: ['team-a', 'team-b'], tags_match: 'all_strict' }
});
// Paginate
const { data: page } = await sdk.listDocuments({
client: apiClient,
path: { bank_id: 'my-bank' },
query: { limit: 20, offset: 40 }
});
console.log(`Page items: ${page.items.length}`);
// [/docs:document-list]
// [docs:document-get] // [docs:document-get]
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' })); const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));

View file

@ -56,6 +56,45 @@ client.retain(
# [/docs:document-update] # [/docs:document-update]
# [docs:document-list]
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
async def list_documents_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# List all documents
result = await api.list_documents(bank_id="my-bank")
print(f"Total documents: {result.total}")
# Filter by document ID substring
result = await api.list_documents(bank_id="my-bank", q="report")
# Filter by tags — only docs tagged with "team-a" (untagged excluded)
result = await api.list_documents(
bank_id="my-bank",
tags=["team-a"],
tags_match="any_strict",
)
# Combine ID search and tags
result = await api.list_documents(
bank_id="my-bank",
q="meeting",
tags=["team-a", "team-b"],
tags_match="all_strict", # must have both tags
)
# Paginate
result = await api.list_documents(bank_id="my-bank", limit=20, offset=40)
print(f"Page items: {len(result.items)}")
asyncio.run(list_documents_example())
# [/docs:document-list]
# [docs:document-get] # [docs:document-get]
import asyncio import asyncio
from hindsight_client_api import ApiClient, Configuration from hindsight_client_api import ApiClient, Configuration

View file

@ -1750,8 +1750,43 @@
"type": "null" "type": "null"
} }
], ],
"description": "Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')",
"title": "Q" "title": "Q"
} },
"description": "Case-insensitive substring filter on document ID (e.g. 'report' matches 'report-2024')"
},
{
"name": "tags",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Filter documents by tags",
"title": "Tags"
},
"description": "Filter documents by tags"
},
{
"name": "tags_match",
"in": "query",
"required": false,
"schema": {
"type": "string",
"description": "How to match tags: 'any', 'all', 'any_strict', 'all_strict'",
"default": "any_strict",
"title": "Tags Match"
},
"description": "How to match tags: 'any', 'all', 'any_strict', 'all_strict'"
}, },
{ {
"name": "limit", "name": "limit",