validate([ 'content' => 'required|string|max:50000', 'type' => 'required|string', 'tags' => 'nullable|array', 'tags.*' => 'string', 'project' => 'nullable|string|max:255', 'confidence' => 'nullable|numeric|min:0|max:1', 'supersedes' => 'nullable|uuid', 'expires_in' => 'nullable|integer|min:1', ]); $workspace = $request->attributes->get('workspace'); $workspaceId = (int) ($request->attributes->get('workspace_id') ?? $workspace?->id); $apiKey = $request->attributes->get('api_key') ?? $request->attributes->get('agent_api_key'); $agentId = $apiKey?->name ?? 'api'; try { $memory = RememberKnowledge::run($validated, $workspaceId, $agentId); return response()->json([ 'data' => $memory->toMcpContext(), ], 201); } catch (\InvalidArgumentException $e) { return response()->json([ 'error' => 'validation_error', 'message' => $e->getMessage(), ], 422); } catch (\RuntimeException $e) { return response()->json([ 'error' => 'service_error', 'message' => 'Brain service temporarily unavailable.', ], 503); } } /** * POST /api/brain/recall * * Semantic search across memories. */ public function recall(Request $request, BrainService $brain): JsonResponse { $validated = $request->validate([ 'query' => 'required|string|max:2000', 'limit' => 'nullable|integer|min:1|max:20', 'top_k' => 'nullable|integer|min:1|max:20', 'workspace_id' => 'nullable|integer|min:1', 'org' => 'nullable|string', 'project' => 'nullable|string', 'type' => 'nullable', 'keywords' => 'nullable|array', 'keywords.*' => 'string|max:255', 'boost_keywords' => 'nullable|array', 'boost_keywords.*' => 'string|max:255', 'filter' => 'nullable|array', 'filter.org' => 'nullable|string', 'filter.project' => 'nullable|string', 'filter.type' => 'nullable', 'filter.agent_id' => 'nullable|string', 'filter.min_confidence' => 'nullable|numeric|min:0|max:1', ]); $workspace = $request->attributes->get('workspace'); $workspaceId = (int) ($request->attributes->get('workspace_id') ?? $workspace?->id ?? $validated['workspace_id'] ?? 0); $filter = $validated['filter'] ?? []; foreach (['org', 'project', 'type'] as $field) { if (array_key_exists($field, $validated) && $validated[$field] !== null) { $filter[$field] = $validated[$field]; } } try { $this->assertValidTypeFilter($filter['type'] ?? null); $result = $brain->recall( $validated['query'], $validated['limit'] ?? $validated['top_k'] ?? 5, $filter, $workspaceId, $this->normaliseStringList($validated['keywords'] ?? []), $this->normaliseStringList($validated['boost_keywords'] ?? []), ); $result['count'] = count($result['memories'] ?? []); return response()->json([ 'data' => $result, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'error' => 'validation_error', 'message' => $e->getMessage(), ], 422); } catch (\RuntimeException $e) { return response()->json([ 'error' => 'service_error', 'message' => 'Brain service temporarily unavailable.', ], 503); } } /** * @param array $values * @return array */ private function normaliseStringList(array $values): array { return array_values(array_filter(array_map( static fn (mixed $value): string => is_string($value) ? trim($value) : '', $values, ), static fn (string $value): bool => $value !== '')); } private function assertValidTypeFilter(mixed $type): void { if ($type === null) { return; } $validTypes = BrainMemory::VALID_TYPES; if (is_string($type)) { if (! in_array($type, $validTypes, true)) { throw new \InvalidArgumentException( sprintf('filter.type must be one of: %s', implode(', ', $validTypes)) ); } return; } if (is_array($type)) { foreach ($type as $value) { if (! is_string($value) || ! in_array($value, $validTypes, true)) { throw new \InvalidArgumentException( sprintf('Each filter.type value must be one of: %s', implode(', ', $validTypes)) ); } } return; } throw new \InvalidArgumentException( sprintf('filter.type must be one of: %s', implode(', ', $validTypes)) ); } /** * DELETE /api/brain/forget/{id} * * Remove a memory. */ public function forget(Request $request, string $id): JsonResponse { $request->validate([ 'reason' => 'nullable|string|max:500', ]); $workspace = $request->attributes->get('workspace'); $workspaceId = (int) ($request->attributes->get('workspace_id') ?? $workspace?->id); $apiKey = $request->attributes->get('api_key') ?? $request->attributes->get('agent_api_key'); $agentId = $apiKey?->name ?? 'api'; try { $result = ForgetKnowledge::run($id, $workspaceId, $agentId, $request->input('reason')); return response()->json([ 'data' => $result, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'error' => 'not_found', 'message' => $e->getMessage(), ], 404); } catch (\RuntimeException $e) { return response()->json([ 'error' => 'service_error', 'message' => 'Brain service temporarily unavailable.', ], 503); } } /** * GET /api/brain/list * * List memories with optional filters. */ public function list(Request $request): JsonResponse { $validated = $request->validate([ 'project' => 'nullable|string', 'type' => 'nullable|string', 'agent_id' => 'nullable|string', 'limit' => 'nullable|integer|min:1|max:100', ]); $workspace = $request->attributes->get('workspace'); $workspaceId = (int) ($request->attributes->get('workspace_id') ?? $workspace?->id); try { $result = ListKnowledge::run($workspaceId, $validated); return response()->json([ 'data' => $result, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'error' => 'validation_error', 'message' => $e->getMessage(), ], 422); } } }