feat(brain): make BrainService::remember() async via EmbedMemory job
remember() now writes the brain_memories row with indexed_at=null and dispatches EmbedMemory::dispatch($memory->id) for async Qdrant + ES indexing, instead of calling qdrantUpsert() synchronously. Response shape matches the row state — caller gets the memory immediately, the Job flips indexed_at once the Qdrant write succeeds. Superseded rows still soft-delete synchronously (part of the remember contract, not the indexing path). php/tests/Feature/Services/BrainServiceRememberTest.php uses Queue::fake() to assert EmbedMemory is dispatched and BrainService::qdrantUpsert() is NOT called directly (subclass probe). Co-authored-by: Codex <noreply@openai.com> Closes tasks.lthn.sh/view.php?id=55
This commit is contained in:
parent
47f241d880
commit
677d890308
2 changed files with 105 additions and 22 deletions
|
|
@ -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<string, mixed> $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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
95
php/tests/Feature/Services/BrainServiceRememberTest.php
Normal file
95
php/tests/Feature/Services/BrainServiceRememberTest.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Jobs\EmbedMemory;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
function rememberBrainService(): BrainService
|
||||
{
|
||||
return new class extends BrainService
|
||||
{
|
||||
public bool $qdrantUpsertCalled = false;
|
||||
|
||||
/**
|
||||
* @return array<float>
|
||||
*/
|
||||
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);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue