Skip to content

Commit e06a612

Browse files
authored
feat(misc): update clients types, test coverage, improve /health endpoint and add changelog (#70)
* doc: changelog and delete doc info * others * others * fixes * fixes
1 parent e599346 commit e06a612

156 files changed

Lines changed: 14143 additions & 11585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,83 @@ jobs:
182182
path: hindsight-cli/target/release/hindsight
183183
retention-days: 1
184184

185+
test-rust-cli:
186+
runs-on: ubuntu-latest
187+
needs: build-rust-cli
188+
env:
189+
HINDSIGHT_API_LLM_PROVIDER: groq
190+
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
191+
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
192+
HINDSIGHT_API_URL: http://localhost:8888
193+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
194+
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
195+
196+
steps:
197+
- uses: actions/checkout@v4
198+
199+
- name: Download CLI artifact
200+
uses: actions/download-artifact@v4
201+
with:
202+
name: hindsight-cli
203+
path: /tmp/cli
204+
205+
- name: Make CLI executable
206+
run: chmod +x /tmp/cli/hindsight
207+
208+
- name: Install uv
209+
uses: astral-sh/setup-uv@v5
210+
with:
211+
enable-cache: true
212+
prune-cache: false
213+
214+
- name: Set up Python
215+
uses: actions/setup-python@v5
216+
with:
217+
python-version-file: ".python-version"
218+
219+
- name: Build API
220+
working-directory: ./hindsight-api
221+
run: uv build
222+
223+
- name: Install API dependencies
224+
working-directory: ./hindsight-api
225+
run: uv sync --no-install-project --index-strategy unsafe-best-match
226+
227+
- name: Create .env file
228+
run: |
229+
cat > .env << EOF
230+
HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }}
231+
HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }}
232+
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
233+
EOF
234+
235+
- name: Start API server
236+
run: |
237+
./scripts/dev/start-api.sh > /tmp/api-server.log 2>&1 &
238+
echo "Waiting for API server to be ready..."
239+
for i in {1..60}; do
240+
if curl -sf http://localhost:8888/health > /dev/null 2>&1; then
241+
echo "API server is ready after ${i}s"
242+
break
243+
fi
244+
if [ $i -eq 60 ]; then
245+
echo "API server failed to start after 60s"
246+
cat /tmp/api-server.log
247+
exit 1
248+
fi
249+
sleep 1
250+
done
251+
252+
- name: Run CLI smoke test
253+
run: |
254+
HINDSIGHT_CLI=/tmp/cli/hindsight ./hindsight-cli/smoke-test.sh
255+
256+
- name: Show API server logs
257+
if: always()
258+
run: |
259+
echo "=== API Server Logs ==="
260+
cat /tmp/api-server.log || echo "No API server log found"
261+
185262
lint-helm-chart:
186263
runs-on: ubuntu-latest
187264

hindsight-api/hindsight_api/api/http.py

Lines changed: 149 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,26 @@ class DocumentResponse(BaseModel):
689689
memory_unit_count: int
690690

691691

692+
class DeleteDocumentResponse(BaseModel):
693+
"""Response model for delete document endpoint."""
694+
695+
model_config = ConfigDict(
696+
json_schema_extra={
697+
"example": {
698+
"success": True,
699+
"message": "Document 'session_1' and 5 associated memory units deleted successfully",
700+
"document_id": "session_1",
701+
"memory_units_deleted": 5,
702+
}
703+
}
704+
)
705+
706+
success: bool
707+
message: str
708+
document_id: str
709+
memory_units_deleted: int
710+
711+
692712
class ChunkResponse(BaseModel):
693713
"""Response model for get chunk endpoint."""
694714

@@ -725,6 +745,108 @@ class DeleteResponse(BaseModel):
725745
deleted_count: int | None = None
726746

727747

748+
class BankStatsResponse(BaseModel):
749+
"""Response model for bank statistics endpoint."""
750+
751+
model_config = ConfigDict(
752+
json_schema_extra={
753+
"example": {
754+
"bank_id": "user123",
755+
"total_nodes": 150,
756+
"total_links": 300,
757+
"total_documents": 10,
758+
"nodes_by_fact_type": {"fact": 100, "preference": 30, "observation": 20},
759+
"links_by_link_type": {"temporal": 150, "semantic": 100, "entity": 50},
760+
"links_by_fact_type": {"fact": 200, "preference": 60, "observation": 40},
761+
"links_breakdown": {"fact": {"temporal": 100, "semantic": 60, "entity": 40}},
762+
"pending_operations": 2,
763+
"failed_operations": 0,
764+
}
765+
}
766+
)
767+
768+
bank_id: str
769+
total_nodes: int
770+
total_links: int
771+
total_documents: int
772+
nodes_by_fact_type: dict[str, int]
773+
links_by_link_type: dict[str, int]
774+
links_by_fact_type: dict[str, int]
775+
links_breakdown: dict[str, dict[str, int]]
776+
pending_operations: int
777+
failed_operations: int
778+
779+
780+
class OperationResponse(BaseModel):
781+
"""Response model for a single async operation."""
782+
783+
model_config = ConfigDict(
784+
json_schema_extra={
785+
"example": {
786+
"id": "550e8400-e29b-41d4-a716-446655440000",
787+
"task_type": "retain",
788+
"items_count": 5,
789+
"document_id": "meeting-notes-2024",
790+
"created_at": "2024-01-15T10:30:00Z",
791+
"status": "pending",
792+
"error_message": None,
793+
}
794+
}
795+
)
796+
797+
id: str
798+
task_type: str
799+
items_count: int
800+
document_id: str | None
801+
created_at: str
802+
status: str
803+
error_message: str | None
804+
805+
806+
class OperationsListResponse(BaseModel):
807+
"""Response model for list operations endpoint."""
808+
809+
model_config = ConfigDict(
810+
json_schema_extra={
811+
"example": {
812+
"bank_id": "user123",
813+
"operations": [
814+
{
815+
"id": "550e8400-e29b-41d4-a716-446655440000",
816+
"task_type": "retain",
817+
"items_count": 5,
818+
"document_id": None,
819+
"created_at": "2024-01-15T10:30:00Z",
820+
"status": "pending",
821+
"error_message": None,
822+
}
823+
],
824+
}
825+
}
826+
)
827+
828+
bank_id: str
829+
operations: list[OperationResponse]
830+
831+
832+
class CancelOperationResponse(BaseModel):
833+
"""Response model for cancel operation endpoint."""
834+
835+
model_config = ConfigDict(
836+
json_schema_extra={
837+
"example": {
838+
"success": True,
839+
"message": "Operation 550e8400-e29b-41d4-a716-446655440000 cancelled",
840+
"operation_id": "550e8400-e29b-41d4-a716-446655440000",
841+
}
842+
}
843+
)
844+
845+
success: bool
846+
message: str
847+
operation_id: str
848+
849+
728850
def create_app(
729851
memory: MemoryEngine,
730852
initialize_memory: bool = True,
@@ -1142,6 +1264,7 @@ async def api_list_banks(request_context: RequestContext = Depends(get_request_c
11421264

11431265
@app.get(
11441266
"/v1/default/banks/{bank_id}/stats",
1267+
response_model=BankStatsResponse,
11451268
summary="Get statistics for memory bank",
11461269
description="Get statistics about nodes and links for a specific agent",
11471270
operation_id="get_agent_stats",
@@ -1242,18 +1365,18 @@ async def api_stats(bank_id: str):
12421365
total_nodes = sum(nodes_by_type.values())
12431366
total_links = sum(links_by_type.values())
12441367

1245-
return {
1246-
"bank_id": bank_id,
1247-
"total_nodes": total_nodes,
1248-
"total_links": total_links,
1249-
"total_documents": total_documents,
1250-
"nodes_by_fact_type": nodes_by_type,
1251-
"links_by_link_type": links_by_type,
1252-
"links_by_fact_type": links_by_fact_type,
1253-
"links_breakdown": links_breakdown,
1254-
"pending_operations": pending_operations,
1255-
"failed_operations": failed_operations,
1256-
}
1368+
return BankStatsResponse(
1369+
bank_id=bank_id,
1370+
total_nodes=total_nodes,
1371+
total_links=total_links,
1372+
total_documents=total_documents,
1373+
nodes_by_fact_type=nodes_by_type,
1374+
links_by_link_type=links_by_type,
1375+
links_by_fact_type=links_by_fact_type,
1376+
links_breakdown=links_breakdown,
1377+
pending_operations=pending_operations,
1378+
failed_operations=failed_operations,
1379+
)
12571380

12581381
except Exception as e:
12591382
import traceback
@@ -1477,6 +1600,7 @@ async def api_get_chunk(chunk_id: str, request_context: RequestContext = Depends
14771600

14781601
@app.delete(
14791602
"/v1/default/banks/{bank_id}/documents/{document_id}",
1603+
response_model=DeleteDocumentResponse,
14801604
summary="Delete a document",
14811605
description="Delete a document and all its associated memory units and links.\n\n"
14821606
"This will cascade delete:\n"
@@ -1503,12 +1627,12 @@ async def api_delete_document(
15031627
if result["document_deleted"] == 0:
15041628
raise HTTPException(status_code=404, detail="Document not found")
15051629

1506-
return {
1507-
"success": True,
1508-
"message": f"Document '{document_id}' and {result['memory_units_deleted']} associated memory units deleted successfully",
1509-
"document_id": document_id,
1510-
"memory_units_deleted": result["memory_units_deleted"],
1511-
}
1630+
return DeleteDocumentResponse(
1631+
success=True,
1632+
message=f"Document '{document_id}' and {result['memory_units_deleted']} associated memory units deleted successfully",
1633+
document_id=document_id,
1634+
memory_units_deleted=result["memory_units_deleted"],
1635+
)
15121636
except HTTPException:
15131637
raise
15141638
except Exception as e:
@@ -1520,6 +1644,7 @@ async def api_delete_document(
15201644

15211645
@app.get(
15221646
"/v1/default/banks/{bank_id}/operations",
1647+
response_model=OperationsListResponse,
15231648
summary="List async operations",
15241649
description="Get a list of all async operations (pending and failed) for a specific agent, including error messages for failed operations",
15251650
operation_id="list_operations",
@@ -1529,10 +1654,10 @@ async def api_list_operations(bank_id: str, request_context: RequestContext = De
15291654
"""List all async operations (pending and failed) for a memory bank."""
15301655
try:
15311656
operations = await app.state.memory.list_operations(bank_id, request_context=request_context)
1532-
return {
1533-
"bank_id": bank_id,
1534-
"operations": operations,
1535-
}
1657+
return OperationsListResponse(
1658+
bank_id=bank_id,
1659+
operations=[OperationResponse(**op) for op in operations],
1660+
)
15361661
except Exception as e:
15371662
import traceback
15381663

@@ -1542,6 +1667,7 @@ async def api_list_operations(bank_id: str, request_context: RequestContext = De
15421667

15431668
@app.delete(
15441669
"/v1/default/banks/{bank_id}/operations/{operation_id}",
1670+
response_model=CancelOperationResponse,
15451671
summary="Cancel a pending async operation",
15461672
description="Cancel a pending async operation by removing it from the queue",
15471673
operation_id="cancel_operation",
@@ -1559,7 +1685,7 @@ async def api_cancel_operation(
15591685
raise HTTPException(status_code=400, detail=f"Invalid operation_id format: {operation_id}")
15601686

15611687
result = await app.state.memory.cancel_operation(bank_id, operation_id, request_context=request_context)
1562-
return result
1688+
return CancelOperationResponse(**result)
15631689
except ValueError as e:
15641690
raise HTTPException(status_code=404, detail=str(e))
15651691
except Exception as e:

hindsight-api/hindsight_api/engine/memory_engine.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,14 @@ async def health_check(self) -> dict:
695695
696696
Returns:
697697
dict with status and optional error message
698+
699+
Note:
700+
Returns unhealthy until initialize() has completed successfully.
698701
"""
702+
# Not healthy until fully initialized
703+
if not self._initialized:
704+
return {"status": "unhealthy", "reason": "not_initialized"}
705+
699706
try:
700707
pool = await self._get_pool()
701708
async with pool.acquire() as conn:

0 commit comments

Comments
 (0)