diff --git a/php/Jobs/DeleteFromIndex.php b/php/Jobs/DeleteFromIndex.php new file mode 100644 index 0000000..b249d68 --- /dev/null +++ b/php/Jobs/DeleteFromIndex.php @@ -0,0 +1,36 @@ + + */ + public array $backoff = [10, 60, 300]; + + public function __construct( + public string $memoryId, + ) {} + + public function handle(BrainService $brain): void + { + $brain->qdrantDelete([$this->memoryId]); + $brain->elasticDelete($this->memoryId); + } +} diff --git a/php/Services/BrainService.php b/php/Services/BrainService.php index bd61002..58706c5 100644 --- a/php/Services/BrainService.php +++ b/php/Services/BrainService.php @@ -214,6 +214,14 @@ class BrainService // Implemented by the Elasticsearch integration ticket. } + /** + * Delete a memory from Elasticsearch. + */ + public function elasticDelete(string $id): void + { + // Implemented by the Elasticsearch integration ticket. + } + /** * Build a Qdrant filter from criteria. * @@ -279,8 +287,10 @@ class BrainService * Delete points from Qdrant by ID. * * @param array $ids + * + * @throws \RuntimeException */ - private function qdrantDelete(array $ids): void + public function qdrantDelete(array $ids): void { $response = $this->http(10) ->post("{$this->qdrantUrl}/collections/{$this->collection}/points/delete", [ @@ -288,7 +298,8 @@ class BrainService ]); if (! $response->successful()) { - Log::warning("Qdrant delete failed: {$response->status()}", ['ids' => $ids, 'body' => $response->body()]); + Log::error("Qdrant delete failed: {$response->status()}", ['ids' => $ids, 'body' => $response->body()]); + throw new \RuntimeException("Qdrant delete failed: {$response->status()}"); } } } diff --git a/php/tests/Feature/Jobs/DeleteFromIndexTest.php b/php/tests/Feature/Jobs/DeleteFromIndexTest.php new file mode 100644 index 0000000..177bd34 --- /dev/null +++ b/php/tests/Feature/Jobs/DeleteFromIndexTest.php @@ -0,0 +1,75 @@ +toString(); + + Http::fake([ + 'https://qdrant.test/collections/openbrain/points/delete' => Http::response(['result' => ['status' => 'ok']]), + ]); + + (new DeleteFromIndex($memoryId))->handle(deleteFromIndexService()); + + Http::assertSent(fn (Request $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points/delete' + && $request->method() === 'POST' + && $request['points'] === [$memoryId]); +}); + +test('DeleteFromIndex_handle_Bad_deletes_soft_deleted_memory_from_qdrant', function (): void { + $workspace = createWorkspace(); + + Http::fake([ + 'https://qdrant.test/collections/openbrain/points/delete' => Http::response(['result' => ['status' => 'ok']]), + ]); + + $memory = BrainMemory::create([ + 'workspace_id' => $workspace->id, + 'agent_id' => 'virgil', + 'type' => 'observation', + 'content' => 'Soft-deleted memories should still be removed from the index.', + 'confidence' => 0.8, + ]); + $memory->delete(); + + (new DeleteFromIndex($memory->id))->handle(deleteFromIndexService()); + + Http::assertSent(fn (Request $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points/delete' + && $request->method() === 'POST' + && $request['points'] === [$memory->id]); +}); + +test('DeleteFromIndex_handle_Ugly_throws_when_qdrant_delete_fails', function (): void { + $memoryId = Str::uuid()->toString(); + + Http::fake([ + 'https://qdrant.test/collections/openbrain/points/delete' => Http::response(['error' => 'unavailable'], 500), + ]); + + try { + (new DeleteFromIndex($memoryId))->handle(deleteFromIndexService()); + $this->fail('Expected Qdrant failure to throw.'); + } catch (RuntimeException $exception) { + expect($exception->getMessage())->toBe('Qdrant delete failed: 500'); + } +});