65 خطوط
4.0 KiB
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;
|
|
}
|
|
}
|
|
}
|