Skip to content

Commit 91e095a

Browse files
authored
fix(control-plane): show pending uploaded documents from server operations (#2420)
Render in-flight and failed file uploads in the Documents view by deriving them from the server's file_convert_retain operations — no client-side store or client-generated document ids. - surface document_id + original_filename on the operations list endpoint (already stored in the operation's result_metadata) - documents-view derives pending/failed rows from those operations, deduped against the real document list by document_id, and polls while in-flight - bridge the brief window where an operation reports completed before the document becomes visible in listDocuments, so the row never flickers Supersedes #2346 (client-side sessionStorage approach). Closes #2314.
1 parent 815d99f commit 91e095a

22 files changed

Lines changed: 382 additions & 25 deletions

File tree

hindsight-api-slim/hindsight_api/api/http.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2596,6 +2596,10 @@ class OperationResponse(BaseModel):
25962596
task_type: str
25972597
items_count: int
25982598
document_id: str | None = None
2599+
filename: str | None = Field(
2600+
default=None,
2601+
description="Original filename for file-conversion operations (file_convert_retain); null for other task types.",
2602+
)
25992603
created_at: str
26002604
updated_at: str | None = Field(
26012605
default=None,

hindsight-api-slim/hindsight_api/engine/memory_engine.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11447,7 +11447,8 @@ async def list_operations(
1144711447
"id": str(row["operation_id"]),
1144811448
"task_type": row["operation_type"],
1144911449
"items_count": result_metadata.get("items_count", 0),
11450-
"document_id": None,
11450+
"document_id": result_metadata.get("document_id"),
11451+
"filename": result_metadata.get("original_filename"),
1145111452
"created_at": row["created_at"].isoformat(),
1145211453
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
1145311454
"status": row["status"],
@@ -12432,6 +12433,7 @@ async def submit_async_file_retain(
1243212433
task_payload=task_payload,
1243312434
result_metadata={
1243412435
"original_filename": file.filename,
12436+
"document_id": item["document_id"],
1243512437
},
1243612438
dedupe_by_bank=False,
1243712439
)

hindsight-api-slim/tests/test_file_retain.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,59 @@ async def read(self):
617617
assert len(doc["original_text"]) > 0
618618

619619

620+
@pytest.mark.asyncio
621+
async def test_list_operations_surfaces_file_document_id_and_filename(memory_no_llm_verify, sample_txt_content):
622+
"""list_operations must expose document_id + filename for file_convert_retain ops.
623+
624+
The control plane derives its pending-upload rows from these fields (it
625+
matches an in-flight operation to the real document via document_id and
626+
labels the row with the original filename), so both must round-trip from
627+
the operation's result_metadata into the list response.
628+
"""
629+
from hindsight_api.models import RequestContext
630+
631+
bank_id = "test_file_op_fields_bank"
632+
context = RequestContext(internal=True)
633+
await memory_no_llm_verify.get_bank_profile(bank_id, request_context=context)
634+
635+
class MockFile:
636+
def __init__(self, content, filename, content_type):
637+
self.content = content
638+
self.filename = filename
639+
self.content_type = content_type
640+
641+
async def read(self):
642+
return self.content
643+
644+
file_items = [
645+
{
646+
"file": MockFile(sample_txt_content, "report.txt", "text/plain"),
647+
"document_id": "doc_op_fields",
648+
"context": None,
649+
"metadata": {},
650+
"tags": [],
651+
"timestamp": None,
652+
"parser": ["markitdown"],
653+
}
654+
]
655+
656+
await memory_no_llm_verify.submit_async_file_retain(
657+
bank_id=bank_id,
658+
file_items=file_items,
659+
document_tags=None,
660+
request_context=context,
661+
)
662+
663+
result = await memory_no_llm_verify.list_operations(
664+
bank_id, task_type="file_convert_retain", request_context=context
665+
)
666+
667+
file_ops = [op for op in result["operations"] if op["task_type"] == "file_convert_retain"]
668+
assert len(file_ops) == 1
669+
assert file_ops[0]["document_id"] == "doc_op_fields"
670+
assert file_ops[0]["filename"] == "report.txt"
671+
672+
620673
@pytest.mark.asyncio
621674
async def test_async_file_retain_serializes_datetime_timestamp(memory_no_llm_verify, sample_txt_content):
622675
"""Async file retain should accept Python datetimes in task payloads."""

hindsight-clients/go/api/openapi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6982,6 +6982,9 @@ components:
69826982
document_id:
69836983
nullable: true
69846984
type: string
6985+
filename:
6986+
nullable: true
6987+
type: string
69856988
created_at:
69866989
title: Created At
69876990
type: string

hindsight-clients/go/model_operation_response.go

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hindsight-clients/python/hindsight_client_api/models/operation_response.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ class OperationResponse(BaseModel):
3131
task_type: StrictStr
3232
items_count: StrictInt
3333
document_id: Optional[StrictStr] = None
34+
filename: Optional[StrictStr] = None
3435
created_at: StrictStr
3536
updated_at: Optional[StrictStr] = None
3637
status: StrictStr
3738
error_message: Optional[StrictStr]
3839
retry_count: Optional[StrictInt] = None
3940
next_retry_at: Optional[StrictStr] = None
4041
progress: Optional[OperationProgress] = None
41-
__properties: ClassVar[List[str]] = ["id", "task_type", "items_count", "document_id", "created_at", "updated_at", "status", "error_message", "retry_count", "next_retry_at", "progress"]
42+
__properties: ClassVar[List[str]] = ["id", "task_type", "items_count", "document_id", "filename", "created_at", "updated_at", "status", "error_message", "retry_count", "next_retry_at", "progress"]
4243

4344
model_config = ConfigDict(
4445
populate_by_name=True,
@@ -87,6 +88,11 @@ def to_dict(self) -> Dict[str, Any]:
8788
if self.document_id is None and "document_id" in self.model_fields_set:
8889
_dict['document_id'] = None
8990

91+
# set to None if filename (nullable) is None
92+
# and model_fields_set contains the field
93+
if self.filename is None and "filename" in self.model_fields_set:
94+
_dict['filename'] = None
95+
9096
# set to None if updated_at (nullable) is None
9197
# and model_fields_set contains the field
9298
if self.updated_at is None and "updated_at" in self.model_fields_set:
@@ -128,6 +134,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
128134
"task_type": obj.get("task_type"),
129135
"items_count": obj.get("items_count"),
130136
"document_id": obj.get("document_id"),
137+
"filename": obj.get("filename"),
131138
"created_at": obj.get("created_at"),
132139
"updated_at": obj.get("updated_at"),
133140
"status": obj.get("status"),

hindsight-clients/typescript/generated/types.gen.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,12 @@ export type OperationResponse = {
27552755
* Document Id
27562756
*/
27572757
document_id?: string | null;
2758+
/**
2759+
* Filename
2760+
*
2761+
* Original filename for file-conversion operations (file_convert_retain); null for other task types.
2762+
*/
2763+
filename?: string | null;
27582764
/**
27592765
* Created At
27602766
*/

hindsight-control-plane/src/components/bank-selector.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,12 @@ function BankSelectorInner() {
376376
setDocAsync(false);
377377
setUploadProgress("");
378378

379+
// Nudge the documents view to surface the new file_convert_retain
380+
// operations right away (it derives pending rows from the server).
381+
if (typeof window !== "undefined") {
382+
window.dispatchEvent(new Event("hindsight:documents-refresh"));
383+
}
384+
379385
// Navigate to documents view
380386
router.push(bankRoute(currentBank!, "?view=documents"));
381387
} catch {

0 commit comments

Comments
 (0)