feat(ai): finalize AI magic edits epic rollout and operations
This commit is contained in:
@@ -7,7 +7,10 @@ use App\Models\AiEditRequest;
|
||||
use App\Models\AiProviderRun;
|
||||
use App\Services\AiEditing\AiEditingRuntimeConfig;
|
||||
use App\Services\AiEditing\AiImageProviderManager;
|
||||
use App\Services\AiEditing\AiObservabilityService;
|
||||
use App\Services\AiEditing\AiStatusNotificationService;
|
||||
use App\Services\AiEditing\AiUsageLedgerService;
|
||||
use App\Services\AiEditing\Safety\AiAbuseEscalationService;
|
||||
use App\Services\AiEditing\Safety\AiSafetyPolicyService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -15,6 +18,8 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class PollAiEditRequest implements ShouldQueue
|
||||
{
|
||||
@@ -43,6 +48,9 @@ class PollAiEditRequest implements ShouldQueue
|
||||
public function handle(
|
||||
AiImageProviderManager $providers,
|
||||
AiSafetyPolicyService $safetyPolicy,
|
||||
AiAbuseEscalationService $abuseEscalation,
|
||||
AiObservabilityService $observability,
|
||||
AiStatusNotificationService $statusNotifications,
|
||||
AiEditingRuntimeConfig $runtimeConfig,
|
||||
AiUsageLedgerService $usageLedger
|
||||
): void {
|
||||
@@ -76,15 +84,37 @@ class PollAiEditRequest implements ShouldQueue
|
||||
if ($result->status === 'succeeded') {
|
||||
$outputDecision = $safetyPolicy->evaluateProviderOutput($result);
|
||||
if ($outputDecision->blocked) {
|
||||
$abuseSignal = $abuseEscalation->recordOutputBlock(
|
||||
(int) $request->tenant_id,
|
||||
(int) $request->event_id,
|
||||
'provider:'.$request->provider
|
||||
);
|
||||
$safetyReasons = $outputDecision->reasonCodes;
|
||||
if (($abuseSignal['escalated'] ?? false) && ! in_array(AiAbuseEscalationService::REASON_CODE, $safetyReasons, true)) {
|
||||
$safetyReasons[] = AiAbuseEscalationService::REASON_CODE;
|
||||
}
|
||||
$metadata = (array) ($request->metadata ?? []);
|
||||
$metadata['abuse'] = $abuseSignal;
|
||||
|
||||
$request->forceFill([
|
||||
'status' => AiEditRequest::STATUS_BLOCKED,
|
||||
'safety_state' => $outputDecision->state,
|
||||
'safety_reasons' => $outputDecision->reasonCodes,
|
||||
'safety_reasons' => $safetyReasons,
|
||||
'failure_code' => $outputDecision->failureCode ?? 'output_policy_blocked',
|
||||
'failure_message' => $outputDecision->failureMessage,
|
||||
'metadata' => $metadata,
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$observability->recordTerminalOutcome(
|
||||
$request,
|
||||
AiEditRequest::STATUS_BLOCKED,
|
||||
$run->duration_ms,
|
||||
true,
|
||||
'poll'
|
||||
);
|
||||
$statusNotifications->notifyTerminalOutcome($request->fresh());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,6 +152,15 @@ class PollAiEditRequest implements ShouldQueue
|
||||
'poll_attempt' => $this->pollAttempt,
|
||||
]);
|
||||
|
||||
$observability->recordTerminalOutcome(
|
||||
$request,
|
||||
AiEditRequest::STATUS_SUCCEEDED,
|
||||
$run->duration_ms,
|
||||
false,
|
||||
'poll'
|
||||
);
|
||||
$statusNotifications->notifyTerminalOutcome($request->fresh());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,8 +170,33 @@ class PollAiEditRequest implements ShouldQueue
|
||||
self::dispatch($request->id, $this->providerTaskId, $this->pollAttempt + 1)
|
||||
->delay(now()->addSeconds(20))
|
||||
->onQueue($runtimeConfig->queueName());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$run->forceFill([
|
||||
'status' => AiProviderRun::STATUS_FAILED,
|
||||
'finished_at' => now(),
|
||||
'duration_ms' => $run->started_at ? (int) max(0, $run->started_at->diffInMilliseconds(now())) : null,
|
||||
'error_message' => sprintf('Polling exhausted after %d attempt(s).', $maxPolls),
|
||||
])->save();
|
||||
|
||||
$request->forceFill([
|
||||
'status' => AiEditRequest::STATUS_FAILED,
|
||||
'failure_code' => 'provider_poll_timeout',
|
||||
'failure_message' => sprintf('Polling timed out after %d attempt(s).', $maxPolls),
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$observability->recordTerminalOutcome(
|
||||
$request,
|
||||
AiEditRequest::STATUS_FAILED,
|
||||
$run->duration_ms,
|
||||
false,
|
||||
'poll'
|
||||
);
|
||||
$statusNotifications->notifyTerminalOutcome($request->fresh());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,5 +208,45 @@ class PollAiEditRequest implements ShouldQueue
|
||||
'failure_message' => $result->failureMessage,
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$observability->recordTerminalOutcome(
|
||||
$request,
|
||||
$result->status === 'blocked' ? AiEditRequest::STATUS_BLOCKED : AiEditRequest::STATUS_FAILED,
|
||||
$run->duration_ms,
|
||||
$result->status === 'blocked',
|
||||
'poll'
|
||||
);
|
||||
$statusNotifications->notifyTerminalOutcome($request->fresh());
|
||||
}
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$request = AiEditRequest::query()->find($this->requestId);
|
||||
if (! $request) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! in_array($request->status, [AiEditRequest::STATUS_QUEUED, AiEditRequest::STATUS_PROCESSING], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = trim($exception->getMessage());
|
||||
$request->forceFill([
|
||||
'status' => AiEditRequest::STATUS_FAILED,
|
||||
'failure_code' => 'queue_job_failed',
|
||||
'failure_message' => $message !== ''
|
||||
? Str::limit($message, 500, '')
|
||||
: 'AI edit polling failed in queue.',
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
app(AiObservabilityService::class)->recordTerminalOutcome(
|
||||
$request,
|
||||
AiEditRequest::STATUS_FAILED,
|
||||
null,
|
||||
false,
|
||||
'poll_failed_hook'
|
||||
);
|
||||
app(AiStatusNotificationService::class)->notifyTerminalOutcome($request->fresh());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user