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:
parent
d2504ac5ed
commit
1b4ad7f435
23 changed files with 1928 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
127
hindsight-clients/go/model_update_document_request.go
Normal file
127
hindsight-clients/go/model_update_document_request.go
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
130
hindsight-clients/go/model_update_document_response.go
Normal file
130
hindsight-clients/go/model_update_document_response.go
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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 }> }
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue