Teknologi 04 Aug 2026 25 Kali Dibaca

Cara Membuat Fitur Tanya Jawab Dokumen PDF dengan Laravel dan AI

Gugun Nurdiansyah
Penulis
Cara Membuat Fitur Tanya Jawab Dokumen PDF dengan Laravel dan AI

Dokumen PDF masih banyak digunakan untuk menyimpan berbagai informasi penting, seperti:

  • Standar operasional perusahaan

  • Proposal bisnis

  • Kontrak kerja

  • Laporan keuangan

  • Modul pembelajaran

  • Buku panduan

  • Dokumen kebijakan

  • Dokumentasi produk

  • Rekam laporan proyek

  • Dokumen teknis

Masalahnya, pengguna harus membaca banyak halaman untuk menemukan informasi tertentu. Proses tersebut dapat memakan waktu, terutama apabila dokumen memiliki puluhan atau ratusan halaman.

Dengan bantuan Artificial Intelligence, pengguna dapat mengunggah PDF kemudian mengajukan pertanyaan menggunakan bahasa sehari-hari.

Contohnya:

  • Apa kesimpulan utama dokumen ini?

  • Siapa pihak yang bertanggung jawab dalam proyek?

  • Berapa nilai kontrak yang disebutkan?

  • Apa saja risiko yang dijelaskan?

  • Bagaimana prosedur pengajuan barang?

  • Kapan masa berlaku perjanjian berakhir?

  • Tolong rangkum dokumen ini menjadi lima poin.

Laravel dapat digunakan sebagai backend untuk mengelola upload, pengguna, database, keamanan, dan antarmuka aplikasi. Sementara itu, OpenAI API dapat memproses dokumen dan menghasilkan jawaban berdasarkan isinya.

OpenAI Responses API mendukung dokumen sebagai input_file. File dapat diberikan melalui ID dari Files API, URL eksternal, atau data Base64. Untuk PDF, model dengan kemampuan visual dapat memproses teks yang diekstrak sekaligus gambar halaman di dalam dokumen.

Apa Itu Fitur Tanya Jawab Dokumen PDF?

Fitur tanya jawab dokumen PDF adalah sistem yang memungkinkan pengguna berinteraksi dengan isi dokumen melalui percakapan.

Pengguna tidak harus mencari kata tertentu secara manual. Sistem AI akan membaca atau mencari bagian yang relevan, kemudian menyusun jawaban berdasarkan pertanyaan pengguna.

Fitur ini dapat dikembangkan untuk:

  • Sistem knowledge base perusahaan

  • Portal dokumen karyawan

  • Aplikasi pembelajaran

  • Sistem analisis kontrak

  • Aplikasi pelayanan pelanggan

  • Sistem pencarian SOP

  • Aplikasi pengelolaan dokumen

  • Sistem informasi hukum

  • Portal laporan perusahaan

  • Aplikasi SaaS berbasis AI

Bagaimana Cara Kerja Tanya Jawab PDF?

Alur dasarnya adalah sebagai berikut:

  1. Pengguna mengunggah dokumen PDF.

  2. Laravel memvalidasi jenis dan ukuran file.

  3. File disimpan pada storage privat.

  4. Laravel mengunggah file ke OpenAI Files API.

  5. OpenAI mengembalikan ID file.

  6. ID file disimpan pada database Laravel.

  7. Pengguna mengajukan pertanyaan.

  8. Laravel mengirim ID file dan pertanyaan ke Responses API.

  9. Model menganalisis isi dokumen.

  10. Jawaban dikembalikan kepada Laravel.

  11. Laravel menampilkan jawaban kepada pengguna.

  12. Pertanyaan dan jawaban dapat disimpan sebagai riwayat.

Arsitektur sederhananya:

Pengguna
   ↓
Form Upload Laravel
   ↓
Private Storage
   ↓
OpenAI Files API
   ↓
OpenAI File ID
   ↓
Database Laravel
   ↓
Pertanyaan Pengguna
   ↓
OpenAI Responses API
   ↓
Jawaban Berdasarkan PDF

Dua Metode Membuat Tanya Jawab PDF

Terdapat dua pendekatan utama yang dapat digunakan.

1. Direct File Input

Pada metode ini, ID dokumen PDF dikirim langsung bersama pertanyaan ke Responses API.

Metode ini cocok untuk:

  • Satu dokumen per percakapan

  • Prototipe

  • Dokumen berukuran kecil atau menengah

  • Sistem analisis dokumen sederhana

  • Penggunaan yang belum terlalu banyak

OpenAI dapat menerima file_id dan input_text dalam satu input Responses API. Untuk file yang akan langsung digunakan sebagai input model, dokumentasi OpenAI merekomendasikan purpose user_data.

2. File Search dan Vector Store

Pada metode ini, dokumen dimasukkan ke dalam vector store. Ketika pengguna bertanya, sistem mencari bagian dokumen yang paling relevan sebelum model menghasilkan jawaban.

Metode ini cocok untuk:

  • Banyak dokumen

  • Knowledge base perusahaan

  • Ribuan halaman

  • Dokumen yang sering ditanyakan

  • Sistem dengan banyak pengguna

  • Aplikasi SaaS berbasis dokumen

File Search merupakan tool pada Responses API yang dapat melakukan pencarian semantik dan kata kunci pada file yang tersimpan dalam vector store. Proses pencarian dikelola sebagai hosted tool sehingga aplikasi tidak perlu membangun seluruh mekanisme pencarian vektor secara manual.

Artikel ini akan menggunakan Direct File Input untuk implementasi utama karena lebih sederhana. Pada bagian akhir akan dijelaskan cara mengembangkannya menggunakan File Search.

Persiapan Sistem

Sebelum memulai, siapkan:

  • PHP

  • Composer

  • Laravel

  • Database MySQL atau PostgreSQL

  • OpenAI API key

  • Akun pengguna Laravel

  • Koneksi internet pada server

  • Web server untuk produksi

Struktur aplikasi yang akan dibuat:

app/
├── Http/
│   └── Controllers/
│       └── PdfQuestionController.php
├── Models/
│   ├── Document.php
│   └── DocumentQuestion.php
└── Services/
    └── OpenAIPdfService.php

resources/
└── views/
    └── documents/
        ├── index.blade.php
        └── show.blade.php

1. Membuat Project Laravel

Buat project Laravel:

composer create-project laravel/laravel pdf-question-answer
cd pdf-question-answer

Atur koneksi database pada .env:

APP_NAME="PDF AI"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pdf_ai
DB_USERNAME=root
DB_PASSWORD=

Jalankan migration awal:

php artisan migrate

Jalankan aplikasi:

php artisan serve

2. Menambahkan OpenAI API Key

Tambahkan konfigurasi berikut ke file .env:

OPENAI_API_KEY=masukkan_api_key_anda
OPENAI_MODEL=gpt-5.6
OPENAI_BASE_URL=https://api.openai.com/v1

Jangan menulis API key langsung di controller atau JavaScript. API key harus tetap berada di backend dan tidak boleh dikirim ke browser.

Buka:

config/services.php

Tambahkan:

'openai' => [
    'api_key' => env('OPENAI_API_KEY'),
    'model' => env('OPENAI_MODEL', 'gpt-5.6'),
    'base_url' => env(
        'OPENAI_BASE_URL',
        'https://api.openai.com/v1'
    ),
],

Setelah memperbarui konfigurasi pada server produksi, jalankan:

php artisan config:clear
php artisan config:cache

3. Membuat Tabel Dokumen

Buat model dan migration:

php artisan make:model Document -m

Isi migration tabel documents:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('documents', function (Blueprint $table) {
            $table->id();

            $table->foreignId('user_id')
                ->constrained()
                ->cascadeOnDelete();

            $table->string('title');
            $table->string('original_name');
            $table->string('disk')->default('local');
            $table->string('path');
            $table->string('mime_type');
            $table->unsignedBigInteger('size');

            $table->string('openai_file_id')
                ->nullable()
                ->index();

            $table->string('vector_store_id')
                ->nullable()
                ->index();

            $table->string('status')
                ->default('processing')
                ->index();

            $table->text('error_message')->nullable();
            $table->timestamp('processed_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('documents');
    }
};

Status dokumen dapat menggunakan nilai:

  • processing

  • ready

  • failed

  • deleted

Model Document:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Document extends Model
{
    protected $fillable = [
        'user_id',
        'title',
        'original_name',
        'disk',
        'path',
        'mime_type',
        'size',
        'openai_file_id',
        'vector_store_id',
        'status',
        'error_message',
        'processed_at',
    ];

    protected function casts(): array
    {
        return [
            'size' => 'integer',
            'processed_at' => 'datetime',
        ];
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function questions(): HasMany
    {
        return $this->hasMany(DocumentQuestion::class);
    }
}

4. Membuat Tabel Riwayat Pertanyaan

Buat model dan migration:

php artisan make:model DocumentQuestion -m

Isi migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create(
            'document_questions',
            function (Blueprint $table) {
                $table->id();

                $table->foreignId('document_id')
                    ->constrained()
                    ->cascadeOnDelete();

                $table->foreignId('user_id')
                    ->constrained()
                    ->cascadeOnDelete();

                $table->text('question');
                $table->longText('answer');
                $table->string('openai_response_id')->nullable();
                $table->json('usage')->nullable();
                $table->timestamps();

                $table->index([
                    'document_id',
                    'created_at',
                ]);
            }
        );
    }

    public function down(): void
    {
        Schema::dropIfExists('document_questions');
    }
};

Model DocumentQuestion:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class DocumentQuestion extends Model
{
    protected $fillable = [
        'document_id',
        'user_id',
        'question',
        'answer',
        'openai_response_id',
        'usage',
    ];

    protected function casts(): array
    {
        return [
            'usage' => 'array',
        ];
    }

    public function document(): BelongsTo
    {
        return $this->belongsTo(Document::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Jalankan migration:

php artisan migrate

5. Membuat OpenAI PDF Service

Buat file:

app/Services/OpenAIPdfService.php

Isi dengan kode berikut:

<?php

namespace App\Services;

use App\Models\Document;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

class OpenAIPdfService
{
    private function client(): PendingRequest
    {
        $apiKey = config('services.openai.api_key');

        if (! is_string($apiKey) || $apiKey === '') {
            throw new RuntimeException(
                'OPENAI_API_KEY belum dikonfigurasi.'
            );
        }

        return Http::baseUrl(
            config('services.openai.base_url')
        )
            ->withToken($apiKey)
            ->acceptJson()
            ->connectTimeout(15)
            ->timeout(120)
            ->retry(2, 500, throw: false);
    }

    /**
     * Mengunggah PDF ke OpenAI Files API.
     */
    public function upload(Document $document): string
    {
        $absolutePath = Storage::disk($document->disk)
            ->path($document->path);

        if (! is_file($absolutePath)) {
            throw new RuntimeException(
                'File PDF tidak ditemukan pada storage.'
            );
        }

        $handle = fopen($absolutePath, 'rb');

        if ($handle === false) {
            throw new RuntimeException(
                'File PDF tidak dapat dibaca.'
            );
        }

        try {
            $response = $this->client()
                ->attach(
                    'file',
                    $handle,
                    $document->original_name
                )
                ->post('/files', [
                    'purpose' => 'user_data',
                ]);
        } finally {
            fclose($handle);
        }

        $this->ensureSuccessful($response);

        $fileId = $response->json('id');

        if (! is_string($fileId) || $fileId === '') {
            throw new RuntimeException(
                'OpenAI tidak mengembalikan file ID.'
            );
        }

        return $fileId;
    }

    /**
     * Mengajukan pertanyaan berdasarkan satu PDF.
     *
     * @return array{
     *     response_id: string|null,
     *     answer: string,
     *     usage: array<string, mixed>
     * }
     */
    public function ask(
        Document $document,
        string $question
    ): array {
        if (
            ! is_string($document->openai_file_id) ||
            $document->openai_file_id === ''
        ) {
            throw new RuntimeException(
                'Dokumen belum siap digunakan.'
            );
        }

        $response = $this->client()->post('/responses', [
            'model' => config('services.openai.model'),
            'instructions' => $this->instructions(),
            'input' => [
                [
                    'role' => 'user',
                    'content' => [
                        [
                            'type' => 'input_file',
                            'file_id' => $document->openai_file_id,
                        ],
                        [
                            'type' => 'input_text',
                            'text' => $question,
                        ],
                    ],
                ],
            ],
        ]);

        $this->ensureSuccessful($response);

        $answer = $this->extractOutputText(
            collect($response->json('output', []))
        );

        if ($answer === '') {
            throw new RuntimeException(
                'AI tidak menghasilkan jawaban.'
            );
        }

        return [
            'response_id' => $response->json('id'),
            'answer' => $answer,
            'usage' => $response->json('usage', []),
        ];
    }

    /**
     * Menghapus file dari OpenAI.
     */
    public function delete(string $fileId): void
    {
        $response = $this->client()
            ->delete("/files/{$fileId}");

        $this->ensureSuccessful($response);
    }

    private function instructions(): string
    {
        return <<<'PROMPT'
Anda adalah asisten tanya jawab dokumen.

Jawab hanya berdasarkan informasi yang terdapat dalam dokumen PDF.
Gunakan bahasa Indonesia yang profesional dan mudah dipahami.
Jika jawaban tidak ditemukan dalam dokumen, katakan dengan jelas bahwa informasinya tidak tersedia.
Jangan menambahkan fakta, angka, nama, tanggal, atau kesimpulan yang tidak didukung dokumen.
Gunakan daftar poin apabila jawaban memiliki beberapa bagian.
Jangan menyebut nomor halaman jika tidak dapat dipastikan dengan akurat.
PROMPT;
    }

    private function ensureSuccessful(
        Response $response
    ): void {
        if ($response->successful()) {
            return;
        }

        logger()->error('OpenAI PDF request failed', [
            'status' => $response->status(),
            'error_type' => $response->json('error.type'),
            'error_code' => $response->json('error.code'),
        ]);

        throw new RuntimeException(
            $response->json(
                'error.message',
                'Permintaan ke layanan AI gagal.'
            )
        );
    }

    private function extractOutputText(
        Collection $output
    ): string {
        return $output
            ->where('type', 'message')
            ->flatMap(function (array $item): array {
                return $item['content'] ?? [];
            })
            ->where('type', 'output_text')
            ->pluck('text')
            ->filter()
            ->implode("\n");
    }
}

Laravel menyediakan HTTP client untuk melakukan request ke layanan eksternal serta mendukung bearer token, multipart attachment, timeout, dan retry.

OpenAI Responses API dapat menerima PDF menggunakan file_id yang sebelumnya diperoleh melalui Files API. File dan pertanyaan dikirim sebagai input_file serta input_text dalam satu pesan pengguna.

6. Membuat Controller Upload dan Tanya Jawab

Buat controller:

php artisan make:controller PdfQuestionController

Isi file:

<?php

namespace App\Http\Controllers;

use App\Models\Document;
use App\Services\OpenAIPdfService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;
use Illuminate\View\View;
use Throwable;

class PdfQuestionController extends Controller
{
    public function index(Request $request): View
    {
        $documents = Document::query()
            ->where('user_id', $request->user()->id)
            ->latest()
            ->paginate(10);

        return view('documents.index', compact('documents'));
    }

    public function store(
        Request $request,
        OpenAIPdfService $pdfService
    ): RedirectResponse {
        $validated = $request->validate([
            'title' => [
                'required',
                'string',
                'max:150',
            ],
            'document' => [
                'required',
                File::types(['pdf'])->max('20mb'),
            ],
        ]);

        $uploadedFile = $request->file('document');

        $path = $uploadedFile->store(
            "documents/{$request->user()->id}",
            'local'
        );

        $document = Document::create([
            'user_id' => $request->user()->id,
            'title' => $validated['title'],
            'original_name' => $uploadedFile
                ->getClientOriginalName(),
            'disk' => 'local',
            'path' => $path,
            'mime_type' => $uploadedFile
                ->getMimeType() ?? 'application/pdf',
            'size' => $uploadedFile->getSize(),
            'status' => 'processing',
        ]);

        try {
            $fileId = $pdfService->upload($document);

            $document->update([
                'openai_file_id' => $fileId,
                'status' => 'ready',
                'processed_at' => now(),
                'error_message' => null,
            ]);
        } catch (Throwable $exception) {
            report($exception);

            $document->update([
                'status' => 'failed',
                'error_message' => $exception->getMessage(),
            ]);

            return back()->withErrors([
                'document' => 'Dokumen gagal diproses.',
            ]);
        }

        return to_route('documents.show', $document)
            ->with(
                'success',
                'Dokumen berhasil diunggah dan siap ditanyakan.'
            );
    }

    public function show(
        Request $request,
        Document $document
    ): View {
        $this->ensureOwner($request, $document);

        $document->load([
            'questions' => fn ($query) => $query->oldest(),
        ]);

        return view('documents.show', compact('document'));
    }

    public function ask(
        Request $request,
        Document $document,
        OpenAIPdfService $pdfService
    ): JsonResponse {
        $this->ensureOwner($request, $document);

        $validated = $request->validate([
            'question' => [
                'required',
                'string',
                'min:3',
                'max:3000',
            ],
        ]);

        if ($document->status !== 'ready') {
            return response()->json([
                'success' => false,
                'message' => 'Dokumen belum siap digunakan.',
            ], 422);
        }

        try {
            $result = $pdfService->ask(
                $document,
                $validated['question']
            );

            $history = $document->questions()->create([
                'user_id' => $request->user()->id,
                'question' => $validated['question'],
                'answer' => $result['answer'],
                'openai_response_id' => $result['response_id'],
                'usage' => $result['usage'],
            ]);

            return response()->json([
                'success' => true,
                'question' => $history->question,
                'answer' => $history->answer,
            ]);
        } catch (Throwable $exception) {
            report($exception);

            return response()->json([
                'success' => false,
                'message' => 'Pertanyaan gagal diproses.',
            ], 500);
        }
    }

    public function destroy(
        Request $request,
        Document $document,
        OpenAIPdfService $pdfService
    ): RedirectResponse {
        $this->ensureOwner($request, $document);

        if ($document->openai_file_id) {
            try {
                $pdfService->delete(
                    $document->openai_file_id
                );
            } catch (Throwable $exception) {
                report($exception);
            }
        }

        Storage::disk($document->disk)
            ->delete($document->path);

        $document->delete();

        return to_route('documents.index')
            ->with('success', 'Dokumen berhasil dihapus.');
    }

    private function ensureOwner(
        Request $request,
        Document $document
    ): void {
        abort_unless(
            $document->user_id === $request->user()->id,
            403
        );
    }
}

Laravel dapat memvalidasi jenis dan ukuran file melalui file validation rule. Uploaded file juga dapat disimpan menggunakan filesystem dengan metode store.

Pada contoh ini, batas upload aplikasi ditetapkan sebesar 20 MB. OpenAI saat ini menetapkan bahwa setiap file yang dikirim sebagai direct file input harus berukuran kurang dari 50 MB dan total file dalam satu request juga dibatasi 50 MB.

7. Membuat Route

Tambahkan route berikut ke:

routes/web.php
<?php

use App\Http\Controllers\PdfQuestionController;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth'])->group(function () {
    Route::get('/documents', [
        PdfQuestionController::class,
        'index',
    ])->name('documents.index');

    Route::post('/documents', [
        PdfQuestionController::class,
        'store',
    ])->name('documents.store');

    Route::get('/documents/{document}', [
        PdfQuestionController::class,
        'show',
    ])->name('documents.show');

    Route::post('/documents/{document}/ask', [
        PdfQuestionController::class,
        'ask',
    ])
        ->middleware('throttle:20,1')
        ->name('documents.ask');

    Route::delete('/documents/{document}', [
        PdfQuestionController::class,
        'destroy',
    ])->name('documents.destroy');
});

Middleware autentikasi memastikan fitur hanya dapat digunakan oleh pengguna yang telah login. Rate limiting membantu mengurangi spam dan penggunaan API yang tidak terkendali.

8. Membuat Halaman Upload PDF

Buat file:

resources/views/documents/index.blade.php

Contoh isi sederhana:

@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Dokumen PDF</h1>

    @if (session('success'))
        <div class="alert alert-success">
            {{ session('success') }}
        </div>
    @endif

    @if ($errors->any())
        <div class="alert alert-danger">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form
        action="{{ route('documents.store') }}"
        method="POST"
        enctype="multipart/form-data"
    >
        @csrf

        <div>
            <label for="title">Judul Dokumen</label>

            <input
                id="title"
                type="text"
                name="title"
                value="{{ old('title') }}"
                required
            >
        </div>

        <div>
            <label for="document">File PDF</label>

            <input
                id="document"
                type="file"
                name="document"
                accept="application/pdf"
                required
            >
        </div>

        <button type="submit">
            Upload dan Proses
        </button>
    </form>

    <hr>

    <h2>Daftar Dokumen</h2>

    @forelse ($documents as $document)
        <article>
            <h3>
                <a href="{{ route(
                    'documents.show',
                    $document
                ) }}">
                    {{ $document->title }}
                </a>
            </h3>

            <p>
                File: {{ $document->original_name }}
            </p>

            <p>
                Status: {{ $document->status }}
            </p>
        </article>
    @empty
        <p>Belum ada dokumen.</p>
    @endforelse

    {{ $documents->links() }}
</div>
@endsection

9. Membuat Halaman Tanya Jawab

Buat file:

resources/views/documents/show.blade.php

Contoh:

@extends('layouts.app')

@section('content')
<div class="container">
    <header>
        <h1>{{ $document->title }}</h1>

        <p>
            {{ $document->original_name }}
        </p>
    </header>

    <section
        id="conversation"
        aria-live="polite"
    >
        @forelse ($document->questions as $item)
            <div class="question">
                <strong>Pertanyaan:</strong>
                <p>{{ $item->question }}</p>
            </div>

            <div class="answer">
                <strong>Jawaban AI:</strong>
                <p>{{ $item->answer }}</p>
            </div>
        @empty
            <p id="emptyMessage">
                Belum ada pertanyaan.
            </p>
        @endforelse
    </section>

    <form id="questionForm">
        @csrf

        <label for="question">
            Tanyakan isi dokumen
        </label>

        <textarea
            id="question"
            name="question"
            maxlength="3000"
            required
        ></textarea>

        <button id="submitButton" type="submit">
            Kirim Pertanyaan
        </button>
    </form>
</div>

<script>
    const form = document.getElementById('questionForm');
    const questionInput = document.getElementById('question');
    const submitButton = document.getElementById(
        'submitButton'
    );
    const conversation = document.getElementById(
        'conversation'
    );

    function addMessage(label, content, className) {
        const wrapper = document.createElement('div');
        const title = document.createElement('strong');
        const text = document.createElement('p');

        wrapper.className = className;
        title.textContent = label;
        text.textContent = content;

        wrapper.appendChild(title);
        wrapper.appendChild(text);
        conversation.appendChild(wrapper);

        return wrapper;
    }

    form.addEventListener('submit', async (event) => {
        event.preventDefault();

        const question = questionInput.value.trim();

        if (!question) {
            return;
        }

        document.getElementById('emptyMessage')?.remove();

        addMessage(
            'Pertanyaan:',
            question,
            'question'
        );

        const loading = addMessage(
            'Jawaban AI:',
            'Sedang membaca dokumen...',
            'answer'
        );

        questionInput.value = '';
        submitButton.disabled = true;

        try {
            const response = await fetch(
                '{{ route('documents.ask', $document) }}',
                {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/json',
                        'X-CSRF-TOKEN':
                            '{{ csrf_token() }}',
                    },
                    body: JSON.stringify({ question }),
                }
            );

            const data = await response.json();

            if (!response.ok || !data.success) {
                throw new Error(
                    data.message ||
                    'Pertanyaan gagal diproses.'
                );
            }

            loading.querySelector('p').textContent =
                data.answer;
        } catch (error) {
            loading.querySelector('p').textContent =
                error.message;
        } finally {
            submitButton.disabled = false;
            questionInput.focus();
        }
    });
</script>
@endsection

Gunakan textContent ketika menampilkan jawaban dari AI. Jangan langsung memasukkan jawaban ke innerHTML, kecuali hasil sudah melalui proses sanitasi yang aman.

10. Menguji Fitur Tanya Jawab PDF

Jalankan aplikasi:

php artisan serve

Buka:

http://127.0.0.1:8000/documents

Lakukan pengujian berikut:

  1. Login ke aplikasi.

  2. Masukkan judul dokumen.

  3. Pilih file PDF.

  4. Klik tombol upload.

  5. Tunggu hingga status menjadi ready.

  6. Buka halaman dokumen.

  7. Ajukan pertanyaan berdasarkan isi PDF.

  8. Periksa apakah jawaban sesuai dengan dokumen.

Contoh pertanyaan:

Tolong rangkum dokumen ini menjadi lima poin.
Apa tujuan utama proyek yang dijelaskan?
Berapa nilai anggaran yang tercantum?
Siapa pihak yang bertanggung jawab?
Apakah dokumen menyebutkan batas waktu pekerjaan?

11. Bagaimana PDF Hasil Scan Diproses?

Sebagian PDF tidak memiliki lapisan teks karena berasal dari hasil scan.

Pada direct file input, OpenAI dapat memproses teks yang diekstrak dan gambar halaman PDF menggunakan model yang mendukung kemampuan visual. Hal tersebut membantu sistem membaca dokumen yang memiliki tabel, diagram, maupun halaman berbentuk gambar. Namun, hasilnya tetap dipengaruhi oleh ketajaman scan, ukuran tulisan, orientasi halaman, dan kualitas dokumen.

Untuk meningkatkan hasil:

  • Gunakan scan dengan resolusi yang jelas.

  • Pastikan halaman tidak miring.

  • Hindari gambar yang terlalu gelap.

  • Pastikan teks tidak terpotong.

  • Gunakan PDF dengan teks asli apabila tersedia.

  • Lakukan OCR sebelum upload apabila hasil scan buruk.

12. Keterbatasan Direct File Input

Direct file input mudah diterapkan, tetapi memiliki beberapa keterbatasan.

Dokumen Diproses dalam Konteks Model

PDF dapat menyertakan teks dan gambar halaman dalam konteks. Hal ini dapat meningkatkan penggunaan token, terutama untuk dokumen panjang atau dokumen yang memiliki banyak elemen visual.

Kurang Efisien untuk Banyak Dokumen

Apabila pengguna memiliki puluhan atau ratusan PDF, mengirim satu dokumen penuh pada setiap pertanyaan menjadi kurang efisien.

Pencarian Lintas Dokumen Terbatas

Direct file input lebih sesuai ketika pengguna sudah memilih dokumen tertentu. Untuk pencarian dalam banyak dokumen, gunakan vector store dan File Search.

Biaya Perlu Dipantau

Dokumen panjang dapat meningkatkan jumlah input yang diproses. Aplikasi sebaiknya mencatat usage, menerapkan batas harian, dan menyediakan pembatasan berdasarkan paket pengguna.

13. Mengembangkan Sistem Menggunakan File Search

Untuk sistem berskala lebih besar, dokumen dapat dimasukkan ke vector store.

Alurnya:

  1. Upload file ke Files API.

  2. Buat vector store.

  3. Tambahkan file ke vector store.

  4. Tunggu status file menjadi completed.

  5. Kirim pertanyaan melalui Responses API.

  6. Aktifkan tool file_search.

  7. Model mencari bagian dokumen yang relevan.

  8. Model menghasilkan jawaban beserta referensi file.

OpenAI mengharuskan knowledge base dibuat dalam vector store sebelum File Search digunakan. Setelah file ditambahkan, status pemrosesan perlu diperiksa hingga menjadi completed.

Contoh method untuk membuat vector store:

public function createVectorStore(
    string $name
): string {
    $response = $this->client()
        ->post('/vector_stores', [
            'name' => $name,
        ]);

    $this->ensureSuccessful($response);

    return $response->json('id');
}

Tambahkan file ke vector store:

public function addFileToVectorStore(
    string $vectorStoreId,
    string $fileId
): void {
    $response = $this->client()->post(
        "/vector_stores/{$vectorStoreId}/files",
        [
            'file_id' => $fileId,
        ]
    );

    $this->ensureSuccessful($response);
}

Ajukan pertanyaan menggunakan File Search:

public function askWithFileSearch(
    string $vectorStoreId,
    string $question
): array {
    $response = $this->client()->post('/responses', [
        'model' => config('services.openai.model'),

        'instructions' => <<<'PROMPT'
Jawab hanya berdasarkan dokumen yang ditemukan.
Jika informasi tidak tersedia, katakan bahwa jawabannya
tidak ditemukan dalam dokumen.
PROMPT,

        'input' => $question,

        'tools' => [
            [
                'type' => 'file_search',
                'vector_store_ids' => [
                    $vectorStoreId,
                ],
                'max_num_results' => 5,
            ],
        ],

        'include' => [
            'file_search_call.results',
        ],
    ]);

    $this->ensureSuccessful($response);

    return [
        'response_id' => $response->json('id'),
        'answer' => $this->extractOutputText(
            collect($response->json('output', []))
        ),
        'output' => $response->json('output', []),
        'usage' => $response->json('usage', []),
    ];
}

Parameter max_num_results dapat digunakan untuk membatasi jumlah hasil pencarian. Hasil pencarian lengkap juga dapat disertakan menggunakan include: ["file_search_call.results"].

Ketika File Search digunakan, respons dapat berisi item file_search_call dan item message. Pesan model juga dapat menyertakan citation atau referensi ke file sumber.

14. Direct File Input atau File Search?

Gunakan Direct File Input Apabila:

  • Pengguna hanya bertanya pada satu PDF.

  • Sistem masih berupa prototipe.

  • Jumlah dokumen sedikit.

  • Dokumen tidak terlalu panjang.

  • Implementasi harus dibuat dengan cepat.

  • Pengguna selalu memilih dokumen sebelum bertanya.

Gunakan File Search Apabila:

  • Sistem mengelola banyak dokumen.

  • Pertanyaan dapat mencari lintas dokumen.

  • Dokumen sering digunakan berulang kali.

  • Aplikasi memiliki banyak pengguna.

  • Dibutuhkan knowledge base perusahaan.

  • Jawaban perlu menyertakan referensi file.

  • Sistem akan dikembangkan menjadi SaaS.

15. Memproses Dokumen Menggunakan Queue

Proses upload dan indexing dapat membutuhkan waktu. Untuk aplikasi produksi, pekerjaan tersebut sebaiknya dipindahkan ke Laravel Queue.

Contoh alur:

  1. Pengguna mengunggah PDF.

  2. Laravel menyimpan dokumen dengan status processing.

  3. Controller melakukan dispatch job.

  4. Queue worker mengunggah file ke OpenAI.

  5. Job memperbarui status menjadi ready.

  6. Pengguna menerima notifikasi.

Buat job:

php artisan make:job ProcessPdfDocument

Contoh dispatch:

ProcessPdfDocument::dispatch($document->id)
    ->afterCommit();

Laravel Queue memungkinkan pekerjaan yang membutuhkan waktu lebih panjang diproses di luar request utama sehingga halaman tidak harus menunggu seluruh pekerjaan selesai.

16. Keamanan Fitur Tanya Jawab PDF

Dokumen perusahaan dapat mengandung informasi rahasia. Karena itu, keamanan harus menjadi bagian utama dalam pengembangan.

Simpan PDF pada Storage Privat

Gunakan disk local atau private object storage. Jangan meletakkan semua dokumen pada folder publik.

Gunakan Autentikasi

Pastikan hanya pengguna yang telah login yang dapat mengunggah dan membaca dokumen.

Periksa Kepemilikan Dokumen

Jangan hanya mengandalkan ID pada URL. Sistem harus memastikan bahwa dokumen benar-benar dimiliki pengguna yang sedang login.

Validasi Jenis dan Ukuran File

Jangan hanya memeriksa ekstensi dari nama file. Gunakan validasi MIME type dan batas ukuran.

Batasi Jumlah Request

Gunakan rate limiting untuk mencegah spam dan biaya API berlebihan.

Jangan Tampilkan Output sebagai HTML Mentah

Gunakan Blade escaping atau JavaScript textContent.

Simpan API Key pada Server

API key tidak boleh berada di frontend, repository, atau file yang dapat diakses publik.

Batasi Akses Multi-Tenant

Untuk aplikasi SaaS, tambahkan tenant_id pada tabel:

  • documents

  • document_questions

  • vector_stores

Setiap query harus dibatasi berdasarkan tenant aktif.

Sediakan Penghapusan Data

Ketika pengguna menghapus dokumen, hapus:

  • File pada storage Laravel

  • Data dokumen dalam database

  • Riwayat pertanyaan apabila diperlukan

  • File yang tersimpan pada penyedia AI

  • File dari vector store apabila digunakan

Jangan Mengirim Data yang Tidak Diperlukan

Hindari mengunggah dokumen sensitif apabila pemrosesan melalui layanan eksternal tidak diizinkan oleh kebijakan perusahaan.

17. Mengurangi Risiko Jawaban yang Salah

Model AI tetap dapat menghasilkan jawaban yang kurang akurat. Beberapa langkah yang dapat diterapkan adalah:

  • Instruksikan model untuk menjawab hanya dari dokumen.

  • Minta model mengakui apabila jawaban tidak ditemukan.

  • Gunakan File Search untuk mengambil konteks relevan.

  • Tampilkan referensi dokumen.

  • Batasi jumlah hasil pencarian secara wajar.

  • Simpan pertanyaan yang gagal dijawab.

  • Sediakan tombol koreksi atau feedback.

  • Lakukan evaluasi menggunakan kumpulan pertanyaan uji.

  • Jangan menggunakan jawaban AI sebagai keputusan final untuk urusan hukum, kesehatan, atau keuangan.

  • Sediakan pemeriksaan manusia untuk keputusan penting.

18. Fitur Lanjutan yang Dapat Ditambahkan

Setelah sistem dasar berjalan, aplikasi dapat dikembangkan dengan:

  • Upload beberapa PDF sekaligus

  • Folder dan kategori dokumen

  • Tanya jawab lintas dokumen

  • Ringkasan otomatis

  • Pembuatan daftar pertanyaan

  • Ekstraksi tanggal dan nominal

  • Deteksi isi kontrak

  • Penanda dokumen penting

  • Citation sumber

  • Riwayat percakapan

  • Streaming jawaban

  • Export hasil ke PDF atau Word

  • Integrasi Google Drive

  • Integrasi penyimpanan S3

  • Sistem role dan permission

  • Dashboard penggunaan token

  • Batas penggunaan per paket

  • Multi-tenant SaaS

  • Integrasi chatbot WhatsApp

  • Pencarian hybrid

  • Audit log aktivitas pengguna

19. Contoh Penerapan dalam Bisnis

Knowledge Base Karyawan

Karyawan dapat mencari prosedur kerja tanpa membaca seluruh dokumen SOP.

Analisis Kontrak

Tim dapat menemukan masa berlaku, nilai kontrak, kewajiban, dan ketentuan penting.

Sistem Pembelajaran

Siswa dapat mengajukan pertanyaan berdasarkan modul yang diunggah.

Customer Service

Petugas dapat mencari jawaban berdasarkan panduan produk dan kebijakan perusahaan.

Laporan Perusahaan

Manajemen dapat meminta ringkasan, temuan utama, dan daftar risiko dari laporan.

Dokumentasi Teknis

Developer dapat mencari konfigurasi, prosedur deployment, atau penjelasan fitur dari dokumentasi sistem.

20. Checklist Sebelum Digunakan di Produksi

Pastikan sistem telah memiliki:

  • Autentikasi pengguna

  • Authorization dokumen

  • Validasi PDF

  • Batas ukuran upload

  • Private storage

  • OpenAI API key di backend

  • Error handling

  • Rate limiting

  • Queue worker

  • Status pemrosesan dokumen

  • Penghapusan file

  • Audit log

  • Pencatatan usage

  • Batas penggunaan pengguna

  • Pemisahan data tenant

  • Kebijakan privasi

  • Backup database

  • Monitoring kegagalan API

  • Pengujian kebocoran data

  • Evaluasi kualitas jawaban

Kesimpulan

Fitur tanya jawab dokumen PDF dapat membantu pengguna menemukan informasi dengan lebih cepat tanpa membaca seluruh halaman secara manual.

Laravel bertugas mengelola:

  • Upload dokumen

  • Penyimpanan file

  • Pengguna

  • Database

  • Authorization

  • Riwayat pertanyaan

  • Rate limiting

  • Tampilan aplikasi

Sementara itu, OpenAI API digunakan untuk:

  • Memproses dokumen PDF

  • Memahami pertanyaan pengguna

  • Menemukan informasi relevan

  • Menghasilkan jawaban

  • Melakukan pencarian melalui File Search

Untuk implementasi sederhana, developer dapat menggunakan Direct File Input dengan mengirim file_id dan pertanyaan ke Responses API.

Untuk aplikasi yang mengelola banyak dokumen, gunakan File Search dan vector store agar sistem dapat mencari bagian yang relevan secara lebih efisien.

Keamanan tetap harus menjadi prioritas. Dokumen harus disimpan secara privat, akses pengguna perlu dibatasi, API key harus berada di backend, dan data harus dapat dihapus ketika tidak lagi diperlukan.

Bagi perusahaan yang membutuhkan aplikasi tanya jawab PDF, knowledge base AI, chatbot dokumen, sistem RAG, integrasi OpenAI API, atau aplikasi custom berbasis Laravel, OVLA Media dapat membantu merancang dan mengembangkan solusi sesuai kebutuhan operasional bisnis.

#tanya jawab PDF Laravel #cara membuat chatbot PDF #Laravel OpenAI API #AI dokumen PDF #chatbot dokumen #aplikasi tanya jawab dokumen #OpenAI file input #Responses API Laravel #File Search OpenAI #vector store OpenAI #RAG Laravel #knowledge base AI #analisis PDF dengan AI #upload PDF Laravel #chatbot AI Laravel #aplikasi AI #document question answering #sistem manajemen dokumen #Laravel developer #pengembangan aplikasi Laravel #aplikasi custom #software house #OVLA Media
Beranda Produk Artikel
Konsultasi
OVLA

Navigasi Utama

Hubungi Kami

Mulai Konsultasi Sekarang