diff --git a/php/Services/BrainService.php b/php/Services/BrainService.php index 5168d9f..f33d41d 100644 --- a/php/Services/BrainService.php +++ b/php/Services/BrainService.php @@ -68,43 +68,31 @@ class BrainService } /** - * Store a memory in both MariaDB and Qdrant. + * Store a memory and queue asynchronous indexing. * - * Creates the MariaDB record and upserts the Qdrant vector within - * a single DB transaction. If the memory supersedes an older one, - * the old entry is soft-deleted from MariaDB and removed from Qdrant. + * Creates the brain database record within a DB transaction and dispatches + * EmbedMemory after commit so embedding, Qdrant, and Elasticsearch + * indexing happen on the queue. * * @param array $attributes Fillable attributes for BrainMemory * @return BrainMemory The created memory */ public function remember(array $attributes): BrainMemory { - $vector = $this->embed($attributes['content']); + $attributes['indexed_at'] = null; - return DB::connection('brain')->transaction(function () use ($attributes, $vector) { + $memory = DB::connection('brain')->transaction(function () use ($attributes) { $memory = BrainMemory::create($attributes); - - $payload = $this->buildQdrantPayload($memory->id, [ - 'workspace_id' => $memory->workspace_id, - 'agent_id' => $memory->agent_id, - 'type' => $memory->type, - 'tags' => $memory->tags ?? [], - 'project' => $memory->project, - 'confidence' => $memory->confidence, - 'source' => $memory->source ?? 'manual', - 'created_at' => $memory->created_at->toIso8601String(), - ]); - $payload['vector'] = $vector; - - $this->qdrantUpsert([$payload]); - if ($memory->supersedes_id) { BrainMemory::where('id', $memory->supersedes_id)->delete(); - $this->qdrantDelete([$memory->supersedes_id]); } return $memory; }); + + \Core\Mod\Agentic\Jobs\EmbedMemory::dispatch($memory->id); + + return $memory; } /** diff --git a/php/tests/Feature/Services/BrainServiceRememberTest.php b/php/tests/Feature/Services/BrainServiceRememberTest.php new file mode 100644 index 0000000..898477c --- /dev/null +++ b/php/tests/Feature/Services/BrainServiceRememberTest.php @@ -0,0 +1,95 @@ + + */ + public function embed(string $text): array + { + return array_fill(0, 768, 0.125); + } + + public function qdrantUpsert(array $points): void + { + $this->qdrantUpsertCalled = true; + } + }; +} + +function rememberBrainAttributes(array $attributes = []): array +{ + return array_merge([ + 'workspace_id' => $attributes['workspace_id'] ?? createWorkspace()->id, + 'agent_id' => 'virgil', + 'type' => 'architecture', + 'content' => 'Brain memories are indexed by queued jobs.', + 'tags' => ['brain', 'queue'], + 'project' => 'agent', + 'confidence' => 0.95, + 'source' => 'ticket-55', + ], $attributes); +} + +test('BrainService_remember_Good_returns_unindexed_memory_and_dispatches_embed_job', function (): void { + Queue::fake(); + $brain = rememberBrainService(); + + $memory = $brain->remember(rememberBrainAttributes([ + 'indexed_at' => now(), + ])); + + expect($memory->exists)->toBeTrue() + ->and($memory->indexed_at)->toBeNull() + ->and($memory->fresh()->indexed_at)->toBeNull() + ->and($brain->qdrantUpsertCalled)->toBeFalse(); + + Queue::assertPushed(EmbedMemory::class, fn (EmbedMemory $job): bool => $job->memoryId === $memory->id); +}); + +test('BrainService_remember_Bad_does_not_call_qdrant_upsert_directly', function (): void { + Queue::fake(); + $brain = rememberBrainService(); + + $brain->remember(rememberBrainAttributes()); + + expect($brain->qdrantUpsertCalled)->toBeFalse(); + + Queue::assertPushed(EmbedMemory::class); +}); + +test('BrainService_remember_Ugly_soft_deletes_superseded_memory_before_dispatching_job', function (): void { + Queue::fake(); + $brain = rememberBrainService(); + $workspace = createWorkspace(); + $oldMemory = BrainMemory::create(rememberBrainAttributes([ + 'workspace_id' => $workspace->id, + 'content' => 'Old memory version.', + ])); + + $memory = $brain->remember(rememberBrainAttributes([ + 'workspace_id' => $workspace->id, + 'content' => 'New memory version.', + 'supersedes_id' => $oldMemory->id, + ])); + + expect(BrainMemory::find($oldMemory->id))->toBeNull() + ->and(BrainMemory::withTrashed()->find($oldMemory->id)?->trashed())->toBeTrue() + ->and($memory->indexed_at)->toBeNull() + ->and($brain->qdrantUpsertCalled)->toBeFalse(); + + Queue::assertPushed(EmbedMemory::class, fn (EmbedMemory $job): bool => $job->memoryId === $memory->id); +});