@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AiPreparationPlan
|
||||
{
|
||||
public const DOMAINS = ['procurement' => 'Compras', 'people' => 'Pessoas e contratações'];
|
||||
|
||||
public const SCOPES = ['process_state' => 'Estado e regras do processo', 'comparison_state' => 'Estado da comparação de propostas',
|
||||
'documentary_state' => 'Versão e actualidade documental', 'fulfilment_counts' => 'Ordem, minuta e contagens de recepção',
|
||||
'selection_state' => 'Estado da selecção de pessoas', 'candidate_counts' => 'Contagem de candidaturas',
|
||||
'clarification_counts' => 'Contagem de esclarecimentos', 'financial_counts' => 'Contagens financeiras autorizadas'];
|
||||
|
||||
public const DEFAULTS = ['state' => 'draft', 'provider' => 'none', 'model' => '', 'domains' => [], 'data_scopes' => [],
|
||||
'monthly_budget_usd' => '0.00', 'monthly_requests' => 0, 'max_input_bytes' => 4500, 'max_output_tokens' => 1000];
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function company(Tenant $company): array
|
||||
{
|
||||
return app(IntelligencePolicy::class)->company($company)['ai_plan'] ?? self::DEFAULTS;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return ['provider' => ['required', Rule::in(['none', 'openai'])],
|
||||
'model' => ['nullable', 'required_if:provider,openai', 'string', 'max:120', 'regex:/^[A-Za-z0-9][A-Za-z0-9_.-]*$/D', 'not_regex:/^sk-/i'],
|
||||
'domains' => 'present|array|list|max:2', 'domains.*' => ['string', 'distinct', Rule::in(array_keys(self::DOMAINS))],
|
||||
'data_scopes' => 'present|array|list|max:8', 'data_scopes.*' => ['string', 'distinct', Rule::in(array_keys(self::SCOPES))],
|
||||
'monthly_budget_usd' => ['required', 'string', 'regex:/^(0|[1-9][0-9]{0,4})(\.[0-9]{1,2})?$/D'],
|
||||
'monthly_requests' => 'required|integer|between:0,100000', 'max_input_bytes' => 'required|integer|between:512,64000',
|
||||
'max_output_tokens' => 'required|integer|between:64,4000', 'api_key' => 'prohibited', 'endpoint' => 'prohibited',
|
||||
'state' => 'prohibited', 'external_processing_enabled' => 'prohibited'];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function checked(array $input): array
|
||||
{
|
||||
$data = Validator::make($input, $this->rules())->validate();
|
||||
if ($data['provider'] !== 'none' && ($data['domains'] === [] || $data['data_scopes'] === [] || $data['monthly_requests'] < 1 || BigDecimal::of($data['monthly_budget_usd'])->isLessThanOrEqualTo(0))) {
|
||||
throw ValidationException::withMessages(['provider' => 'Para preparar um fornecedor, escolha percursos, dados e limites mensais positivos.']);
|
||||
}
|
||||
|
||||
return ['state' => 'draft', 'provider' => $data['provider'], 'model' => $data['model'] ?? '', 'domains' => $data['domains'], 'data_scopes' => $data['data_scopes'],
|
||||
'monthly_budget_usd' => (string) BigDecimal::of($data['monthly_budget_usd'])->toScale(2), 'monthly_requests' => (int) $data['monthly_requests'],
|
||||
'max_input_bytes' => (int) $data['max_input_bytes'], 'max_output_tokens' => (int) $data['max_output_tokens']];
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller supplies an already authorised, redacted factual brief.
|
||||
* This preview neither sends requests nor reserves a spending allowance.
|
||||
*
|
||||
* @param array<string,mixed> $brief
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function preview(Tenant $company, string $domain, array $brief): array
|
||||
{
|
||||
$plan = $this->company($company);
|
||||
$base = ['status' => 'not_configured', 'external_execution_enabled' => false, 'usage_checked' => false,
|
||||
'policy_revision' => app(IntelligencePolicy::class)->company($company)['revision'], 'provider' => $plan['provider'], 'model' => $plan['model'],
|
||||
'domain' => $domain, 'max_input_bytes' => $plan['max_input_bytes'], 'max_output_tokens' => $plan['max_output_tokens'], 'input_bytes' => 0, 'facts' => []];
|
||||
if ($plan['provider'] === 'none') {
|
||||
return $base;
|
||||
}
|
||||
if (! in_array($domain, $plan['domains'], true)) {
|
||||
return [...$base, 'status' => 'outside_scope'];
|
||||
}
|
||||
$map = match ($domain) {
|
||||
'procurement' => ['request' => 'process_state', 'comparison' => 'comparison_state', 'qualification' => 'documentary_state', 'opening' => 'documentary_state',
|
||||
'order' => 'fulfilment_counts', 'receipts' => 'fulfilment_counts', 'finance' => 'financial_counts'],
|
||||
'people' => ['rules' => 'process_state', 'candidates' => 'candidate_counts', 'selection' => 'selection_state', 'contract' => 'fulfilment_counts', 'clarifications' => 'clarification_counts'],
|
||||
default => [],
|
||||
};
|
||||
$facts = [];
|
||||
foreach ($brief['claims'] as $claim) {
|
||||
if (isset($map[$claim['id']]) && in_array($map[$claim['id']], $plan['data_scopes'], true)) {
|
||||
$facts[] = ['id' => $claim['id'], 'text' => $claim['text'], 'source' => $claim['source']];
|
||||
}
|
||||
}
|
||||
$bytes = strlen(json_encode(['facts' => $facts], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
|
||||
|
||||
return [...$base, 'status' => $facts === [] ? 'no_permitted_facts' : ($bytes > $plan['max_input_bytes'] ? 'input_exceeds_limit' : 'draft'),
|
||||
'input_bytes' => $bytes, 'facts' => $bytes > $plan['max_input_bytes'] ? [] : $facts];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user