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:
parent
ecf609c8aa
commit
1d70abfe85
13 changed files with 490 additions and 17 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
174
hindsight-api/tests/test_list_documents.py
Normal file
174
hindsight-api/tests/test_list_documents.py
Normal 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)
|
||||||
|
|
@ -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())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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' }));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue