برمجة تطبيق ‘لخصلي’ لتلخيص المقالات باستخدام ChatGPT ولارافل – Laravel

70
0


يسعى مجال الذكاء الاصطناعي إلى محاكاة ذكاء البشر في قدرتهم على تحليل المعطيات الحسية كالصورة والأصوات واستنتاج المعلومات المفيدة منها، وبينما يُمثل البشر تلك المعطيات بيولوجيًا طور علماء الرياضيات والحاسوب طرقًا لتمثيل ومعالجة تلك المعطيات رقميًا لنتمكن من معالجتها بخوارزميات رياضية ضمن الحاسوب، حيث يمكن تدريب نماذج الذكاء الاصطناعي على البيانات لتتمكن محاكاة ذكاء البشر في مجال محدد، أحد أنواع تلك النماذج هي نماذج اللغة الكبيرة LLM’s (أو Large Language Models) وهي نماذج دُربت على كميات كبيرة من النصوص فامتلكت القدرة على توليد نصوص مشابهة لها وبل حتى فهم نصوص جديدة لم تشاهدها ضمن بيانات التدريب من قبل.

من نماذج اللغة الكبيرة النموذج ChatGPT من شركة OpenAI والذي دُرّب على كمية كبيرة من النصوص الموجودة على شبكة الانترنت وطوّر ليستجيب إلى الأوامر المرسلة له من قبل المستخدم، مما يجعل هذا النموذج قادرًا على محاكاة تلك النصوص وتحديد النقاط الرئيسية فيها والرد عليها بردود كما لو أنها مكتوبة من قبل إنسان، لذا يمكن الاستفادة من مرونة التعامل معه وفهمه للأوامر باللغة الطبيعية في العديد من التطبيقات العملية، كتوليد المحتوى أو التعديل على نص معين أو التلخيص واستخراج النقاط الرئيسية من نص ما والكثير من الأفكار الأخرى، أي يمكن اعتبار تلك النماذج كمساعد ذكي يمكن طلب أي مهمة منه كما لو كنا نطلبها من شخص حقيقي، وأهم ما في تلك النماذج هو سهولة برمجتها نسبيًا، فلم نعد بحاجة لتعلم لغة برمجة مخصصة بل يمكننا التعامل معها باللغة الطبيعية وبعدة لغات أيضًا منها اللغة العربية كما سنرى لاحقًا.

وفي هذا المقال سنستفيد من قدرة ذلك النموذج على فهم النصوص وإعادة صياغتها وتلخيصها في تلخيص مقالات من الإنترنت، حيث سنبني تطبيق ويب باستخدام لارافل Laravel لجلب محتوى المقال الذي يرغب المستخدم في تلخيصه، ثم الطلب من نموذج ChatGPT تلخيص ذلك المقال للمستخدم، ولمتابعة هذا المقال يجب أن تملك خبرة أساسية في لغة PHP عمومًا وإطار لارافل خصوصًا.

إنشاء مشروع لارافل جديد وبناء الواجهات الأمامية

يتألف مشروعنا بشكل عام من صفحتين رئيسيتين الأولى يدخل فيها المستخدم رابط المقال الذي يريد تلخيصه، والثانية يظهر له فيها الملخص لذلك المقال بعد أن يجلب التطبيق صفحة المقال من على الأنترنت ويستخرج محتوى المقال ويرسلها إلى نموذج ChatGPT لتلخيصها كما سنرى بالتفصيل لاحقًا، ولنبدأ أولًا بإنشاء مشروع وليكن بالاسم summarizer عبر تنفيذ الأمر التالي ضمن الطرفية والذي سيُنشئ مجلد جديد باسم المشروع يحوي هيكلية الملفات الأساسية لمشروع لارافل جديد:

composer create-project laravel/laravel summarizer

نحتاج لهذا المشروع صفحتين، الصفحة الأولى لإدخال رابط المقال المُراد تلخيصه وتحوي على نموذج form بسيط يرسل القيم بداخله بالطريقة POST إلى المسار summary/ والذي سنُعرفه لاحقًا، ويحوي النموذج على حقل لإدخال الرابط بالاسم url وزر الإرسال ولا ننسى تضمين قيمة الحقل csrf ضمن النموذج، وننشئ للصفحة ملف جديد بصيغة قالب blade جديدة ضمن المسار التالي resources\views\index.blade.php يحوي التالي:

<head>
    <title>لخصلي</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <h1>?</h1>
    <form action="/summary" method="POST">
        @csrf
        <input type="url" name="url" placeholder="أدخل رابط المقال">
        <button type="submit">لخصلي</button>
    </form>
</body>

والصفحة الثانية هي لعرض نتيجة التلخيص، تحوي على الملخص ورابط للمقال الأصل في حال أراد المستخدم قراءته ورابط للرجوع للصفحة الرئيسية لطلب تلخيص جديد، ونُنشئ ملف قالب تلك الصفحة ضمن المسار resources\views\summary.blade.php ويحوي التالي:

<head>
    <title>لخصلي</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <h2>ملخص المقال</h2>
    <p>{{ $summary }}</p>
    <footer>
        <a href="{{$url}}" target="_blank">قراءة المقال</a>
        <a href="/">تلخيص مقال جديد</a>
    </footer>
</body>

نلاحظ ربط ملف تنسيقات CSS بالاسم style.css في كلا الصفحتين لتنسيق محتواهما، ونُنشئ ذلك الملف ضمن المسار public\style.css ويحوي التنسيق التالي:

body {
    direction: rtl;
    display: flex;
    flex-direction: column;
    max-width: 60ch;
    justify-items: center;
    align-items: center;
    font-size: x-large;
    margin: 0 auto;
    padding: 20vh 10vw;
    background-color: #f9f9f9;
    color: #121212;
    font-family: system-ui;
}

p {
    line-height: 2;
    text-align: center;
    border: .25rem solid;
    padding: 3rem;
    white-space: pre-line;
}

a {
    padding: 1rem;
    font-size: inherit;
    margin: 0 .5rem;
    color: darkblue;
}

footer {
    margin-top: 2rem;
}

form {
    display: flex;
    flex-direction: column;
    gap: 2rem;
    text-align: center;
}

button {
    background-color: #121212;
    color: #f9f9f9;
    border: none;
    padding: 1rem 2rem;
    font-size: inherit;
    cursor: pointer;
    font-family: inherit;
}

input {
    padding: 1rem 2rem;
    font-size: inherit;
    font-family: inherit;
    text-align: center;
    border: .2rem solid;
}

نعرف ضمن ملف التوجيه routes\web.php مسارين الأول لعرض صفحة الإدخال نعيد فيها قالب الصفحة الأولى، والمسار الثاني summary/ لاستقبال طلبات HTTP من نوع POST نستخرج ضمنه قيمة الرابط url المُدخل من قبل المستخدم ويلخص ذلك المقال ثم يعرض النتيجة ضمن قالب الصفحة الثانية ليكون محتوى الملف كالتالي:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// صفحة الإدخال
Route::view('/', 'index');

// صفحة عرض التلخيص 
Route::post('/summary', function (Request $request) {
    $url = $request->input('url');

    return view('summary', [
        'url' => $url,
        'summary' => 'ملخص المقال هنا'
    ]);
});

والآن يمكننا استعراض الصفحتين بتشغيل خادم التطوير أولًا باستخدام الأمر التالي:

php artisan serve

واستعراض الصفحة الرئيسية:

وإدخال أي رابط ضمن تلك الصفحة وطلب تلخيصه لنستعرض صفحة النتيجة:

summary-template.png

بعد أن جهزنا المشروع الجديد والصفحات وعملية إرسال طلب تلخيص جديد سنبدأ في الفقرات التالية تضمين عملية جلب محتوى المقال المطلوب ثم تلخيصه باستخدام ChatGPT.

جلب محتوى المقال

نبدأ بإنشاء صنف جديد بالاسم Article ضمن المسار app\Article.php والذي سيكون مسؤولًا عن جلب محتوى المقال عن طريق الرابط له، ويحوي على التابع fromURL والذي يقبل رابط المقال كمعامل أول ويعيد نص المقال كسلسلة نصية، ويُنفذ ذلك باستخدام الصنف المساعد HTTP الذي يتيحه لارافل لإرسال طلب من نوع GET لجلب محتوى صفحة المقال كاملةً، ثم الاستعانة بالصنف DOMDocument لتحليل محتوى HTML من تلك الصفحة واستخراج المحتوى النصي لعناصر الفقرات ذات الوسم p والتي تحوي عادةً المحتوى النصي للمقال، ونجمع محتوى تلك الفقرات معًا في سلسلة نصية لتكون المحتوى النصي للمقال كاملًا ضمن الصفحة كالتالي:

<?php

namespace App;

use DOMDocument;
use Illuminate\Support\Facades\Http;

class Article
{
    public function fromURL($url): string
    {
        // جلب محتوى الصفحة
        $html = Http::withHeaders(['User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'])
            ->get($url)
            ->body();

        // استخراج الفقرات
        $document = new DOMDocument();
        @$document->loadHTML($html);
        $paragraphs = $document->getElementsByTagName('p');

        // جمع محتوى الفقرات معًا
        $article = '';
        foreach ($paragraphs as $paragraph) {
            $article = trim($article . "\n\n" . $paragraph->textContent);
        }
        return $article;
    }
}

أضفنا للطلب المُرسل الترويسة User-Agent والتي تتطلبها بعض المواقع لإرسال محتوى الصفحة، ونلاحظ أيضًا إضافة العلامة @ قبل تحميل محتوى الصفحة باستخدام التابع loadHTML لتجنب بعض الأخطاء التي قد تحدث عند محاولة تفسير محتوى الصفحة، ولاستخدام ذلك الصنف يمكننا تمريره كمعامل لتابع معالجة مسار صفحة النتيجة ليُنشئ لارافل تلقائيًا كائنًا جديدًا من ذلك الصنف ويمرره لتابع المسار لنتمكن من استخدامه لجلب محتوى المقال من الرابط المُرسل كالتالي:

use App\Article;

...

// صفحة عرض التلخيص 
Route::post('/summary', function (Request $request, Article $article) {
    $url = $request->input('url');

    // جلب محتوى المقال
    $content = $article->fromURL($url);

    return view('summary', [
        'url' => $url,
        'summary' => 'ملخص المقال هنا'
    ]);
});

وفي الفقرة التالية سنرسل ذلك المحتوى إلى ChatGPT لتلخيصه ثم عرضه للمستخدم.

تلخيص محتوى المقال باستخدام ChatGPT

بعد أن استخرجنا محتوى المقال كاملًا يمكننا إرساله إلى ChatGPT محاطًا بتعليمة نخبر بها النموذج بأننا نريد ملخص قصير عن ذلك النص، ولكن نماذج اللغة الكبيرة مثل ChatGPT تملك حد أقصى من طول المحتوى المُرسل إليها وهو 4097 رمز (أو token) بحسب التوثيق الرسمي للنموذج، والرمز هو قطعة من النص يحدد طولها بكلمة أو عدة أحرف أو عدة كلمات أحيانًا، أي في حال كان المقال طويل وعدد الرموز المكونة له أكبر من الحد الأقصى لن نتمكن من إرساله دفعة واحدة، ويمكن الاطلاع من خلال أداة توفرها الشركة على حساب تلك الرموز من أي نص.

ويمكن حل المشكلة الحد الأقصى للرموز بعدة طرق والطريقة التي سنستخدمها هي تقسيم نص المقال إلى عدة مقاطع أقصر، ثم تلخيص كل مقطع منها على حدى وجمعها معًا للحصول على ملخص المقال كاملًا، وفي حال كان ذلك الملخص أطول مما نرغب بعرضه للمستخدم نعيد تلخيص الملخص نفسه مجددًا للحصول على ملخص أقصر، ونكرر ذلك حتى الحصول على ملخص بطول مناسب للمقال كاملًا، وانتبه إلى أن هذه الطريقة اعتمدناها فقط لأغراض تعليمية وقد لا تعطي نتائج جيدة لذا ابحث عن طريقة أفضل لحل هذه المشكلة.

نكمل العمل بإنشاء صنف جديد بالاسم Summary ضمن المسار app\Summary.php سيحوي على عدة لتقسيم وتلخيص النصوص سنشرحها تباعًا:

<?php

namespace App;

class Summary
{
 ...
}

التابع الرئيسي لتلخيص المقال اسمه toWords وهو يستقبل النص المراد تلخيصه وعدد يعبر عن عدد الكلمات الأقصى للملخص النهائي لذلك النص، بحيث يكون ذلك التابع مسؤولًا عن تلخيص النص باستخدام التابع summarize الذي سنعرفه لاحقًا، ثم التحقق من عدد كلمات الملخص وتكرار عملية التلخيص إن لزم الأمر إلى أن نحصل على ملخص نهائي بالطول المناسب:

class Summary
{
    public function toWords(int $at_most, string $text)
    {
        // تلخيص أولي للنص
        $summary = $this->summarize($text);

        // تكرار عملية التلخيص حسب الطول المطلوب
        while (count(explode(" ", $summary)) > $at_most)
            $summary = $this->summarize($summary);

        return $summary;
    }

    public function summarize(string $text): string
    {
        ...
    }
}

ونستدعي ذلك التابع ضمن تابع معالجة بعد تعريفه ضمن المعاملات في ملف التوجيه routes/web.php لتلخيص محتوى المقال بعد استخراجه إلى عدد كلمات مناسب وليكن 150 كلمة وتمرير الملخص إلى قالب الصفحة لعرضه للمستخدم كالتالي:

// صفحة عرض التلخيص
Route::post('/summary', function (Request $request, Article $article, Summary $summarizer) {
    $url = $request->input('url');

    // جلب محتوى المقال
    $content = $article->fromURL($url);

    // تلخيص المقال
    $summary = $summarizer->toWords(100, $content);

    return view('summary', [
        'url' => $url,
        'summary' => $summary
    ]);
});

التابع summarize ضمن الصنف Summary مهمته تجزئة النص إلى فقرات بطول مناسب لإرسالها إلى ChatGPT للتلخيص، حيث نبدأ بتجزئة النص إلى فقراته التي يفصل بينها سطر فارغ، ثم تجميعها إلى مجموعات ولتكن مؤلفة من 8 فقرات معًا، بعدها نطلب تلخيص كل مجموعة على حدى، وبما طلب تلخيص كل مجموعة لا يتعلق بالأخرى فيمكننا إرسال طلبات التلخيص لتلك المجموعات معًا على التوازي لاختصار وقت التلخيص الكلي، فلا داعي في طريقتنا لانتظار تلخيص أول فقرة حتى نلخص التي تليها، وننفذ ذلك باستخدام التابع pool من الصنف المساعد في لارافل HTTP والذي نمرر له تابعًا يقبل معامل من النوع Pool والذي يعبر عن مجموعة من طلبات HTTP ستُرسل معًا على التوازي، توليد الطلب الواحد سنوكله لتابع منفصل بالاسم summaryRequest، وبعد انتهاء كل الطلبات نستخرج منها الملخصات من مفتاح JSON في المسار choices.0.message.content باستخدام التابع json من كل طلب منها، ثم نضمها معًا مجددًا لتشكل الملخص لكل الفقرات المرسلة:

class Summary
{

   ...

    public function summarize(string $text)
    {
        // الفاصل بين الفقرات
        $seperator = "\n\n";

        // فصل النص إلى فقرات بطول مناسب
        $paragraphs = collect(explode($seperator, $text))
            ->chunk(8)
            ->map(fn ($chunks) => $chunks->join($seperator));

        // إرسال طلبات التلخيص على التوازي
        $responses = Http::pool(fn (Pool $pool) => $paragraphs->map(
            fn (string $paragraph) => $this->summaryRequest($pool, $paragraph)
        ));

        // استخراج تجميع الملخصات معًا
        return collect($responses)
            ->map(fn ($response) => $response->json('choices.0.message.content'))
            ->join($seperator);
    }

    protected function summaryRequest(Pool $pool, string $text)
    {
        ...
    }
}

ضمن التابع summaryRequest سنرسل الطلب باستخدام pool$ إلى ChatGPT لتلخيص محتوى الفقرة text$ المٌمررة له، وإرسال أي طلب للواجهة البرمجية الخاصة به تحتاج منا أولًا الحصول على مفتاح استيثاق يمكن توليده بعد إنشاء حساب جديد ضمن موقع الشركة المالكة للنموذج OpenAI وإكمال إعدادات الحساب وتعيين وسيلة الدفع ضمنه، وبعد توليد المفتاح نضعه ضمن ملف متغيرات البيئة env. ضمن مفتاح بالاسم OPENAI_API_KEY ضمن ملفات المشروع لنتمكن من اختبار إرسال الطلبات محليًا كالتالي:

...
OPENAI_API_KEY=<مفتاح الاستيثاق>

وبحسب صفحة التوثيق الخاصة بتلك الواجهة البرمجية فيجب أن نضع ذلك المفتاح ضمن قيمة الترويسة Authorization يسبقها الكلمة Bearer ويفصل بينهما مسافة، ويمكننا استخراج قيمة المفتاح السابق باستخدام التابع env من لارافل، ويُرسل طلب HTTP بالنوع POST إلى نموذج المحادثة ChatGPT إلى مسار الواجهة البرمجية https://api.openai.com/v1/chat/completions، وضمن جسم الطلب نحدد قيمتين، الأولى هي اسم النموذج ضمن المفتاح model في حالتنا هو gpt-3.5-turbo، والقيمة الثانية هي messages تحوي تاريخ الرسائل الواقعة بين المستخدم والنموذج سابقًا، وفي حالتنا نحتاج رسالة مستخدم واحدة تحوي تعليمات للنموذج بتلخيص الفقرة ضمن تلك الرسالة وصيغتها كالتالي:

اقتباس

لخص الفقرة التالية من المقال تلخيص دقيق: <الفقرة> التلخيص:

سيفهم النموذج التعليمة السابقة ويرد برسالة تحوي تلخيص للفقرة المذكورة، ليكون بذلك تابع تلخيص الفقرة summaryRequest كالتالي:

    protected function summaryRequest(Pool $pool, string $text)
    {
        return $pool
            ->withHeaders(['Authorization' => "Bearer " . env('OPENAI_API_KEY')])
            ->post('https://api.openai.com/v1/chat/completions', [
                'model' => 'gpt-3.5-turbo',
                'messages' => [
                    ['role' => 'user', 'content' => implode("\n", [
                        "لخص الفقرة التالية من المقال تلخيص دقيق:",
                        $text,
                        "التلخيص:"
                    ])]
                ],
            ]);
    }

بذلك يكون تضمين صنف التلخيص Summary جاهزًا سنختبره في الفقرة التالية ونرى نتيجة تلخيصه.

اختبار التطبيق

لنختبر التطبيق بتلخيص مقال من موقع أكاديمية حسوب عن الذكاء الاصطناعي بإدخال رابطه ضمن صفحة الإدخال وطلب التلخيص:

result-1.png

نلاحظ أن الملخص يحوي ذكر لمعظم الأفكار التي يغطيها المقال ما يعطي صورة عامة عنه، ولنختبره مجددًا بتلخيص جديد من أكاديمية حسوب عن تعلم البرمجة:

result-2.png

يمكن التطوير على التطبيق من عدة نواحي، فمثلًا يمكن أن نوفر للمستخدم خيارات لإدخال محتوى المقال عن طريق رابط مثلًا أو نسخ المحتوى مباشرةً، أو أن نسمح للمستخدم بتحديد طول الملخص الذي يرغب به، أو الطلب من النموذج كتابة الملخص على شكل نقاط رئيسية، أو تحديد لغة النتيجة قبل عرضها للمستخدم، وهناك العديد من الأفكار المفيدة التي يمكن تطبيقها بالاستفادة من نماذج اللغة كنموذج ChatGPT وميزاته بفهم اللغة الطبيعية بلغات متعددة.

اقرأ أيضًا



المصدر

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *