Як створити спеціалізований чат-бот. Детальний гайд на основі кейса клієнта

💡 Усі статті, обговорення, новини про AI — в одному місці. Приєднуйтесь до AI спільноти!

Мене звати Ігор Козлов, я Big Data & ML Engineer в Levi9. Останнім часом значно зросло використання різних Великих Мовних Моделей (LLM, Large Language Model) для повсякденного розв’язання задач у різних сферах. В ІТ-сфері їх часто застосовують як асистентів-помічників під час написання коду (наприклад, GPT модель від OpenAI).

Більшість таких моделей натреновані на великому обсязі даних з різних галузей, але що робити, коли ми хочемо додати специфічні, вузькоспрямовані знання? Коли потрібно розробити чатбот, який буде відповідати на питання про продукцію компанії чи надавати документацію проєкту? У цьому гайді я відповім на ці питання, розповім як створити спеціалізований чатбот та поділюсь тим, на що варто звернути увагу для покращення результату.

Почати варто з самого визначення Large Language Model (LLM), мовної моделі, що спеціалізується на розумінні та генерації тексту й використовує архітектуру трансформерів (transformers). Наразі існує багато різних моделей LLM, найпопулярнішими серед яких є GPT-3.5, GPT-4, LLama, Cohere та BERT. Для нашого проєкту ми будемо користуватись GPT-моделями від OpenAI.

Обираємо хмарного провайдера

Щоб користуватись GPT-моделями, ми маємо обрати один з двох варіантів:

  1. Використовувати безпосередньо від OpenAI.
  2. Використовувати Azure Open AI.

Вони відрізняються переважно безпекою та доступністю. У таблиці нижче для зручності наводжу основні (не всі) відмінності, на які потрібно звернути увагу:

Характеристика

Azure OpenAI

OpenAI

Безпека

Має стандартну безпеку екосистеми Azure, є можливість використовувати віртуальні мережі (VNETS) та приватні ендпойнти (private endpoints)

Не зазначено

Дані для тонкого налаштування (fine tuning)

Використовується тільки для поточного користувача сервісу

Дані можуть бути використані безпосередньо для покращення GPT моделей. Є опція відмовитись від цього надіславши запит в службу підтримки

Prompts та completions

Зберігаються 30 днів в зашифрованому вигляді, доступні тільки для авторизованих користувачів

Зберігаються для тренування, дотренування та покращення моделей

Регіональна доступність

Доступно в незначній кількості регіонів (повний перелік тут)

Доступно в багатьох країнах та регіонах (повний перелік тут)

Отже, якщо для вашого проєкту важлива безпека даних, і ви не хочете, щоб вони використовувалися кимось, крім вас, а також у вашому регіоні доступний Azure OpenAI — краще використовувати його. Це також стосується і випадку, якщо ви вже використовуєте Azure як хмарного провайдера. Також Azure OpenAI інтегрований з Azure Cognitive Services. Детальніше про безпеку, яку надає сервіс, можна прочитати тут.

Наш приклад не має чутливих даних та специфічних безпекових вимог, тому ми будемо використовувати OpenAI. Усе, що потрібно — залогінитися та отримати ключ API.

Як передати LLM специфічні знання

LLM мають суттєве обмеження — на вхід вони можуть отримувати лімітовану кількість інформації. Цей обʼєм даних вимірюється у токенах, які формують контекстне вікно. Розмір контекстного вікна, своєю чергою, не дозволяє надати моделі великий текст, на основі якого вона має генерувати відповіді. Тому аби впровадити нову інформацію для моделі, існують такі способи, як тонке налаштування (fine tuning) та Retrieval Augmented Generation (RAG, пошук та генерація).

Тонке налаштування більше підходить для навчання моделі специфічних завдань (наприклад, асистент з побудови раціону здорового харчування) або стилів (написання технічних статей тощо). Коли необхідно відтворити факти або дати відповідь на запитання, модель може використати знання, які отримала під час початкового навчання. Це пов’язано з тим, що під час тонкого налаштуванні використовуються ваги моделі, які більше нагадують довгострокову пам’ять (long-term memory).

З іншого боку, те, що ми передаємо на вхід моделі через контекстне вікно, більше схоже на короткострокову пам’ять. Ці дані можна порівняти з нотатками, які модель може використати, щоб надати відповідь.

Саме на контекстному вікні й побудований RAG. Ідея полягає в тому, що ви створили для себе базу знань і шукаєте в ній інформацію, найближчу до запитання, яке було задано. Найчастіше для цього використовуються векторні бази даних. Ваші дані (текст) зберігається у вигляді вектора в БД.

Запитання, яке було поставлене, також трансформується у вектор, після чого в БД відбувається пошук вектора, який буде найближчим або подібним до вектора-запитання. Наступним кроком знайдена інформація та саме питання передається мовній моделі, разом з інструкцією відповіді на нього, що базується на наданій інформації. Візуально це можна відобразити так:

Джерело

Саме RAG ми використовуємо для реалізації нашого чатбота, побудованого на вузькоспеціалізованих знаннях.

Як приєднати різні джерела даних до LLM

Першим кроком є безпосередньо побудова бази даних (індексу) з інформації, на основі якої ми хочемо отримувати відповіді. Найголовніше в цьому — наявність надійних, високоякісних даних. Ви не зможете побудувати надійного чатбота, якщо дані, які ви надали на цьому етапі, суперечливі або не містять інформації, необхідної для відповіді на поставлене питання. Тому варто ще раз наголосити, що якісні дані — основа успіху будь-якого проєкту, пов’язаного з машинним навчанням.

Звісно, ми хочемо бути гнучкими в джерелах отримання наших даних. Для чатбота також важливо міксувати інформацію з різних джерел в одній базі даних, щоб розширити список питань, на які він може дати відповідь. Саме тому для побудови нашої RAG-системи я обрав LLamaIndex — фреймворк, який дозволяє приєднати різні джерела даних до великих мовних моделей.

Основні (але не всі) джерела даних, які можуть бути використані разом з LLamaIndex, наведені на зображенні:

Розробники створили спеціальний портал, де зібрані основні джерела даних, конектори й навіть готові застосунки з використанням RAG. Використати їх або додати свій можна за посиланням.

Джерелом даних був вебсайт клієнта. Для прикладу та збереження конфіденційності в статті я буду використовувати вебсайт з документацією LlamaIndex, який має чимало цікавої інформації.

Загалом LLamaIndex виконує всі основні функції, які нам потрібні:

  • завантаження даних;
  • створення бази даних;
  • пошук по базі даних;
  • передача питання, контексту та інструкції до мовної моделі;
  • повернення відповіді.

Чому сподобався саме LLamaIndex? По-перше, це швидкість розвитку проєкту та спільнота. Багато хто покращує його самостійно, і це входить в наступні релізи. По-друге, підтримка в Discord-спільноті. Ваше питання точно не залишиться без відповіді. І, по-третє, екосистема навколо.

Я вже згадував про LLamaHub. Є ще LLamaІndex RAGs, який дозволяє побудувати чатбот з вашими даними, використовуючи вже наявний чатбот. Це саме той асистент на базі великої мовної моделі, про який я згадував на початку статті.

Для візуалізації чатбота ми використаємо streamlit.io. Цей сервіс дозволяє швидко і з мінімальними зусиллями візуалізувати роботу моделі. У цьому випадку він надає простий UI чатботу. На мою думку, для успіху реалізації проєкту в машинному навчанні важливо якнайшвидше отримати зворотний зв’язок від замовника.

Streamlit дозволяє нам швидко побудувати інтерфейс, провзаємодіявши з яким, замовник може надати оцінку реалізації проєкту до безпосередньої інтеграції моделі в систему. Ну що ж, почнімо!

Створюємо перший прототип

Отже, клонуємо проєкт.

Переходимо в корінь проєкту, створюємо файл .streamlit/secrets.toml, додаємо до нього ваш API ключ та встановлюємо всі необхідні бібліотеки (рекомендую зробити це в окремому віртуальному середовищі): pip install -r requirements.txt. Запускаємо проєкт streamlit run 1_🏠_Home.py та переходимо за вказаною адресою.

Далі бот запитує необхідну інформацію (звідки брати дані, яка функція, чи хочемо ми змінити якісь параметри):

Вказуємо необхідний вебсайт (ви також можете обрати PDF-документ, csv-файл та інше) та надаємо інструкцію. У першому прототипі залишаємо параметри за замовчуванням. Зліва на панелі в нас з’являється лінк, за допомогою якого ми можемо перейти до чатбота. Обираємо Generated RAG agent та потрібного агента.

Ставимо декілька питань:

Як бачимо, ми використовуємо vector_tool. Це функція, яка власне робить пошук нашою базою даних.

Також ви могли звернути увагу на вкладку Sources:

Вона показує, який саме текст з бази даних був використаний для відповіді на питання. Це дуже важливо для оцінки якості відповіді та валідації коректності даних. На цьому етапі можна сміливо тиснути Deploy в правому верхньому кутку та надсилати лінк замовнику, щоб він оцінив відповіді й сам поспілкувався з ботом. Ранній зворотний зв’язок дуже важливий і допомагає покращити продукт.

Тепер оцінимо якість наших відповідей.

Програмні методи оцінки

Найголовніший відгук, звісно, від замовника. Але нам потрібно й самостійно оцінювати застосунок, навіть якщо ми не розбираємось в доменній області.

Екосистема LLamaIndex має для цього Rag Evaluator.

Перше, що треба зробити, це підготувати дані для тесту. Реалізувати це можна власноруч, надавши такі складові, як «Контекст», «Питання» та «Відповідь». Одним з прикладів може бути «Це питання просто для тесту», «Питання тестове?», «Так». Але, погодьтесь, такий датасет дещо складно створювати. Тому ще раз звернемося до штучного інтелекту, щоб він підготував нам все необхідне.

Наступні кроки:

  1. Обираємо n сторінок з сайту. Для реального проєкту краще взяти найбільш релевантні та складні сторінки або документи.
  2. Завантажуємо індекс, який ми побудували раніше (він доступний в корні проєкту rags/cache/agents/{Agent_ID}/storage).
  3. За допомогою GPT генеруємо питання та контрольні відповіді.
  4. Порівнюємо ці відповіді з тими, які надає наш індекс.

Основні критерії оцінки:

  • Достовірність (Correctness) — наскільки згенерована відповідь відповідає контрольній відповіді стосовно питання.
  • Семантична схожість (Semantic Similarity) — чи семантично схожа згенерована відповідь з контрольною відповіддю.
  • Точність (Faithfulness) — чи відповідає відповідь наданому контексту (чи є галюцинації).
  • Релевантність контексту (Context Relevance) — чи релевантні отриманий контекст і відповідь на питання.
  • Дотримання вказівок (Guideline Adherence) — чи відповідає передбачена відповідь конкретним вказівкам.

Код для оцінки якості:

from llama_index.llama_dataset.generator import RagDatasetGenerator
from llama_index.llms import OpenAI
from llama_index import ServiceContext, StorageContext, load_index_from_storage
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from llama_index.readers import TrafilaturaWebReader
from llama_index.llama_pack import download_llama_pack
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("-k", "--apikey", help="OpenAI API key")
args = vars(parser.parse_args())
os.environ["OPENAI_API_KEY"] = args["apikey"]
# set context for llm provider
gpt_4_context = ServiceContext.from_defaults(
   llm=OpenAI(model="gpt-3.5-turbo", temperature=0.3, chunk_size=512)
)
urls = [
   'https://docs.llamaindex.ai/en/stable/use_cases/chatbots.html',
   'https://docs.llamaindex.ai/en/stable/use_cases/agents.html',
]
reader = TrafilaturaWebReader()
documents = reader.load_data(urls)
# instantiate a DatasetGenerator
dataset_generator = RagDatasetGenerator.from_documents(
   documents,
   service_context=gpt_4_context,
   num_questions_per_chunk=2,  # set the number of questions per nodes
   show_progress=True,
)
# generate dataset
rag_dataset = dataset_generator.generate_dataset_from_nodes()
RagEvaluatorPack = download_llama_pack(
   "RagEvaluatorPack", "./rag_evaluator_pack"
)
rag_evaluator_pack = RagEvaluatorPack(
   query_engine=index.as_query_engine(),
   rag_dataset=rag_dataset
)
# PERFORM EVALUATION
benchmark_df = rag_evaluator_pack.run()
print(benchmark_df)

Приклад результату роботи скрипта:

Загалом надані відповіді були непогані, але можливо ми зможемо їх покращити. Саме тут грають роль ті параметри, які ми залишили за замовчуванням на початку конфігурації. Розглянемо їх більш детально.

Розширене налаштування

Embedding model

Одним з кроків, які ми робимо для побудови чатбота, є створення бази даних. Для швидкого пошуку використовуються векторні представлення (vector embeddings), які зберігаються в БД. Аби вони були якісні, нам потрібно обрати кращу модель для їх побудови та визначити вимоги до неї (однією з вимог може бути підтримка багатомовності, якщо ми хочемо спілкуватися з нашим ботом кількома мовами).

У такому разі слова різними мовами розташовуються в одному векторному просторі, що дозволяє нам швидко знайти схожість, навіть якщо питання поставити не тією мовою, у якій зберігаються дані.

Під час вибору моделі нам допоможе Massive Text Embedding Benchmark (MTEB). Ідея полягає в тому, що різні моделі порівнюються на одній і тій самі задачі та даних. Ми можемо перейти за посиланням та подивитися ці результати.

Нас цікавить саме задача отримання (Retrieval). За замовчуванням використовується text-similarity-ada-001 та text-embedding-ada-002. Як бачимо, вони розташовуються достатньо низько у списку. Тому варто спробувати інші моделі також. Я рекомендую звернути увагу на Cohere/Cohere-embed-multilingual-v3.0 та intfloat/multilingual-e5-large, які ми тестували для замовника і вони показали кращі результати за умов використання декількох мов для спілкування з ботом.

Chunk size

Вибрати оптимальний розмір чанку важливо з декількох причин:

  1. Релевантність та точність. Малий розмір чанку надає нам більшу точність, але ми можемо втратити частину важливої інформації, яка перебуває в сусідніх чанках (пізніше я розповім, як можна обійти це). Водночас великий розмір чанку може містити багато зайвої інформації, яка також буде використана для надання відповіді.
  2. Швидкість відповіді. Що більше розмір чанку, то більше інформації ми передаємо LLM, що може підвищити час очікування відповіді.

Щоб обрати оптимальний розмір, варто використати метрики релевантності контексту (Context Relevancy) та точності (Faithfulness), про які ми згадували раніше. А також порівняти результати для різних розмірів саме на ваших даних, адже не існує універсального. Загалом для отримання метрик ми можемо використати код, який наведено вище, підставляючи необхідний розмір чанку. Приклад результату:

Top K

Це характеристика, яка визначає, скільки документів, близьких до нашого питання, ми будемо брати до уваги й передавати в LLM для генерування відповіді. Вибір цього значення також пов’язаний з розміром чанку, адже у разі невеликого розміру потрібна інформація може бути в пʼяти найближчих чанках, тоді як ми за замовчуванням обираємо, до прикладу, два.

Додаткова обробка результатів

Додаткова обробка потрібна для того, щоб уникнути недоліків, які можуть виникнути під час пошуку потрібного документу. Загалом ми беремо результат пошуку нашою базою даних (саме документи, які можуть бути використані для відповіді на поставлене питання) та додатково їх обробляємо.

Зараз це неможливо зробити через LLamaIndex RAGs, який ми використовували для генерації індексу та побудови інтерфейсу. Та ми можемо завантажити вже згенерований нами індекс та додати до нього необхідні кроки.

Переранжування (re-ranking)

Деякі нерелевантні документи можуть отримати високі оцінки, тоді як деякі релевантні, навпаки, можуть отримати нижчі бали. Так, не всі документи top-k є релевантними, і не всі відповідні документи є в top-k. Переранжування уточнює ці результати та відображає найбільш відповідні запиту.

Зазвичай для переранжування використовуються мовні моделі. Їхній рейтинг ми можемо знайти в MTEB на вкладці Reranking.

Особисто моя рекомендація — використати Cohere rarank:

from llama_index.postprocessor.cohere_rerank import CohereRerank
cohere_rerank = CohereRerank(api_key=api_key, top_n=2)
query_engine = index.as_query_engine(
   similarity_top_k=10,
   node_postprocessors=[cohere_rerank],
)
response = query_engine.query(
   "What is node postprocessor?",
)

Отримання додаткового контексту

Як згадував вище, за маленького розміру чанку потрібна інформація може міститися «навколо» знайденого документа. За допомогою PrevNextNodePostprocessor ми також можемо отримати документи, які перебувають «до і після» знайденого документа.

Приклад використання:

from llama_index.postprocessor import PrevNextNodePostprocessor
postprocessor = PrevNextNodePostprocessor(
   docstore=index.docstore,
   num_nodes=1,  # number of nodes to fetch when looking forawrds or backwards
   mode="next",  # can be either 'next', 'previous', or 'both'
)
postprocessor.postprocess_nodes(nodes)

Сортування за датою

Хочу зазначити, що якщо вам необхідно отримувати найновіші документи, то це можливо зробити за допомогою FixedRecencyPostprocessor. Для цього потрібно додати до метаданих документа поле «дата» під час побудови індексу й потім використати додаткову обробку. Приклад:

from llama_index.extractors import (
   BaseExtractor,
)
from datetime import date
from llama_index.postprocessor import FixedRecencyPostprocessor
from llama_index.ingestion import IngestionPipeline
from llama_index.readers import TrafilaturaWebReader
class CustomExtractor(BaseExtractor):
   def extract(self, nodes):
       metadata_list = [
           {
               "date":  date.today()
           }
           for node in nodes
       ]
       return metadata_list
extractors = [CustomExtractor()]
urls = [
  'https://docs.llamaindex.ai/en/stable/use_cases/chatbots.html',
  'https://docs.llamaindex.ai/en/stable/use_cases/agents.html',
]
reader = TrafilaturaWebReader()
documents = reader.load_data(urls)
pipeline = IngestionPipeline(transformations=extractors)
nodes = pipeline.run(documents=documents)
postprocessor = FixedRecencyPostprocessor(
   tok_k=10, date_key="date"  # the key in the metadata to find the date
)
postprocessor.postprocess_nodes(nodes)

Оскільки сторінки документації не мають дати, ми просто ставимо дату додавання сторінки в індекс. Ви можете реалізувати свою логіку, підставляючи необхідну дату з потрібного джерела.

OpenAI GPTs

Нещодавно OpenAI опублікувало GPTs та Assistants API. Загалом вони дозволяють побудувати аналог ChatGPT на ваших даних.

Хлопці з LlamaIndex зробили порівняння їхні рішення та Assistants API від OpenAI. Основні висновки:

  • для індексу з багатьох документів краще працює LLamaIndex;
  • Assistants API набагато швидше за LLamaIndex, коли вся інформація об’єднана в один документ;
  • обробка пʼяти документів в LLamaIndex зайняла пʼять хвилин, коли в Assistants API така сама обробка зайняла одну годину.

Знаючи ці основні моменти, ви також можете орієнтуватись, який метод побудови чатбота обрати.

Підсумки

Цей гайд має на меті допомогти вам побудувати базовий чатбот навколо ваших даних з мінімальною кількістю коду або взагалі без його використання (за допомогою іншого чатботу, як не дивно). Цього цілком достатньо для побудови прототипу та верифікації вашої ідеї клієнтом.

Після отримання відповіді від замовника з основними проблемами, ви можете покращити роботу боту за допомогою таких методів, як-от вибір моделі, розміру чанку або додавання ще одної обробки після отримання результатів. Користуйтесь!

👍ПодобаєтьсяСподобалось22
До обраногоВ обраному13
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Доброго дня, Ігоре!
Дякую за статтю — вона дуже корисна і багато де проливає світло.
Хотів ось що запитати: припустимо є датасет в CSV формату (160, 10000) і він стосується нерухомості — адреси, координати, спальні, соціально-економічні параметри районів, ціна тощо.
Чи можна в цьому випадку використовувати RAG+LLM для векторизації датасету і далі отримання відповідей користувачем на свої запитання, наприклад «який найбезпечніший район», «які об’єкти знаходяться в максимальній близькості до шкіл, парків» тощо? У мене є сумніви, оскільки датасет — в основному це табличні числові дані, а якщо є текстові — то це більше власні імена — міста, райони, області, адреси. Чи буде ефект у цьому випадку на Вашу думку?

Доброго дня
Треба дивитися безпосередньо на дані. На мій погляд це можливо, якщо долати боту додатковий контекст:
1) як соціально-економічні параметри району впливають на безпечність району? Типу задати йому правила що, умовно кажучи, 1-погано, 10-добре
2) чи є дані про школи та парки та їх координати в датасеті? Якщо так, і знову таки задати правило як визначити, то бот може це робити
Якщо даних в датасеті немає, то можна побудувати агента, який буде шукати ці дані в інтернеті

Загалом, боти нормально працюють на числових даних, якщо є вказівки як з ними працювати

Привіт, намагався прйти по всім степам та встановити і запустити rags
Було декілька проблем :

— де брати `ваш API ключ` ? Це мій особистий ключ OpenAI `OPENAI_API_KEY` ?
Якщо це мій власний ключ вид OpenAI то програма буде працювати з моделю у хмарі чи локально ?

— треба вказати що додатково необхідно встановлювити `poetry`, `streamlit`

— після команди `streamlit run 1_🏠_Home.py` відкридась сторінка з помилкою `ModuleNotFoundError: No module named ’streamlit_pills’`
довелось встановити `streamlit_pills`

— знову після команди `streamlit run 1_🏠_Home.py` відкридась сторінка з помилкою `ModuleNotFoundError: No module named ’llama_index’`
довелось встановити `llama_index`

— знову після команди `streamlit run 1_🏠_Home.py` відкридась сторінка з помилкою `ImportError: cannot import name ’LLM’ from ’llama_index.llms.base’ (/usr/local/lib/python3.11/site-packages/llama_index/llms/base.py)`

photos.app.goo.gl/DSdpyVWVCarEwKiMA
і тут вже я не знаю що робити :/

Буду вдячний за відповіді

Привіт
1) так, це особистий ключ, взятий на сайті OpenAI. Буде працювати з моделлю у хмарі. Можна також юзати локальні моделі або моделі не від OpenAI. Лінк на доку:
docs.llamaindex.ai/...​e_guides/models/llms.html
2) в requirements.txt ці ліби є, тому в статті і написав першим кроком зробити

pip install -r requirements.txt

)))
3) по

ImportError: cannot import name ’LLM’ from ’llama_index.llms.base’

теж треба було робити сетап через requirements.txt, там вказана конкретна версія llama-index. Коли ви самі встановили без вказівки версії, встановилась остання, де цей модуль оновлений і перенесений.
Рекомендую все таки запустити pip install -r requirements.txt в корні проекту rags

Якщо ж команду встановлення requirements.txt запускали, перевірте чи ви інші команди виконуєте в тому ж віртуальному середовищі де її запускали.

так все спрацювало, мій косяк був.
Якщо цей проект запускає llama (вид Meta) то чому використовується ключ OpenAI?

Це не проект від llama, meta etc.
Це просто назва, розробник Jerry Liu
А самі моделі можна використовувати від різних платних провайдерів, OpenAI як одна з опцій

Цікаво, а як додати роботу с графічними матеріалами ?, наприклад як користувачу заповнити картку продуктової позиції в умовній ERP?

LlamaIndex може працювати і з зображеннями, і можна побудувати агента який буде відповідати на питання про них. Для цього будується multi-modal RAG
Приклади:
1) blog.llamaindex.ai/...​ex-and-neo4j-a2c542eb0206
2) github.com/...​red_image_retrieval.ipynb
Якщо іде розмова про заповнення картки продукта, то вже будуємо фреймворк який може, наприклад, отримати інпут від юзера, прогнати його та покращити через LLM, та оновити опис в БД за допомогою custom function call (platform.openai.com/...​s/guides/function-calling)
Схожий приклад такого фреймворку:
1) lionagi.readthedocs.io/en/latest/index.html
2) llamahub.ai/...​/tools-shopify?from=tools

Тут також використовується custom function call. Тобто Вам треба реалізувати необхідну логіку та комбінувати з застосуванням чат-боту

В Levi9 робили мобільний застосунок-асистент з харчування. За допомогою чатботу генерувався план харчування на тиждень (зберігався в БД), і за допомогою чатботу можна було внести зміни в цей план

Треба швидше вчити пайтон, щоби робити такі крутезні штуки )

В екосистемі LlamaIndex багато прикладів no-code чи low code) наприклад базовий приклад зі статті. Тому прям якогось особливого знання python не потрібно)

Підписатись на коментарі