'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 */ public function company(Tenant $company): array { return app(IntelligencePolicy::class)->company($company)['ai_plan'] ?? self::DEFAULTS; } /** @return array */ 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 $input * @return array */ 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 $brief * @return array */ 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]; } }