feat: change tags for a document (#517)

* feat: add update document tags endpoint with observation invalidation

Adds PATCH /v1/default/banks/{bank_id}/documents/{document_id} to change
tags on a document without re-processing content.

- Updates tags on the document and all associated memory units atomically
- Invalidates observations derived from the document's memory units
- Resets consolidated_at on the document's own units for re-consolidation
- Also resets consolidated_at on co-source memories from other documents
  that shared those observations (matching delete_document behavior)
- Triggers async consolidation when observations are invalidated
- 9 new tests covering all invalidation scenarios

UI: adds inline tag editor to the document detail panel in the control plane
Docs: new "Update Document Tags" section in documents.mdx with Python/JS examples

* refactor: simplify UpdateDocumentTagsResponse to {success: true}

* refactor: make PATCH /documents generic update_document endpoint

Renames update_document_tags → update_document (engine + HTTP + clients + UI).
Currently only tags are supported; the structure is open for future fields.
Tags are the only field with side effects (observation invalidation + re-consolidation).
This commit is contained in:
Nicolò Boschi 2026-03-07 09:00:13 +01:00 committed by GitHub
parent d2504ac5ed
commit 1b4ad7f435
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1928 additions and 9 deletions

View file

@ -1206,6 +1206,30 @@ class DocumentResponse(BaseModel):
tags: list[str] = FieldWithDefault(list, description="Tags associated with this document")
class UpdateDocumentRequest(BaseModel):
"""Request model for updating a document's mutable fields."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"tags": ["team-a", "team-b"],
}
}
)
tags: list[str] | None = Field(
default=None,
description="New tags for the document and its memory units. "
"Triggers observation invalidation and re-consolidation.",
)
class UpdateDocumentResponse(BaseModel):
"""Response model for update document endpoint."""
success: bool = True
class DeleteDocumentResponse(BaseModel):
"""Response model for delete document endpoint."""
@ -3304,6 +3328,55 @@ def _register_routes(app: FastAPI):
logger.error(f"Error in /v1/default/chunks/{chunk_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.patch(
"/v1/default/banks/{bank_id}/documents/{document_id:path}",
response_model=UpdateDocumentResponse,
summary="Update document",
description="Update mutable fields on a document without re-processing its content.\n\n"
"**Tags** (`tags`): Propagated to all associated memory units. Observations derived from "
"those units are invalidated and queued for re-consolidation under the new tags. "
"Co-source memories from other documents that shared those observations are also reset.\n\n"
"At least one field must be provided.",
operation_id="update_document",
tags=["Documents"],
)
async def api_update_document(
bank_id: str,
document_id: str,
body: UpdateDocumentRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""
Update mutable fields on a document without re-processing its content.
Args:
bank_id: Memory Bank ID (from path)
document_id: Document ID (from path)
body: Fields to update (tags, metadata, context)
"""
if body.tags is None:
raise HTTPException(status_code=422, detail="At least one field (tags) must be provided")
try:
result = await app.state.memory.update_document(
document_id,
bank_id,
tags=body.tags,
request_context=request_context,
)
if not result:
raise HTTPException(status_code=404, detail="Document not found")
return UpdateDocumentResponse(success=True)
except OperationValidationError as e:
raise HTTPException(status_code=e.status_code, detail=e.reason)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/documents/{document_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/documents/{document_id:path}",
response_model=DeleteDocumentResponse,

View file

@ -3440,6 +3440,140 @@ class MemoryEngine(MemoryEngineInterface):
return result
async def update_document(
self,
document_id: str,
bank_id: str,
*,
tags: list[str] | None = None,
request_context: "RequestContext",
) -> bool:
"""
Update mutable fields on a document without re-processing its content.
Tag changes propagate to all associated memory units and trigger observation
invalidation + re-consolidation (same semantics as delete_document):
- Observations referencing the document's memory units are deleted.
- The document's own units and any co-source memories from other documents
have consolidated_at reset so they are re-consolidated under the new tags.
Args:
document_id: Document ID to update
bank_id: Bank ID that owns the document
tags: New tags to apply to the document and all its memory units (optional)
request_context: Request context for authentication.
Returns:
True if the document was found and updated, False if not found
"""
await self._authenticate_tenant(request_context)
if self._operation_validator:
from hindsight_api.extensions import BankWriteContext
ctx = BankWriteContext(bank_id=bank_id, operation="update_document", request_context=request_context)
await self._validate_operation(self._operation_validator.validate_bank_write(ctx))
pool = await self._get_pool()
invalidated_obs = 0
async with acquire_with_retry(pool) as conn:
async with conn.transaction():
set_parts: list[str] = ["updated_at = now()"]
params: list[Any] = []
p = 1
if tags is not None:
set_parts.append(f"tags = ${p}")
params.append(tags)
p += 1
params.extend([document_id, bank_id])
doc_id_found = await conn.fetchval(
f"""
UPDATE {fq_table("documents")}
SET {", ".join(set_parts)}
WHERE id = ${p} AND bank_id = ${p + 1}
RETURNING id
""",
*params,
)
if not doc_id_found:
return False
if tags is not None:
unit_rows = await conn.fetch(
f"SELECT id FROM {fq_table('memory_units')} WHERE document_id = $1 AND fact_type IN ('experience', 'world')",
document_id,
)
unit_ids = [str(row["id"]) for row in unit_rows]
await conn.execute(
f"UPDATE {fq_table('memory_units')} SET tags = $1 WHERE document_id = $2",
tags,
document_id,
)
if unit_ids:
import uuid as uuid_module
unit_uuids = [uuid_module.UUID(uid) for uid in unit_ids]
unit_uuid_set = {str(u) for u in unit_uuids}
affected_obs = await conn.fetch(
f"""
SELECT id, source_memory_ids FROM {fq_table("memory_units")}
WHERE bank_id = $1
AND fact_type = 'observation'
AND source_memory_ids && $2::uuid[]
""",
bank_id,
unit_uuids,
)
if affected_obs:
obs_ids = [obs["id"] for obs in affected_obs]
seen: set[str] = set()
other_source_uuids: list[uuid_module.UUID] = []
for obs in affected_obs:
for src_id in obs["source_memory_ids"] or []:
src_str = str(src_id)
if src_str not in unit_uuid_set and src_str not in seen:
other_source_uuids.append(src_id)
seen.add(src_str)
await conn.execute(
f"DELETE FROM {fq_table('memory_units')} WHERE id = ANY($1::uuid[])",
obs_ids,
)
await conn.execute(
f"""
UPDATE {fq_table("memory_units")}
SET consolidated_at = NULL
WHERE id = ANY($1::uuid[])
AND fact_type IN ('experience', 'world')
""",
unit_uuids,
)
if other_source_uuids:
await conn.execute(
f"""
UPDATE {fq_table("memory_units")}
SET consolidated_at = NULL
WHERE id = ANY($1::uuid[])
AND fact_type IN ('experience', 'world')
""",
other_source_uuids,
)
invalidated_obs = len(obs_ids)
logger.info(
f"[OBSERVATIONS] Deleted {invalidated_obs} observations, reset "
f"{len(unit_ids)} document source memories and "
f"{len(other_source_uuids)} co-source memories for re-consolidation "
f"after document update on '{document_id}' in bank {bank_id}"
)
if invalidated_obs > 0:
await self.submit_async_consolidation(bank_id=bank_id, request_context=request_context)
return True
async def delete_memory_unit(
self,
unit_id: str,

View file

@ -455,3 +455,281 @@ class TestClearObservationsForMemory:
assert await _get_consolidated_at(conn, m2) is None
await memory.delete_bank(bank_id, request_context=request_context)
# ---------------------------------------------------------------------------
# Tests: update_document
# ---------------------------------------------------------------------------
async def _insert_document_with_memories(
conn, bank_id: str, doc_id: str, memories: list[tuple[str, str]]
) -> list[uuid.UUID]:
"""Insert a document and attach memory units to it. Returns list of memory UUIDs."""
await conn.execute(
"""
INSERT INTO documents (id, bank_id, original_text, content_hash, created_at, updated_at)
VALUES ($1, $2, 'some doc', 'hash123', NOW(), NOW())
""",
doc_id,
bank_id,
)
mem_ids = []
for text, fact_type in memories:
mem_id = uuid.uuid4()
await conn.execute(
"""
INSERT INTO memory_units (id, bank_id, text, fact_type, event_date, document_id, created_at, updated_at, consolidated_at)
VALUES ($1, $2, $3, $4, NOW(), $5, NOW(), NOW(), NOW())
""",
mem_id,
bank_id,
text,
fact_type,
doc_id,
)
mem_ids.append(mem_id)
return mem_ids
class TestUpdateDocumentTagsObservationCleanup:
@pytest.mark.asyncio
async def test_update_tags_returns_updated_document(
self, memory: MemoryEngine, request_context: RequestContext
):
"""update_document returns the updated document with new tags."""
bank_id = f"test-tag-update-basic-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
await _insert_document_with_memories(conn, bank_id, doc_id, [("Alice loves hiking.", "experience")])
result = await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
assert result is True
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_returns_none_for_missing_document(
self, memory: MemoryEngine, request_context: RequestContext
):
"""update_document returns False when document does not exist."""
bank_id = f"test-tag-update-missing-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
result = await memory.update_document(
"nonexistent-doc", bank_id, tags=["tag"], request_context=request_context
)
assert result is False
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_propagates_to_memory_units(
self, memory: MemoryEngine, request_context: RequestContext
):
"""Changing document tags also updates all associated memory unit tags."""
bank_id = f"test-tag-update-propagate-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience"), ("Alice hikes weekly.", "world")]
)
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()):
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
async with pool.acquire() as conn:
for mem_id in mem_ids:
tags = await conn.fetchval(
"SELECT tags FROM memory_units WHERE id = $1", mem_id
)
assert list(tags) == ["new-tag"], f"Memory unit {mem_id} should have updated tags"
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_invalidates_observations(
self, memory: MemoryEngine, request_context: RequestContext
):
"""Observations referencing the document's memory units are deleted on tag change."""
bank_id = f"test-tag-update-obs-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
obs_id = await _insert_observation(conn, bank_id, "Alice is a hiker.", mem_ids)
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()):
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
async with pool.acquire() as conn:
obs_ids = await _get_observation_ids(conn, bank_id)
assert str(obs_id) not in obs_ids, "Observation should have been invalidated"
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_resets_consolidated_at_on_affected_units(
self, memory: MemoryEngine, request_context: RequestContext
):
"""Affected memory units get consolidated_at reset for re-consolidation under new tags."""
bank_id = f"test-tag-update-reset-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
obs_id = await _insert_observation(conn, bank_id, "Alice is a hiker.", mem_ids)
# Verify memory starts consolidated
assert await _get_consolidated_at(conn, mem_ids[0]) is not None
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()):
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
async with pool.acquire() as conn:
consolidated_at = await _get_consolidated_at(conn, mem_ids[0])
assert consolidated_at is None, "Memory unit should be reset for re-consolidation"
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_triggers_consolidation_when_observations_invalidated(
self, memory: MemoryEngine, request_context: RequestContext
):
"""submit_async_consolidation is called when observations are invalidated."""
bank_id = f"test-tag-update-cons-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
await _insert_observation(conn, bank_id, "Alice is a hiker.", mem_ids)
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()) as mock_consolidate:
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
mock_consolidate.assert_awaited_once()
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_no_consolidation_when_no_observations(
self, memory: MemoryEngine, request_context: RequestContext
):
"""submit_async_consolidation is NOT called when no observations are invalidated."""
bank_id = f"test-tag-update-nocons-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
# No observations inserted
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()) as mock_consolidate:
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
mock_consolidate.assert_not_awaited()
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_resets_co_source_memories_from_other_documents(
self, memory: MemoryEngine, request_context: RequestContext
):
"""Co-source memories from other documents that shared an invalidated observation are also reset."""
bank_id = f"test-tag-update-cosource-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
doc_mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
# Unrelated memory from another document — co-sourced in the same observation
other_mem = await _insert_memory(conn, bank_id, "Alice also rock-climbs.")
obs_id = await _insert_observation(
conn, bank_id, "Alice loves outdoor activities.", doc_mem_ids + [other_mem]
)
# Verify other_mem starts consolidated
assert await _get_consolidated_at(conn, other_mem) is not None
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()):
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
async with pool.acquire() as conn:
obs_ids = await _get_observation_ids(conn, bank_id)
assert str(obs_id) not in obs_ids, "Observation should have been invalidated"
# other_mem (co-source from another document) must also be reset
consolidated_at = await _get_consolidated_at(conn, other_mem)
assert consolidated_at is None, "Co-source memory from other document should be reset"
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_tags_does_not_affect_unrelated_observations(
self, memory: MemoryEngine, request_context: RequestContext
):
"""Observations referencing memories from a different document are not affected."""
bank_id = f"test-tag-update-unrelated-{uuid.uuid4().hex[:8]}"
await _ensure_bank(memory, bank_id, request_context)
pool = await memory._get_pool()
async with pool.acquire() as conn:
doc_id = f"doc-{uuid.uuid4().hex[:8]}"
mem_ids = await _insert_document_with_memories(
conn, bank_id, doc_id, [("Alice loves hiking.", "experience")]
)
# Unrelated memory not in the document
unrelated = await _insert_memory(conn, bank_id, "Bob likes cycling.")
unrelated_obs_id = await _insert_observation(
conn, bank_id, "Bob is a cyclist.", [unrelated]
)
with patch.object(memory, "submit_async_consolidation", new=AsyncMock()):
await memory.update_document(
doc_id, bank_id, tags=["new-tag"], request_context=request_context
)
async with pool.acquire() as conn:
obs_ids = await _get_observation_ids(conn, bank_id)
assert str(unrelated_obs_id) in obs_ids, "Unrelated observation should remain untouched"
await memory.delete_bank(bank_id, request_context=request_context)

View file

@ -1435,6 +1435,61 @@ paths:
summary: Get document details
tags:
- Documents
patch:
description: |-
Update mutable fields on a document without re-processing its content.
**Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.
At least one field must be provided.
operationId: update_document
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: document_id
required: true
schema:
title: Document Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateDocumentRequest'
required: true
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateDocumentResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Update document
tags:
- Documents
/v1/default/banks/{bank_id}/tags:
get:
description: "List all unique tags in a memory bank with usage counts. Supports\
@ -4791,6 +4846,29 @@ components:
required:
- disposition
title: UpdateDispositionRequest
UpdateDocumentRequest:
description: Request model for updating a document's mutable fields.
example:
tags:
- team-a
- team-b
properties:
tags:
items:
type: string
nullable: true
type: array
title: UpdateDocumentRequest
UpdateDocumentResponse:
description: Response model for update document endpoint.
example:
success: true
properties:
success:
default: true
title: Success
type: boolean
title: UpdateDocumentResponse
UpdateMentalModelRequest:
description: Request model for updating a mental model.
example:

View file

@ -591,3 +591,144 @@ func (a *DocumentsAPIService) ListDocumentsExecute(r ApiListDocumentsRequest) (*
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiUpdateDocumentRequest struct {
ctx context.Context
ApiService *DocumentsAPIService
bankId string
documentId string
updateDocumentRequest *UpdateDocumentRequest
authorization *string
}
func (r ApiUpdateDocumentRequest) UpdateDocumentRequest(updateDocumentRequest UpdateDocumentRequest) ApiUpdateDocumentRequest {
r.updateDocumentRequest = &updateDocumentRequest
return r
}
func (r ApiUpdateDocumentRequest) Authorization(authorization string) ApiUpdateDocumentRequest {
r.authorization = &authorization
return r
}
func (r ApiUpdateDocumentRequest) Execute() (*UpdateDocumentResponse, *http.Response, error) {
return r.ApiService.UpdateDocumentExecute(r)
}
/*
UpdateDocument Update document
Update mutable fields on a document without re-processing its content.
**Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.
At least one field must be provided.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param documentId
@return ApiUpdateDocumentRequest
*/
func (a *DocumentsAPIService) UpdateDocument(ctx context.Context, bankId string, documentId string) ApiUpdateDocumentRequest {
return ApiUpdateDocumentRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
documentId: documentId,
}
}
// Execute executes the request
// @return UpdateDocumentResponse
func (a *DocumentsAPIService) UpdateDocumentExecute(r ApiUpdateDocumentRequest) (*UpdateDocumentResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPatch
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *UpdateDocumentResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DocumentsAPIService.UpdateDocument")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/documents/{document_id}"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"document_id"+"}", url.PathEscape(parameterValueToString(r.documentId, "documentId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.updateDocumentRequest == nil {
return localVarReturnValue, nil, reportError("updateDocumentRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
// body params
localVarPostBody = r.updateDocumentRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}

View file

@ -0,0 +1,127 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.16
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the UpdateDocumentRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UpdateDocumentRequest{}
// UpdateDocumentRequest Request model for updating a document's mutable fields.
type UpdateDocumentRequest struct {
Tags []string `json:"tags,omitempty"`
}
// NewUpdateDocumentRequest instantiates a new UpdateDocumentRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUpdateDocumentRequest() *UpdateDocumentRequest {
this := UpdateDocumentRequest{}
return &this
}
// NewUpdateDocumentRequestWithDefaults instantiates a new UpdateDocumentRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUpdateDocumentRequestWithDefaults() *UpdateDocumentRequest {
this := UpdateDocumentRequest{}
return &this
}
// GetTags returns the Tags field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateDocumentRequest) GetTags() []string {
if o == nil {
var ret []string
return ret
}
return o.Tags
}
// GetTagsOk returns a tuple with the Tags field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateDocumentRequest) GetTagsOk() ([]string, bool) {
if o == nil || IsNil(o.Tags) {
return nil, false
}
return o.Tags, true
}
// HasTags returns a boolean if a field has been set.
func (o *UpdateDocumentRequest) HasTags() bool {
if o != nil && !IsNil(o.Tags) {
return true
}
return false
}
// SetTags gets a reference to the given []string and assigns it to the Tags field.
func (o *UpdateDocumentRequest) SetTags(v []string) {
o.Tags = v
}
func (o UpdateDocumentRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UpdateDocumentRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if o.Tags != nil {
toSerialize["tags"] = o.Tags
}
return toSerialize, nil
}
type NullableUpdateDocumentRequest struct {
value *UpdateDocumentRequest
isSet bool
}
func (v NullableUpdateDocumentRequest) Get() *UpdateDocumentRequest {
return v.value
}
func (v *NullableUpdateDocumentRequest) Set(val *UpdateDocumentRequest) {
v.value = val
v.isSet = true
}
func (v NullableUpdateDocumentRequest) IsSet() bool {
return v.isSet
}
func (v *NullableUpdateDocumentRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUpdateDocumentRequest(val *UpdateDocumentRequest) *NullableUpdateDocumentRequest {
return &NullableUpdateDocumentRequest{value: val, isSet: true}
}
func (v NullableUpdateDocumentRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUpdateDocumentRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,130 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.16
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the UpdateDocumentResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UpdateDocumentResponse{}
// UpdateDocumentResponse Response model for update document endpoint.
type UpdateDocumentResponse struct {
Success *bool `json:"success,omitempty"`
}
// NewUpdateDocumentResponse instantiates a new UpdateDocumentResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUpdateDocumentResponse() *UpdateDocumentResponse {
this := UpdateDocumentResponse{}
var success bool = true
this.Success = &success
return &this
}
// NewUpdateDocumentResponseWithDefaults instantiates a new UpdateDocumentResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUpdateDocumentResponseWithDefaults() *UpdateDocumentResponse {
this := UpdateDocumentResponse{}
var success bool = true
this.Success = &success
return &this
}
// GetSuccess returns the Success field value if set, zero value otherwise.
func (o *UpdateDocumentResponse) GetSuccess() bool {
if o == nil || IsNil(o.Success) {
var ret bool
return ret
}
return *o.Success
}
// GetSuccessOk returns a tuple with the Success field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *UpdateDocumentResponse) GetSuccessOk() (*bool, bool) {
if o == nil || IsNil(o.Success) {
return nil, false
}
return o.Success, true
}
// HasSuccess returns a boolean if a field has been set.
func (o *UpdateDocumentResponse) HasSuccess() bool {
if o != nil && !IsNil(o.Success) {
return true
}
return false
}
// SetSuccess gets a reference to the given bool and assigns it to the Success field.
func (o *UpdateDocumentResponse) SetSuccess(v bool) {
o.Success = &v
}
func (o UpdateDocumentResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UpdateDocumentResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.Success) {
toSerialize["success"] = o.Success
}
return toSerialize, nil
}
type NullableUpdateDocumentResponse struct {
value *UpdateDocumentResponse
isSet bool
}
func (v NullableUpdateDocumentResponse) Get() *UpdateDocumentResponse {
return v.value
}
func (v *NullableUpdateDocumentResponse) Set(val *UpdateDocumentResponse) {
v.value = val
v.isSet = true
}
func (v NullableUpdateDocumentResponse) IsSet() bool {
return v.isSet
}
func (v *NullableUpdateDocumentResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUpdateDocumentResponse(val *UpdateDocumentResponse) *NullableUpdateDocumentResponse {
return &NullableUpdateDocumentResponse{value: val, isSet: true}
}
func (v NullableUpdateDocumentResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUpdateDocumentResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -88,6 +88,8 @@ hindsight_client_api/models/token_usage.py
hindsight_client_api/models/tool_calls_include_options.py
hindsight_client_api/models/update_directive_request.py
hindsight_client_api/models/update_disposition_request.py
hindsight_client_api/models/update_document_request.py
hindsight_client_api/models/update_document_response.py
hindsight_client_api/models/update_mental_model_request.py
hindsight_client_api/models/update_webhook_request.py
hindsight_client_api/models/validation_error.py

View file

@ -113,6 +113,8 @@ from hindsight_client_api.models.token_usage import TokenUsage
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_document_request import UpdateDocumentRequest
from hindsight_client_api.models.update_document_response import UpdateDocumentResponse
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError

View file

@ -23,6 +23,8 @@ from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
from hindsight_client_api.models.update_document_request import UpdateDocumentRequest
from hindsight_client_api.models.update_document_response import UpdateDocumentResponse
from hindsight_client_api.api_client import ApiClient, RequestSerialized
from hindsight_client_api.api_response import ApiResponse
@ -1268,3 +1270,324 @@ class DocumentsApi:
)
@validate_call
async def update_document(
self,
bank_id: StrictStr,
document_id: StrictStr,
update_document_request: UpdateDocumentRequest,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> UpdateDocumentResponse:
"""Update document
Update mutable fields on a document without re-processing its content. **Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset. At least one field must be provided.
:param bank_id: (required)
:type bank_id: str
:param document_id: (required)
:type document_id: str
:param update_document_request: (required)
:type update_document_request: UpdateDocumentRequest
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._update_document_serialize(
bank_id=bank_id,
document_id=document_id,
update_document_request=update_document_request,
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "UpdateDocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def update_document_with_http_info(
self,
bank_id: StrictStr,
document_id: StrictStr,
update_document_request: UpdateDocumentRequest,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[UpdateDocumentResponse]:
"""Update document
Update mutable fields on a document without re-processing its content. **Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset. At least one field must be provided.
:param bank_id: (required)
:type bank_id: str
:param document_id: (required)
:type document_id: str
:param update_document_request: (required)
:type update_document_request: UpdateDocumentRequest
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._update_document_serialize(
bank_id=bank_id,
document_id=document_id,
update_document_request=update_document_request,
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "UpdateDocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def update_document_without_preload_content(
self,
bank_id: StrictStr,
document_id: StrictStr,
update_document_request: UpdateDocumentRequest,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Update document
Update mutable fields on a document without re-processing its content. **Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset. At least one field must be provided.
:param bank_id: (required)
:type bank_id: str
:param document_id: (required)
:type document_id: str
:param update_document_request: (required)
:type update_document_request: UpdateDocumentRequest
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._update_document_serialize(
bank_id=bank_id,
document_id=document_id,
update_document_request=update_document_request,
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "UpdateDocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _update_document_serialize(
self,
bank_id,
document_id,
update_document_request,
authorization,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if bank_id is not None:
_path_params['bank_id'] = bank_id
if document_id is not None:
_path_params['document_id'] = document_id
# process the query parameters
# process the header parameters
if authorization is not None:
_header_params['authorization'] = authorization
# process the form parameters
# process the body parameter
if update_document_request is not None:
_body_params = update_document_request
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# set the HTTP header `Content-Type`
if _content_type:
_header_params['Content-Type'] = _content_type
else:
_default_content_type = (
self.api_client.select_header_content_type(
[
'application/json'
]
)
)
if _default_content_type is not None:
_header_params['Content-Type'] = _default_content_type
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='PATCH',
resource_path='/v1/default/banks/{bank_id}/documents/{document_id}',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)

View file

@ -87,6 +87,8 @@ from hindsight_client_api.models.token_usage import TokenUsage
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_document_request import UpdateDocumentRequest
from hindsight_client_api.models.update_document_response import UpdateDocumentResponse
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError

View file

@ -0,0 +1,92 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.16
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class UpdateDocumentRequest(BaseModel):
"""
Request model for updating a document's mutable fields.
""" # noqa: E501
tags: Optional[List[StrictStr]] = None
__properties: ClassVar[List[str]] = ["tags"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of UpdateDocumentRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# set to None if tags (nullable) is None
# and model_fields_set contains the field
if self.tags is None and "tags" in self.model_fields_set:
_dict['tags'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of UpdateDocumentRequest from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"tags": obj.get("tags")
})
return _obj

View file

@ -0,0 +1,87 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.16
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictBool
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class UpdateDocumentResponse(BaseModel):
"""
Response model for update document endpoint.
""" # noqa: E501
success: Optional[StrictBool] = True
__properties: ClassVar[List[str]] = ["success"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of UpdateDocumentResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of UpdateDocumentResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"success": obj.get("success") if obj.get("success") is not None else True
})
return _obj

View file

@ -161,6 +161,9 @@ import type {
UpdateDirectiveData,
UpdateDirectiveErrors,
UpdateDirectiveResponses,
UpdateDocumentData,
UpdateDocumentErrors,
UpdateDocumentResponses,
UpdateMentalModelData,
UpdateMentalModelErrors,
UpdateMentalModelResponses,
@ -679,6 +682,31 @@ export const getDocument = <ThrowOnError extends boolean = false>(
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/documents/{document_id}", ...options });
/**
* Update document
*
* Update mutable fields on a document without re-processing its content.
*
* **Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.
*
* At least one field must be provided.
*/
export const updateDocument = <ThrowOnError extends boolean = false>(
options: Options<UpdateDocumentData, ThrowOnError>,
) =>
(options.client ?? client).patch<
UpdateDocumentResponses,
UpdateDocumentErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/documents/{document_id}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
/**
* List tags
*

View file

@ -2081,6 +2081,32 @@ export type UpdateDispositionRequest = {
disposition: DispositionTraits;
};
/**
* UpdateDocumentRequest
*
* Request model for updating a document's mutable fields.
*/
export type UpdateDocumentRequest = {
/**
* Tags
*
* New tags for the document and its memory units. Triggers observation invalidation and re-consolidation.
*/
tags?: Array<string> | null;
};
/**
* UpdateDocumentResponse
*
* Response model for update document endpoint.
*/
export type UpdateDocumentResponse = {
/**
* Success
*/
success?: boolean;
};
/**
* UpdateMentalModelRequest
*
@ -3535,6 +3561,48 @@ export type GetDocumentResponses = {
export type GetDocumentResponse =
GetDocumentResponses[keyof GetDocumentResponses];
export type UpdateDocumentData = {
body: UpdateDocumentRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Document Id
*/
document_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/documents/{document_id}";
};
export type UpdateDocumentErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateDocumentError =
UpdateDocumentErrors[keyof UpdateDocumentErrors];
export type UpdateDocumentResponses = {
/**
* Successful Response
*/
200: UpdateDocumentResponse;
};
export type UpdateDocumentResponse2 =
UpdateDocumentResponses[keyof UpdateDocumentResponses];
export type ListTagsData = {
body?: never;
headers?: {

View file

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
import { sdk, lowLevelClient, DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function GET(
request: NextRequest,
@ -26,6 +26,42 @@ export async function GET(
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ documentId: string }> }
) {
try {
const { documentId } = await params;
const searchParams = request.nextUrl.searchParams;
const bankId = searchParams.get("bank_id");
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const body = await request.json();
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/documents/${documentId}`,
{
method: "PATCH",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
return NextResponse.json(error, { status: response.status });
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error updating document tags:", error);
return NextResponse.json({ error: "Failed to update document tags" }, { status: 500 });
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ documentId: string }> }

View file

@ -23,7 +23,16 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { X, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
import {
X,
Trash2,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Pencil,
Check,
} from "lucide-react";
const ITEMS_PER_PAGE = 50;
@ -44,6 +53,11 @@ export function DocumentsView() {
const [loadingDocument, setLoadingDocument] = useState(false);
const [deletingDocumentId, setDeletingDocumentId] = useState<string | null>(null);
// Tag editing state
const [editingTags, setEditingTags] = useState(false);
const [tagInput, setTagInput] = useState("");
const [savingTags, setSavingTags] = useState(false);
// Delete confirmation dialog state
const [documentToDelete, setDocumentToDelete] = useState<{
id: string;
@ -85,6 +99,8 @@ export function DocumentsView() {
setLoadingDocument(true);
setSelectedDocument({ id: documentId }); // Set placeholder to show loading
setEditingTags(false);
setTagInput("");
try {
const doc: any = await client.getDocument(documentId, currentBank);
@ -133,6 +149,41 @@ export function DocumentsView() {
setDocumentToDelete({ id: documentId, memoryCount });
};
const startEditTags = () => {
setTagInput((selectedDocument?.tags ?? []).join(", "));
setEditingTags(true);
};
const cancelEditTags = () => {
setEditingTags(false);
setTagInput("");
};
const saveDocumentTags = async () => {
if (!currentBank || !selectedDocument) return;
const newTags = tagInput
.split(",")
.map((t) => t.trim())
.filter((t) => t.length > 0);
setSavingTags(true);
try {
await client.updateDocument(selectedDocument.id, currentBank, newTags);
setSelectedDocument({ ...selectedDocument, tags: newTags });
// Update tags in the documents list too
setDocuments((prev) =>
prev.map((d) => (d.id === selectedDocument.id ? { ...d, tags: newTags } : d))
);
setEditingTags(false);
setTagInput("");
} catch (error) {
console.error("Error updating document tags:", error);
} finally {
setSavingTags(false);
}
};
// Auto-load documents when component mounts or bank changes
useEffect(() => {
if (currentBank) {
@ -417,11 +468,64 @@ export function DocumentsView() {
)}
{/* Tags */}
{selectedDocument.tags && selectedDocument.tags.length > 0 && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
Tags
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-bold text-muted-foreground uppercase">Tags</div>
{!editingTags && (
<Button
variant="ghost"
size="sm"
onClick={startEditTags}
className="h-6 px-2 gap-1 text-xs"
>
<Pencil className="h-3 w-3" />
Edit
</Button>
)}
</div>
{editingTags ? (
<div className="space-y-2">
<Input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
placeholder="tag1, tag2, tag3"
className="text-sm h-8"
onKeyDown={(e) => {
if (e.key === "Enter") saveDocumentTags();
if (e.key === "Escape") cancelEditTags();
}}
autoFocus
/>
<p className="text-xs text-muted-foreground">
Comma-separated. Leave empty to remove all tags.
</p>
<div className="flex gap-2">
<Button
size="sm"
onClick={saveDocumentTags}
disabled={savingTags}
className="h-7 px-3 gap-1 text-xs"
>
{savingTags ? (
<span className="animate-spin"></span>
) : (
<Check className="h-3 w-3" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={cancelEditTags}
disabled={savingTags}
className="h-7 px-3 gap-1 text-xs"
>
<X className="h-3 w-3" />
Cancel
</Button>
</div>
</div>
) : selectedDocument.tags && selectedDocument.tags.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedDocument.tags.map((tag: string, i: number) => (
<span
@ -432,8 +536,10 @@ export function DocumentsView() {
</span>
))}
</div>
</div>
)}
) : (
<div className="text-sm text-muted-foreground">No tags</div>
)}
</div>
{/* Delete Button */}
<div className="pt-2 border-t border-border">

View file

@ -336,6 +336,20 @@ export class ControlPlaneClient {
return this.fetchApi(`/api/documents/${documentId}?bank_id=${bankId}`);
}
/**
* Update tags on a document and its associated memory units
*/
async updateDocument(documentId: string, bankId: string, tags: string[]) {
return this.fetchApi<{ success: boolean }>(
`/api/documents/${encodeURIComponent(documentId)}?bank_id=${bankId}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tags }),
}
);
}
/**
* Delete document and all its associated memory units
*/

View file

@ -50,7 +50,9 @@
".next-64080/types/**/*.ts",
".next-64080/dev/types/**/*.ts",
".next-50432/types/**/*.ts",
".next-50432/dev/types/**/*.ts"
".next-50432/dev/types/**/*.ts",
".next-54840/types/**/*.ts",
".next-54840/dev/types/**/*.ts"
],
"exclude": [
"node_modules"

View file

@ -107,6 +107,34 @@ hindsight document get my-bank meeting-2024-03-15
</TabItem>
</Tabs>
## Update Document
Update mutable fields on an existing document without re-processing the content. Currently supports updating `tags`.
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={documentsPy} section="document-update" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={documentsMjs} section="document-update" language="javascript" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
# Replace tags with new values
hindsight document update-tags my-bank meeting-2024-03-15 --tags team-a --tags team-b
# Remove all tags
hindsight document update-tags my-bank meeting-2024-03-15
```
</TabItem>
</Tabs>
:::info Observations are re-consolidated
When tags change, any consolidated observations derived from the document's memories are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.
:::
## Delete Document
Remove a document and all its associated memories:

View file

@ -103,6 +103,29 @@ console.log(`Created: ${doc.created_at}`);
// [/docs:document-get]
// [docs:document-update]
// Fix tags on a document retained with the wrong scope
const { data: updateResult, error: updateError } = await sdk.updateDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' },
body: { tags: ['team-a', 'team-b'] }
});
if (updateError) {
throw new Error(`Failed to update tags: ${JSON.stringify(updateError)}`);
}
console.log(`Updated: ${updateResult.success}`);
// Remove all tags (make document visible everywhere)
await sdk.updateDocument({
client: apiClient,
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15-section-1' },
body: { tags: [] }
});
// [/docs:document-update]
// [docs:document-delete]
// Delete document and all its memories
const { data: deleteResult } = await sdk.deleteDocument({

View file

@ -120,6 +120,35 @@ asyncio.run(get_document_example())
# [/docs:document-get]
# [docs:document-update]
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi
from hindsight_client_api.models import UpdateDocumentRequest
async def update_document_example():
config = Configuration(host="http://localhost:8888")
api_client = ApiClient(config)
api = DocumentsApi(api_client)
# Fix tags on a document retained with the wrong scope
result = await api.update_document(
bank_id="my-bank",
document_id="meeting-2024-03-15",
update_document_request=UpdateDocumentRequest(tags=["team-a", "team-b"]),
)
print(f"Updated: {result.success}")
# Remove all tags (make document visible everywhere)
await api.update_document(
bank_id="my-bank",
document_id="meeting-2024-03-15",
update_document_request=UpdateDocumentRequest(tags=[]),
)
asyncio.run(update_document_example())
# [/docs:document-update]
# [docs:document-delete]
from hindsight_client_api import ApiClient, Configuration
from hindsight_client_api.api import DocumentsApi

View file

@ -2048,6 +2048,82 @@
}
}
},
"patch": {
"tags": [
"Documents"
],
"summary": "Update document",
"description": "Update mutable fields on a document without re-processing its content.\n\n**Tags** (`tags`): Propagated to all associated memory units. Observations derived from those units are invalidated and queued for re-consolidation under the new tags. Co-source memories from other documents that shared those observations are also reset.\n\nAt least one field must be provided.",
"operationId": "update_document",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "document_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Document Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateDocumentRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateDocumentResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"Documents"
@ -7503,6 +7579,46 @@
"title": "UpdateDispositionRequest",
"description": "Request model for updating disposition traits."
},
"UpdateDocumentRequest": {
"properties": {
"tags": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Tags",
"description": "New tags for the document and its memory units. Triggers observation invalidation and re-consolidation."
}
},
"type": "object",
"title": "UpdateDocumentRequest",
"description": "Request model for updating a document's mutable fields.",
"example": {
"tags": [
"team-a",
"team-b"
]
}
},
"UpdateDocumentResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success",
"default": true
}
},
"type": "object",
"title": "UpdateDocumentResponse",
"description": "Response model for update document endpoint."
},
"UpdateMentalModelRequest": {
"properties": {
"name": {