fox/app/Services/Fox/FoxMemoryService.php

200 lines
6.3 KiB
PHP

<?php
namespace App\Services\Fox;
use App\Models\Fox\FoxLearningEvent;
use App\Models\Fox\FoxMemoryEdge;
use App\Models\Fox\FoxMemoryNode;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class FoxMemoryService
{
public function createNode(array $data): FoxMemoryNode
{
return FoxMemoryNode::create([
'type' => $data['type'] ?? 'fact',
'title' => $data['title'],
'content' => $data['content'] ?? null,
'meta' => $data['meta'] ?? null,
'confidence' => $data['confidence'] ?? 1.0,
'weight' => $data['weight'] ?? 1,
]);
}
public function updateNode(FoxMemoryNode $node, array $data): FoxMemoryNode
{
$node->update(array_filter([
'type' => $data['type'] ?? null,
'title' => $data['title'] ?? null,
'content' => $data['content'] ?? null,
'meta' => $data['meta'] ?? null,
'confidence' => $data['confidence'] ?? null,
'weight' => $data['weight'] ?? null,
], fn ($v) => $v !== null));
return $node->fresh();
}
public function createEdge(
FoxMemoryNode $source,
FoxMemoryNode $target,
string $relation,
float $strength = 1.0,
array $meta = [],
): FoxMemoryEdge {
return FoxMemoryEdge::create([
'source_node_id' => $source->id,
'target_node_id' => $target->id,
'relation' => $relation,
'strength' => $strength,
'meta' => empty($meta) ? null : $meta,
]);
}
public function storeLearningEvent(
string $input,
array $extractedMemory = [],
string $status = FoxLearningEvent::STATUS_PENDING,
): FoxLearningEvent {
return FoxLearningEvent::create([
'input' => $input,
'extracted_memory' => empty($extractedMemory) ? null : $extractedMemory,
'status' => $status,
]);
}
public function approveLearningEvent(FoxLearningEvent $event): void
{
$event->update([
'status' => FoxLearningEvent::STATUS_APPROVED,
'approved_at' => now(),
]);
$memory = $event->extracted_memory ?? [];
if (empty($memory)) {
return;
}
DB::transaction(function () use ($memory) {
foreach ($memory['nodes'] ?? [] as $nodeData) {
$existing = FoxMemoryNode::where('title', $nodeData['title'])
->where('type', $nodeData['type'] ?? 'fact')
->first();
if ($existing) {
$this->updateNode($existing, $nodeData);
} else {
$this->createNode($nodeData);
}
}
foreach ($memory['edges'] ?? [] as $edgeData) {
$source = FoxMemoryNode::where('title', $edgeData['source'])->first();
$target = FoxMemoryNode::where('title', $edgeData['target'])->first();
if ($source && $target) {
$exists = FoxMemoryEdge::where('source_node_id', $source->id)
->where('target_node_id', $target->id)
->where('relation', $edgeData['relation'])
->exists();
if (! $exists) {
$this->createEdge(
$source,
$target,
$edgeData['relation'],
(float) ($edgeData['strength'] ?? 1.0),
);
}
}
}
});
}
public function rejectLearningEvent(FoxLearningEvent $event): void
{
$event->update([
'status' => FoxLearningEvent::STATUS_REJECTED,
'rejected_at' => now(),
]);
}
/**
* Simple relevance search via title/content LIKE match.
* Structure ready for vector embeddings later — swap this method body.
*/
public function searchRelevantMemories(string $query, int $limit = 10): Collection
{
if (blank($query)) {
return collect();
}
$terms = collect(explode(' ', $query))
->filter(fn ($t) => mb_strlen($t) > 2)
->take(5);
$q = FoxMemoryNode::query();
foreach ($terms as $term) {
$q->orWhere('title', 'ilike', "%{$term}%")
->orWhere('content', 'ilike', "%{$term}%")
->orWhere('type', 'ilike', "%{$term}%");
}
$results = $q->orderByDesc('weight')
->orderByDesc('confidence')
->limit($limit)
->get();
$results->each(fn (FoxMemoryNode $n) => $n->touchUsed());
return $results;
}
public function buildGraphPayload(): array
{
$nodes = FoxMemoryNode::orderByDesc('weight')->get();
$edges = FoxMemoryEdge::all();
$typeColors = [
'user' => '#22d3ee',
'project' => '#818cf8',
'place' => '#f59e0b',
'device' => '#34d399',
'skill' => '#a78bfa',
'preference' => '#fb7185',
'fact' => '#ffffff',
];
return [
'nodes' => $nodes->map(fn (FoxMemoryNode $n) => [
'id' => $n->id,
'label' => $n->title,
'type' => $n->type,
'color' => $typeColors[$n->type] ?? '#818cf8',
'content' => $n->content,
'weight' => $n->weight,
'confidence' => $n->confidence,
])->values()->all(),
'edges' => $edges->map(fn (FoxMemoryEdge $e) => [
'id' => $e->id,
'source' => $e->source_node_id,
'target' => $e->target_node_id,
'relation' => $e->relation,
'strength' => $e->strength,
])->values()->all(),
];
}
public function stats(): array
{
return [
'nodes_count' => FoxMemoryNode::count(),
'edges_count' => FoxMemoryEdge::count(),
'pending_count' => FoxLearningEvent::pending()->count(),
];
}
}