Files
Investidor-V2/app/Services/Ai/AiGateway.php
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

69 lines
3.9 KiB
PHP

<?php
namespace App\Services\Ai;
use App\Models\AiRun;
use App\Models\User;
use Illuminate\Support\Facades\DB;
final class AiGateway
{
public function __construct(private EvidenceRepository $evidence, private AssistiveRules $rules) {}
/** @param array<string, mixed> $input */
public function execute(User $actor, array $input): AiRun
{
abort_unless(config('investmoz_ai.enabled') && $actor->can('manage-portal'), 403);
$capability = $input['capability'];
abort_unless(in_array($capability, config('investmoz_ai.capabilities'), true), 422);
$purpose = match ($capability) {
'classify' => 'interest_triage', 'quality' => 'data_quality', 'sla' => 'execution_monitoring', default => 'investment_information',
};
abort_unless(($input['purpose'] ?? null) === $purpose, 422, 'Finalidade incompatível.');
abort_unless(DB::table('ai_controls')->where('capability', $capability)->where('suspended', false)->exists(), 503, 'Capacidade suspensa.');
$start = hrtime(true);
$language = $input['language'] ?? 'pt';
$items = match ($capability) {
'assistant', 'search' => $this->evidence->search($input['query'], $language, $input),
'summary' => $this->evidence->excerpts($input['source_id']),
'recommend' => $this->rules->recommend($input),
'classify' => $this->rules->classify((int) $input['interest_id']),
'quality' => $this->rules->quality(),
'sla' => $this->rules->sla(),
default => abort(422),
};
$mode = match ($capability) {
'assistant', 'search' => 'lexical_evidence', 'summary' => 'extractive_preview', default => 'structured_rules',
};
$result = [
'items' => $items, 'mode' => $mode, 'model' => null, 'language' => $language,
'status' => $items === [] ? 'no_evidence_or_matches' : 'review_required',
'confidence' => 'not_calibrated', 'generated_claims' => [], 'predictions' => [],
'limitations' => ['not_an_official_decision', 'no_return_or_approval_guarantee', 'no_generative_model',
'no_semantic_search', 'human_review_required', 'bounded_results_not_exhaustive'],
'computed_at' => now()->toIso8601String(),
'human_handoff_url' => '/management/ai',
];
$sourceIds = array_values(array_unique(array_filter(array_column($items, 'source_id'))));
return DB::transaction(function () use ($actor, $input, $capability, $mode, $result, $sourceIds, $start): AiRun {
// Recheck suspension and evidence immediately before retaining a result.
abort_unless(DB::table('ai_controls')->where('capability', $capability)->lockForUpdate()->value('suspended') === false, 503);
abort_unless($this->evidence->allStillEligible($sourceIds), 409, 'A autorização de uma fonte mudou.');
$run = AiRun::create([
'actor_id' => $actor->id, 'capability' => $capability, 'purpose' => $input['purpose'], 'mode' => $mode,
'rules_version' => config('investmoz_ai.rules_version'), 'prompt_version' => config('investmoz_ai.prompt_version'),
'input_hash' => hash_hmac('sha256', json_encode($input, JSON_THROW_ON_ERROR), (string) config('app.key')),
'result' => $result, 'source_ids' => $sourceIds,
'latency_ms' => (int) ((hrtime(true) - $start) / 1000000),
]);
DB::table('audit_events')->insert(['source_system' => 'ai-gateway', 'entity_type' => 'ai_runs',
'entity_id' => $run->id, 'actor_user_id' => $actor->id, 'action' => 'assistance_generated',
'after_data' => json_encode(['capability' => $capability, 'mode' => $mode, 'source_ids' => $sourceIds,
'rules_version' => config('investmoz_ai.rules_version'), 'latency_ms' => $run->latency_ms], JSON_THROW_ON_ERROR)]);
return $run;
});
}
}