New_Micro_Learning/backend/app/Modules/AI/Application/ProcessAiJob.php

65 خطوط
4.0 KiB
PHP

<?php
namespace App\Modules\AI\Application;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Throwable;
final class ProcessAiJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 600;
public function __construct(public readonly string $jobId) {}
public function handle(DocumentExtractor $extractor, AiProviderResolver $providers): void
{
$job = DB::table('ai_jobs')->find($this->jobId);
if (! $job || $job->cancelled_at) {
return;
}
DB::table('ai_jobs')->where('id', $this->jobId)->update(['status' => 'processing', 'progress' => 10, 'started_at' => now(), 'updated_at' => now()]);
try {
$input = json_decode($job->input ?: '{}', true);
$fragments = [];
foreach (DB::table('ai_source_documents')->where('ai_job_id', $this->jobId)->get() as $document) {
foreach ($extractor->extract(Storage::disk($document->disk)->path($document->path), $document->kind) as $position => $fragment) {
$id = (string) str()->ulid();
DB::table('ai_source_fragments')->insert(['id' => $id, 'organization_id' => $job->organization_id, 'source_document_id' => $document->id, 'position' => $position + 1, 'locator' => $fragment['locator'], 'heading' => $fragment['heading'], 'content' => $fragment['content'], 'content_hash' => hash('sha256', $fragment['content']), 'metadata' => json_encode(['schemaVersion' => 1]), 'created_at' => now(), 'updated_at' => now()]);
$fragments[] = ['id' => $id, ...$fragment];
}
}
if ($fragments === []) {
$fragments[] = ['id' => 'prompt', 'locator' => null, 'heading' => $input['topic'] ?? null, 'content' => $input['objective'] ?? $input['topic'] ?? ''];
}
DB::table('ai_jobs')->where('id', $this->jobId)->update(['progress' => 65, 'updated_at' => now()]);
try {
$proposal = $providers->byId($job->provider, $job->organization_id)->structure($fragments, $input);
} catch (Throwable $providerException) {
$fallback = $providers->fallbackForOrganization($job->organization_id, $job->provider);
if (! $fallback) {
throw $providerException;
}
$proposal = $fallback->structure($fragments, $input);
$proposal['fallbackDisclosure'] = 'The configured external provider failed; the organization fallback provider generated this draft.';
}
$suggestionId = (string) str()->ulid();
DB::table('ai_suggestions')->insert(['id' => $suggestionId, 'organization_id' => $job->organization_id, 'ai_job_id' => $this->jobId, 'suggestion_type' => 'course_draft', 'payload' => json_encode($proposal), 'confidence' => 70, 'rationale' => 'Source-grounded structure requires Designer review before acceptance.', 'status' => 'draft', 'created_at' => now(), 'updated_at' => now()]);
DB::table('ai_jobs')->where('id', $this->jobId)->update(['status' => 'completed', 'progress' => 100, 'output' => json_encode(['suggestionId' => $suggestionId]), 'input_units' => array_sum(array_map(fn ($item) => mb_strlen($item['content']), $fragments)), 'output_units' => mb_strlen(json_encode($proposal)), 'completed_at' => now(), 'updated_at' => now()]);
DB::table('subscriptions')->where('organization_id', $job->organization_id)->where('status', 'active')->whereNotNull('ai_credit_quota')->increment('ai_credits_used');
} catch (Throwable $exception) {
DB::table('ai_jobs')->where('id', $this->jobId)->update(['status' => 'failed', 'error' => mb_substr($exception->getMessage(), 0, 4000), 'updated_at' => now()]);
throw $exception;
}
}
}