feat: extract Brain operations into CorePHP Actions + API routes
- Create 4 Actions in Actions/Brain/ (RememberKnowledge, RecallKnowledge, ForgetKnowledge, ListKnowledge) using the Action trait pattern - Slim MCP tool handlers to thin wrappers calling Actions - Add BrainController with REST endpoints (remember, recall, forget, list) - Add API route file with api.auth + api.scope.enforce middleware - Wire ApiRoutesRegistering in Boot.php - Rename routes/ → Routes/ to match CorePHP convention - Remove empty database/migrations/ (legacy Laravel boilerplate) Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
8bc6e62f13
commit
8b8a9c26e5
16 changed files with 515 additions and 141 deletions
62
Actions/Brain/ForgetKnowledge.php
Normal file
62
Actions/Brain/ForgetKnowledge.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Actions\Brain;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Remove a memory from the shared OpenBrain knowledge store.
|
||||
*
|
||||
* Deletes from both MariaDB and Qdrant. Workspace-scoped.
|
||||
*
|
||||
* Usage:
|
||||
* ForgetKnowledge::run('uuid-here', 1, 'virgil', 'outdated info');
|
||||
*/
|
||||
class ForgetKnowledge
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private BrainService $brain,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{forgotten: string, type: string}
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function handle(string $id, int $workspaceId, string $agentId = 'anonymous', ?string $reason = null): array
|
||||
{
|
||||
if ($id === '') {
|
||||
throw new \InvalidArgumentException('id is required');
|
||||
}
|
||||
|
||||
$memory = BrainMemory::where('id', $id)
|
||||
->where('workspace_id', $workspaceId)
|
||||
->first();
|
||||
|
||||
if (! $memory) {
|
||||
throw new \InvalidArgumentException("Memory '{$id}' not found in this workspace");
|
||||
}
|
||||
|
||||
Log::info('OpenBrain: memory forgotten', [
|
||||
'id' => $id,
|
||||
'type' => $memory->type,
|
||||
'agent_id' => $agentId,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
|
||||
$this->brain->forget($id);
|
||||
|
||||
return [
|
||||
'forgotten' => $id,
|
||||
'type' => $memory->type,
|
||||
];
|
||||
}
|
||||
}
|
||||
57
Actions/Brain/ListKnowledge.php
Normal file
57
Actions/Brain/ListKnowledge.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Actions\Brain;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* List memories in the shared OpenBrain knowledge store.
|
||||
*
|
||||
* Pure MariaDB query using model scopes — no vector search.
|
||||
* Use RecallKnowledge for semantic queries.
|
||||
*
|
||||
* Usage:
|
||||
* $memories = ListKnowledge::run(1, ['type' => 'decision']);
|
||||
*/
|
||||
class ListKnowledge
|
||||
{
|
||||
use Action;
|
||||
|
||||
/**
|
||||
* @param array{project?: string, type?: string, agent_id?: string, limit?: int} $filter
|
||||
* @return array{memories: array, count: int}
|
||||
*/
|
||||
public function handle(int $workspaceId, array $filter = []): array
|
||||
{
|
||||
$limit = min(max((int) ($filter['limit'] ?? 20), 1), 100);
|
||||
|
||||
$query = BrainMemory::forWorkspace($workspaceId)
|
||||
->active()
|
||||
->latestVersions()
|
||||
->forProject($filter['project'] ?? null)
|
||||
->byAgent($filter['agent_id'] ?? null);
|
||||
|
||||
$type = $filter['type'] ?? null;
|
||||
if ($type !== null) {
|
||||
if (is_string($type) && ! in_array($type, BrainMemory::VALID_TYPES, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('type must be one of: %s', implode(', ', BrainMemory::VALID_TYPES))
|
||||
);
|
||||
}
|
||||
$query->ofType($type);
|
||||
}
|
||||
|
||||
$memories = $query->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'memories' => $memories->map(fn (BrainMemory $m) => $m->toMcpContext())->all(),
|
||||
'count' => $memories->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
85
Actions/Brain/RecallKnowledge.php
Normal file
85
Actions/Brain/RecallKnowledge.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Actions\Brain;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
|
||||
/**
|
||||
* Semantic search across the shared OpenBrain knowledge store.
|
||||
*
|
||||
* Uses vector similarity to find memories relevant to a natural
|
||||
* language query, with optional filtering by project, type, agent,
|
||||
* or minimum confidence.
|
||||
*
|
||||
* Usage:
|
||||
* $results = RecallKnowledge::run('how does auth work?', 1);
|
||||
*/
|
||||
class RecallKnowledge
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private BrainService $brain,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{project?: string, type?: string|array, agent_id?: string, min_confidence?: float} $filter
|
||||
* @return array{memories: array, scores: array<string, float>, count: int}
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function handle(string $query, int $workspaceId, array $filter = [], int $topK = 5): array
|
||||
{
|
||||
if ($query === '') {
|
||||
throw new \InvalidArgumentException('query is required and must be a non-empty string');
|
||||
}
|
||||
if (mb_strlen($query) > 2000) {
|
||||
throw new \InvalidArgumentException('query must not exceed 2,000 characters');
|
||||
}
|
||||
|
||||
if ($topK < 1 || $topK > 20) {
|
||||
throw new \InvalidArgumentException('top_k must be between 1 and 20');
|
||||
}
|
||||
|
||||
if (isset($filter['type'])) {
|
||||
$typeValue = $filter['type'];
|
||||
$validTypes = BrainMemory::VALID_TYPES;
|
||||
|
||||
if (is_string($typeValue)) {
|
||||
if (! in_array($typeValue, $validTypes, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('filter.type must be one of: %s', implode(', ', $validTypes))
|
||||
);
|
||||
}
|
||||
} elseif (is_array($typeValue)) {
|
||||
foreach ($typeValue as $t) {
|
||||
if (! is_string($t) || ! in_array($t, $validTypes, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Each filter.type value must be one of: %s', implode(', ', $validTypes))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($filter['min_confidence'])) {
|
||||
$mc = $filter['min_confidence'];
|
||||
if (! is_numeric($mc) || $mc < 0.0 || $mc > 1.0) {
|
||||
throw new \InvalidArgumentException('filter.min_confidence must be between 0.0 and 1.0');
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->brain->recall($query, $topK, $filter, $workspaceId);
|
||||
|
||||
return [
|
||||
'memories' => $result['memories'],
|
||||
'scores' => $result['scores'],
|
||||
'count' => count($result['memories']),
|
||||
];
|
||||
}
|
||||
}
|
||||
94
Actions/Brain/RememberKnowledge.php
Normal file
94
Actions/Brain/RememberKnowledge.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Actions\Brain;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
|
||||
/**
|
||||
* Store a memory in the shared OpenBrain knowledge store.
|
||||
*
|
||||
* Persists content with embeddings to both MariaDB and Qdrant.
|
||||
* Handles supersession (replacing old memories) and expiry.
|
||||
*
|
||||
* Usage:
|
||||
* $memory = RememberKnowledge::run($data, 1, 'virgil');
|
||||
*/
|
||||
class RememberKnowledge
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private BrainService $brain,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{content: string, type: string, tags?: array, project?: string, confidence?: float, supersedes?: string, expires_in?: int} $data
|
||||
* @return BrainMemory The created memory
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function handle(array $data, int $workspaceId, string $agentId = 'anonymous'): BrainMemory
|
||||
{
|
||||
$content = $data['content'] ?? null;
|
||||
if (! is_string($content) || $content === '') {
|
||||
throw new \InvalidArgumentException('content is required and must be a non-empty string');
|
||||
}
|
||||
if (mb_strlen($content) > 50000) {
|
||||
throw new \InvalidArgumentException('content must not exceed 50,000 characters');
|
||||
}
|
||||
|
||||
$type = $data['type'] ?? null;
|
||||
if (! is_string($type) || ! in_array($type, BrainMemory::VALID_TYPES, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('type must be one of: %s', implode(', ', BrainMemory::VALID_TYPES))
|
||||
);
|
||||
}
|
||||
|
||||
$confidence = (float) ($data['confidence'] ?? 0.8);
|
||||
if ($confidence < 0.0 || $confidence > 1.0) {
|
||||
throw new \InvalidArgumentException('confidence must be between 0.0 and 1.0');
|
||||
}
|
||||
|
||||
$tags = $data['tags'] ?? null;
|
||||
if (is_array($tags)) {
|
||||
foreach ($tags as $tag) {
|
||||
if (! is_string($tag)) {
|
||||
throw new \InvalidArgumentException('Each tag must be a string');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$supersedes = $data['supersedes'] ?? null;
|
||||
if ($supersedes !== null) {
|
||||
$existing = BrainMemory::where('id', $supersedes)
|
||||
->where('workspace_id', $workspaceId)
|
||||
->first();
|
||||
|
||||
if (! $existing) {
|
||||
throw new \InvalidArgumentException("Memory '{$supersedes}' not found in this workspace");
|
||||
}
|
||||
}
|
||||
|
||||
$expiresIn = isset($data['expires_in']) ? (int) $data['expires_in'] : null;
|
||||
if ($expiresIn !== null && $expiresIn < 1) {
|
||||
throw new \InvalidArgumentException('expires_in must be at least 1 hour');
|
||||
}
|
||||
|
||||
return $this->brain->remember([
|
||||
'workspace_id' => $workspaceId,
|
||||
'agent_id' => $agentId,
|
||||
'type' => $type,
|
||||
'content' => $content,
|
||||
'tags' => $tags,
|
||||
'project' => $data['project'] ?? null,
|
||||
'confidence' => $confidence,
|
||||
'supersedes_id' => $supersedes,
|
||||
'expires_at' => $expiresIn ? now()->addHours($expiresIn) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
10
Boot.php
10
Boot.php
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Mod\Agentic;
|
||||
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ApiRoutesRegistering;
|
||||
use Core\Events\ConsoleBooting;
|
||||
use Core\Events\McpToolsRegistering;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
|
|
@ -24,6 +25,7 @@ class Boot extends ServiceProvider
|
|||
*/
|
||||
public static array $listens = [
|
||||
AdminPanelBooting::class => 'onAdminPanel',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
McpToolsRegistering::class => 'onMcpTools',
|
||||
];
|
||||
|
|
@ -134,6 +136,14 @@ class Boot extends ServiceProvider
|
|||
// in the existing boot() method until we migrate to pure event-driven nav.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API routes registration event.
|
||||
*/
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn () => require __DIR__.'/Routes/api.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle console booting event.
|
||||
*/
|
||||
|
|
|
|||
164
Controllers/Api/BrainController.php
Normal file
164
Controllers/Api/BrainController.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Controllers\Api;
|
||||
|
||||
use Core\Front\Controller;
|
||||
use Core\Mod\Agentic\Actions\Brain\ForgetKnowledge;
|
||||
use Core\Mod\Agentic\Actions\Brain\ListKnowledge;
|
||||
use Core\Mod\Agentic\Actions\Brain\RecallKnowledge;
|
||||
use Core\Mod\Agentic\Actions\Brain\RememberKnowledge;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BrainController extends Controller
|
||||
{
|
||||
/**
|
||||
* POST /api/brain/remember
|
||||
*
|
||||
* Store a memory in OpenBrain.
|
||||
*/
|
||||
public function remember(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->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');
|
||||
$apiKey = $request->attributes->get('api_key');
|
||||
$agentId = $apiKey?->name ?? 'api';
|
||||
|
||||
try {
|
||||
$memory = RememberKnowledge::run($validated, $workspace->id, $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): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'query' => 'required|string|max:2000',
|
||||
'top_k' => 'nullable|integer|min:1|max:20',
|
||||
'filter' => 'nullable|array',
|
||||
'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');
|
||||
|
||||
try {
|
||||
$result = RecallKnowledge::run(
|
||||
$validated['query'],
|
||||
$workspace->id,
|
||||
$validated['filter'] ?? [],
|
||||
$validated['top_k'] ?? 5,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
$apiKey = $request->attributes->get('api_key');
|
||||
$agentId = $apiKey?->name ?? 'api';
|
||||
|
||||
try {
|
||||
$result = ForgetKnowledge::run($id, $workspace->id, $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');
|
||||
|
||||
try {
|
||||
$result = ListKnowledge::run($workspace->id, $validated);
|
||||
|
||||
return response()->json([
|
||||
'data' => $result,
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'error' => 'validation_error',
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,9 @@ declare(strict_types=1);
|
|||
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||
|
||||
use Core\Mcp\Dependencies\ToolDependency;
|
||||
use Core\Mod\Agentic\Actions\Brain\ForgetKnowledge;
|
||||
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Remove a memory from the shared OpenBrain knowledge store.
|
||||
|
|
@ -61,43 +60,19 @@ class BrainForget extends AgentTool
|
|||
|
||||
public function handle(array $args, array $context = []): array
|
||||
{
|
||||
try {
|
||||
$id = $this->requireString($args, 'id');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$workspaceId = $context['workspace_id'] ?? null;
|
||||
if ($workspaceId === null) {
|
||||
return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai');
|
||||
}
|
||||
|
||||
$id = $args['id'] ?? '';
|
||||
$reason = $this->optionalString($args, 'reason', null, 500);
|
||||
$agentId = $context['agent_id'] ?? $context['session_id'] ?? 'anonymous';
|
||||
|
||||
return $this->withCircuitBreaker('brain', function () use ($id, $workspaceId, $reason, $agentId) {
|
||||
// Verify memory exists and belongs to this workspace
|
||||
$memory = BrainMemory::where('id', $id)
|
||||
->where('workspace_id', $workspaceId)
|
||||
->first();
|
||||
return $this->withCircuitBreaker('brain', function () use ($id, $workspaceId, $agentId, $reason) {
|
||||
$result = ForgetKnowledge::run($id, (int) $workspaceId, $agentId, $reason);
|
||||
|
||||
if (! $memory) {
|
||||
return $this->error("Memory '{$id}' not found in this workspace");
|
||||
}
|
||||
|
||||
Log::info('OpenBrain: memory forgotten', [
|
||||
'id' => $id,
|
||||
'type' => $memory->type,
|
||||
'agent_id' => $agentId,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
|
||||
app(BrainService::class)->forget($id);
|
||||
|
||||
return $this->success([
|
||||
'forgotten' => $id,
|
||||
'type' => $memory->type,
|
||||
]);
|
||||
return $this->success($result);
|
||||
}, fn () => $this->error('Brain service temporarily unavailable. Memory could not be removed.', 'service_unavailable'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||
|
||||
use Core\Mcp\Dependencies\ToolDependency;
|
||||
use Core\Mod\Agentic\Actions\Brain\ListKnowledge;
|
||||
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
|
||||
|
|
@ -73,28 +74,8 @@ class BrainList extends AgentTool
|
|||
return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai');
|
||||
}
|
||||
|
||||
$project = $this->optionalString($args, 'project');
|
||||
$type = $this->optionalEnum($args, 'type', BrainMemory::VALID_TYPES);
|
||||
$agentId = $this->optionalString($args, 'agent_id');
|
||||
$limit = $this->optionalInt($args, 'limit', 20, 1, 100);
|
||||
$result = ListKnowledge::run((int) $workspaceId, $args);
|
||||
|
||||
$query = BrainMemory::forWorkspace((int) $workspaceId)
|
||||
->active()
|
||||
->latestVersions()
|
||||
->forProject($project)
|
||||
->byAgent($agentId);
|
||||
|
||||
if ($type !== null) {
|
||||
$query->ofType($type);
|
||||
}
|
||||
|
||||
$memories = $query->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return $this->success([
|
||||
'count' => $memories->count(),
|
||||
'memories' => $memories->map(fn (BrainMemory $m) => $m->toMcpContext())->all(),
|
||||
]);
|
||||
return $this->success($result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ declare(strict_types=1);
|
|||
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||
|
||||
use Core\Mcp\Dependencies\ToolDependency;
|
||||
use Core\Mod\Agentic\Actions\Brain\RecallKnowledge;
|
||||
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
|
||||
/**
|
||||
* Semantic search across the shared OpenBrain knowledge store.
|
||||
|
|
@ -93,17 +93,12 @@ class BrainRecall extends AgentTool
|
|||
|
||||
public function handle(array $args, array $context = []): array
|
||||
{
|
||||
try {
|
||||
$query = $this->requireString($args, 'query', 2000);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$workspaceId = $context['workspace_id'] ?? null;
|
||||
if ($workspaceId === null) {
|
||||
return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai');
|
||||
}
|
||||
|
||||
$query = $args['query'] ?? '';
|
||||
$topK = $this->optionalInt($args, 'top_k', 5, 1, 20);
|
||||
$filter = $this->optional($args, 'filter', []);
|
||||
|
||||
|
|
@ -111,37 +106,11 @@ class BrainRecall extends AgentTool
|
|||
return $this->error('filter must be an object');
|
||||
}
|
||||
|
||||
// Validate filter type values if present
|
||||
if (isset($filter['type'])) {
|
||||
$typeValue = $filter['type'];
|
||||
$validTypes = BrainMemory::VALID_TYPES;
|
||||
|
||||
if (is_string($typeValue)) {
|
||||
if (! in_array($typeValue, $validTypes, true)) {
|
||||
return $this->error(sprintf('filter.type must be one of: %s', implode(', ', $validTypes)));
|
||||
}
|
||||
} elseif (is_array($typeValue)) {
|
||||
foreach ($typeValue as $t) {
|
||||
if (! is_string($t) || ! in_array($t, $validTypes, true)) {
|
||||
return $this->error(sprintf('Each filter.type value must be one of: %s', implode(', ', $validTypes)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return $this->error('filter.type must be a string or array of strings');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($filter['min_confidence'])) {
|
||||
if (! is_numeric($filter['min_confidence']) || $filter['min_confidence'] < 0.0 || $filter['min_confidence'] > 1.0) {
|
||||
return $this->error('filter.min_confidence must be a number between 0.0 and 1.0');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->withCircuitBreaker('brain', function () use ($query, $topK, $filter, $workspaceId) {
|
||||
$result = app(BrainService::class)->recall($query, $topK, $filter, (int) $workspaceId);
|
||||
return $this->withCircuitBreaker('brain', function () use ($query, $workspaceId, $filter, $topK) {
|
||||
$result = RecallKnowledge::run($query, (int) $workspaceId, $filter, $topK);
|
||||
|
||||
return $this->success([
|
||||
'count' => count($result['memories']),
|
||||
'count' => $result['count'],
|
||||
'memories' => $result['memories'],
|
||||
'scores' => $result['scores'],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ declare(strict_types=1);
|
|||
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||
|
||||
use Core\Mcp\Dependencies\ToolDependency;
|
||||
use Core\Mod\Agentic\Actions\Brain\RememberKnowledge;
|
||||
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
|
||||
/**
|
||||
* Store a memory in the shared OpenBrain knowledge store.
|
||||
|
|
@ -85,62 +85,15 @@ class BrainRemember extends AgentTool
|
|||
|
||||
public function handle(array $args, array $context = []): array
|
||||
{
|
||||
try {
|
||||
$content = $this->requireString($args, 'content', 50000);
|
||||
$type = $this->requireEnum($args, 'type', BrainMemory::VALID_TYPES);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$workspaceId = $context['workspace_id'] ?? null;
|
||||
if ($workspaceId === null) {
|
||||
return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai');
|
||||
}
|
||||
|
||||
$agentId = $context['agent_id'] ?? $context['session_id'] ?? 'anonymous';
|
||||
$tags = $this->optional($args, 'tags');
|
||||
$project = $this->optionalString($args, 'project');
|
||||
$supersedes = $this->optionalString($args, 'supersedes');
|
||||
$expiresIn = $this->optionalInt($args, 'expires_in', null, 1);
|
||||
|
||||
$confidence = $this->optional($args, 'confidence', 0.8);
|
||||
if (! is_numeric($confidence) || $confidence < 0.0 || $confidence > 1.0) {
|
||||
return $this->error('confidence must be a number between 0.0 and 1.0');
|
||||
}
|
||||
$confidence = (float) $confidence;
|
||||
|
||||
if (is_array($tags)) {
|
||||
foreach ($tags as $tag) {
|
||||
if (! is_string($tag)) {
|
||||
return $this->error('Each tag must be a string');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($supersedes !== null) {
|
||||
$existing = BrainMemory::where('id', $supersedes)
|
||||
->where('workspace_id', $workspaceId)
|
||||
->first();
|
||||
|
||||
if (! $existing) {
|
||||
return $this->error("Memory '{$supersedes}' not found in this workspace");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->withCircuitBreaker('brain', function () use (
|
||||
$content, $type, $workspaceId, $agentId, $tags, $project, $confidence, $supersedes, $expiresIn
|
||||
) {
|
||||
$memory = app(BrainService::class)->remember([
|
||||
'workspace_id' => $workspaceId,
|
||||
'agent_id' => $agentId,
|
||||
'type' => $type,
|
||||
'content' => $content,
|
||||
'tags' => $tags,
|
||||
'project' => $project,
|
||||
'confidence' => $confidence,
|
||||
'supersedes_id' => $supersedes,
|
||||
'expires_at' => $expiresIn ? now()->addHours($expiresIn) : null,
|
||||
]);
|
||||
return $this->withCircuitBreaker('brain', function () use ($args, $workspaceId, $agentId) {
|
||||
$memory = RememberKnowledge::run($args, (int) $workspaceId, $agentId);
|
||||
|
||||
return $this->success([
|
||||
'memory' => $memory->toMcpContext(),
|
||||
|
|
|
|||
27
Routes/api.php
Normal file
27
Routes/api.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Controllers\Api\BrainController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agentic API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Brain (OpenBrain knowledge store) endpoints.
|
||||
| Auto-wrapped with 'api' middleware and /api prefix by ApiRoutesRegistering.
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware(['api.auth', 'api.scope.enforce'])
|
||||
->prefix('brain')
|
||||
->name('brain.')
|
||||
->group(function () {
|
||||
Route::post('remember', [BrainController::class, 'remember'])->name('remember');
|
||||
Route::post('recall', [BrainController::class, 'recall'])->name('recall');
|
||||
Route::delete('forget/{id}', [BrainController::class, 'forget'])->name('forget')
|
||||
->where('id', '[0-9a-f-]+');
|
||||
Route::get('list', [BrainController::class, 'list'])->name('list');
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<?php
|
||||
|
||||
// API routes are registered via Core modules
|
||||
Reference in a new issue