$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(), ]; } }