feat(brain): add DeleteFromIndex job
Inverse of the EmbedMemory job (#56): removes a memory from Qdrant (and the future Elasticsearch index) when brain_forget fires or a memory is soft-deleted. - php/Jobs/DeleteFromIndex.php — Laravel Job, 3 retries with backoff - BrainService: qdrantDelete() private→public and now throws on HTTP failure (was silent Log::warning — wouldn't trigger Job retry) - elasticDelete() stub added (fills in with the ES integration ticket) - php/tests/Feature/Jobs/DeleteFromIndexTest.php — success + HTTP-failure paths via mocked Http Co-authored-by: Codex <noreply@openai.com> Closes tasks.lthn.sh/view.php?id=57
This commit is contained in:
parent
4dc5ed8d14
commit
8d520adb5e
3 changed files with 124 additions and 2 deletions
36
php/Jobs/DeleteFromIndex.php
Normal file
36
php/Jobs/DeleteFromIndex.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Jobs;
|
||||
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DeleteFromIndex implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/**
|
||||
* @var array<int, int>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> $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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
php/tests/Feature/Jobs/DeleteFromIndexTest.php
Normal file
75
php/tests/Feature/Jobs/DeleteFromIndexTest.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Jobs\DeleteFromIndex;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Http\Client\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
function deleteFromIndexService(): BrainService
|
||||
{
|
||||
return new BrainService(
|
||||
ollamaUrl: 'https://ollama.test',
|
||||
qdrantUrl: 'https://qdrant.test',
|
||||
collection: 'openbrain',
|
||||
embeddingModel: 'embeddinggemma',
|
||||
verifySsl: false,
|
||||
);
|
||||
}
|
||||
|
||||
test('DeleteFromIndex_handle_Good_deletes_memory_from_qdrant', function (): void {
|
||||
$memoryId = Str::uuid()->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');
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue