├── .gitignore ├── LICENSE ├── LangChainQA.ipynb ├── PrepareDataset.ipynb ├── README.md ├── YaGPT.py ├── config.json ├── images └── scrshot.png ├── requirements.txt └── telegram.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yandex DataSphere Education 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LangChainQA.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ee149ac5", 6 | "metadata": { 7 | "cellId": "mes8twdaihp199vumyadbj", 8 | "execution_id": "dd6efd9c-99e4-4ce2-b68d-5a6249bf0a13" 9 | }, 10 | "source": [ 11 | "## Строим вопрос-ответного бота по технологии Retrieval-Augmented Generation на LangChain\n", 12 | "\n", 13 | "[LangChain](https://python.langchain.com) - это набирающая популярность библиотека для работы с большими языковыми моделями и для построения конвейеров обработки текстовых данных. В одной библиотеке присутствуют все элементы, которые помогут нам создать вопрос-ответного бота на наборе текстовых данных: вычисление эмбеддингов, запуск больших языковых моделей для генерации текста, поиск по ключу в векторных базах данных и т.д.\n", 14 | "\n", 15 | "Для начала, установим `langchain` и сопутствующие библиотеки." 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 16, 21 | "id": "e26382e7", 22 | "metadata": { 23 | "cellId": "yzdzyw1o8ujhmnrk4xly6m" 24 | }, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "\u001b[33m WARNING: The script filetype is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 31 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 32 | "\u001b[0m\u001b[33m WARNING: The script pysemver is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 33 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 34 | "\u001b[0m\u001b[33m WARNING: The script normalizer is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 35 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 36 | "\u001b[0m\u001b[33m WARNING: The script langsmith is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 37 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 38 | "\u001b[0m\u001b[33m WARNING: The script huggingface-cli is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 39 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 40 | "\u001b[0m\u001b[33m WARNING: The script unstructured-ingest is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 41 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 42 | "\u001b[0m\u001b[33m WARNING: The script transformers-cli is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 43 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 44 | "\u001b[0m\u001b[33m WARNING: The script langchain-server is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 45 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 46 | "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", 47 | "pandas-gbq 0.17.9 requires pyarrow<10.0dev,>=3.0.0, but you have pyarrow 13.0.0 which is incompatible.\n", 48 | "tensorflow 2.12.0 requires wrapt<1.15,>=1.11.0, but you have wrapt 1.15.0 which is incompatible.\u001b[0m\u001b[31m\n", 49 | "\u001b[0m" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "%pip install -q langchain sentence_transformers lancedb unstructured" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "id": "c29e1a7f", 60 | "metadata": { 61 | "cellId": "01gt55s25xlq8thmgt142tx", 62 | "execution_id": "202bd627-2f9f-4492-928d-9d30336acd19" 63 | }, 64 | "source": [ 65 | "## Разбиваем документ на части\n", 66 | "\n", 67 | "Для работы retrival augmented generation нам необходимо по запросу найти наиболее релевантные фрагменты исходного текста, на основе которых будет формироваться ответ. Для этого нам надо разбить текст на такие фрагменты, по которым мы сможем вычислять эмбеддинг, и которые будут с запасом помещаться во входное окно используемой большой языковой модели.\n", 68 | "\n", 69 | "Для этого можно использовать механизмы фреймворка LangChain - например, `RecursiveCharacterTextSplitter`. Он разбивает текст на перекрывающиеся фрагменты по набору типовых разделителей - абзацы, переводы строк, разделители слов.\n", 70 | "\n", 71 | "> **ВАЖНО**: Перед выполнением ячейки не забудьте установить имя пользователя, которое вы использовали на предыдущем шаге.\n", 72 | "\n", 73 | "Размер `chunk_size` нужно выбирать исходя из нескольких показателей:\n", 74 | "* Допустимая длина контекста для эмбеддинг-модели. Yandex GPT Embeddings допускают 2048 токенов, в то время как многие открытые модели HuggingFace имеют длину контекста 512-1024 токена\n", 75 | "* Допустимый размер окна контекста большой языковой модели. Если мы хотим использовать в запросе top 3 результатов поиска, то 3 * chunk_size+prompt_size+response_size должно не превышать длины контекста модели." 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 19, 81 | "id": "3c9d383f", 82 | "metadata": { 83 | "cellId": "503n864wu3akn1mjo318yj" 84 | }, 85 | "outputs": [ 86 | { 87 | "name": "stderr", 88 | "output_type": "stream", 89 | "text": [ 90 | " 0%| | 0/3 [00:00 В зависимости от объема текста, эта ячейка может выполняться достаточно длительное время - вспомним про задержку в 1 сек между запросами!" 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": 25, 553 | "id": "bcaa8e7d", 554 | "metadata": { 555 | "cellId": "j0bkb4g6igstk2umstio2j" 556 | }, 557 | "outputs": [], 558 | "source": [ 559 | "db = LanceDB.from_documents(fragments, embeddings, connection=table)" 560 | ] 561 | }, 562 | { 563 | "cell_type": "markdown", 564 | "id": "6dfb33a2-2946-4c60-899b-f4c0d6ddcbfc", 565 | "metadata": {}, 566 | "source": [ 567 | "Теперь посмотрим, насколько хорошо находятся фрагменты текста по какому-то запросу:" 568 | ] 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": 26, 573 | "id": "2f4be3a5", 574 | "metadata": { 575 | "cellId": "fdty939nkuf433bv37l2cs" 576 | }, 577 | "outputs": [ 578 | { 579 | "name": "stdout", 580 | "output_type": "stream", 581 | "text": [ 582 | "----------------------------------------\n", 583 | "самое важное в тех людях которые работают вместе с тобой над проектами над продуктами мы внутри яндекс класс ну на самом деле прям самое важное то есть я думаю ну понятно что я минимальный порог входа если вы не знаете формулу боец и хотите заниматься математикой и мэлин то возможно вы не пройдете наши собеседования но это не так критично а в том плане что это там можно изучить и тому подобное а есть важные человеческие качества которые мы тоже проверяем и во многом мы это проверяем например за счет того что студенты к нам не приходят сразу работать они там идут на проектах потом работали На проекте потом поработать на стажировке мы про них много чего узнали что не проверишь Нам очень важно чтобы человек во 1 был неприменимым программиста Ой был отменен Наоборот твое место это люди невы Смысл программиста в том чтобы автоматизировать И есть наши водятся чтобы автоматизировать рутину убрать людей Если человек готов Непрерывно делать 1 и ту же работу которую можно за вечер затянуть Это наверное не наш случай\n", 584 | "----------------------------------------\n", 585 | "то меньше это нормально слушай ну сеть яндекса растет постепенно но год от года да конечно понятно что на единицу как это на на объем единиц нужно некоторые объемы единиц людей которые так сказать будут обслуживать вот эти вот сервера но это не линейное а зависит не линейное конечно не надо Я так понимаю что все равно потребность есть достаточно большая да конечно мы всегда находимся в поиске возможно ну потому что у нас очень сложные циклы собеседования но тем не менее слушай мне то есть ты сейчас объяснил на самом деле\n", 586 | "----------------------------------------\n", 587 | "или 6 и Потому что мы иногда считаем скиллами да вот она да там большой ежегодной конференции вот с кем точно 5 а вот продукту вроде бы немножко побольше ну потому что понятно там еще разработка потом там какая то тестовая эксплуатация была Кстати как на ранней стадии понятно но и вот ты вот все эти стадии прошел и сейчас ты в общем смотришь на продукт как бы можно сказать не в некотором смысле сверху Вот твой путь ты мог бы как то оценить Ты внутри яндекс клауд 5 лет До этого чем ты занимался потому что были же навыки\n", 588 | "----------------------------------------\n", 589 | "Которым возможно интересна общая работа классу люкс архитектором в яндекс клауд Ты описал и команду и задачи и вот но все равно есть люди которые не знают блин сомневаются Подойдет мне не подойдет вот Где мог бы описать идеального кандидата человека которому нужно попробовать к нам прийти Чтобы вот он прям решился такой блин пойду Вот чтобы ты вот в этих людях ценил Давайте Приходите мы точно на вас посмотрим Давай попробую сформулировать так Идеального кандидата на виллы не аня История в том что приходят разные люди И они Все равно находят Себе место где им классно где им интересно и комфортно в команде а вот они разные прям по профилю правда вот когда уже смотришь на большую команду то я очень хорошо это понимаешь но вот Если вам нравятся Разбираться в каких то там Технических деталях и проблемах причем вам нравится это делать своими руками Но при этом Вам Доставляет удовольствие не то что вы разобрались А то что Вы помогли кому то тем что вы разобрались Вы можете хорошо донести свою мысль да и вообще там\n" 590 | ] 591 | } 592 | ], 593 | "source": [ 594 | "q=\"Почему стоит работать в Яндексе?\"\n", 595 | "\n", 596 | "res = db.similarity_search(q)\n", 597 | "for x in res:\n", 598 | " print('-'*40)\n", 599 | " print(x.page_content)" 600 | ] 601 | }, 602 | { 603 | "cell_type": "markdown", 604 | "id": "df22dc90-810c-44bf-b77f-c2763b05ed47", 605 | "metadata": {}, 606 | "source": [ 607 | "Ещё один полезный интерфейс для поиска текстов - это `Retriever`, убедимся, что он тоже работает:" 608 | ] 609 | }, 610 | { 611 | "cell_type": "code", 612 | "execution_count": 27, 613 | "id": "209d30d6", 614 | "metadata": { 615 | "cellId": "94wo2w5zglg0oyuubdoqlzc" 616 | }, 617 | "outputs": [ 618 | { 619 | "name": "stdout", 620 | "output_type": "stream", 621 | "text": [ 622 | "самое важное в тех людях которые работают вместе с тобой над проектами над продуктами мы внутри яндекс класс ну на самом деле прям самое важное то есть я думаю ну понятно что я минимальный порог входа если вы не знаете формулу боец и хотите заниматься математикой и мэлин то возможно вы не пройдете наши собеседования но это не так критично а в том плане что это там можно изучить и тому подобное а есть важные человеческие качества которые мы тоже проверяем и во многом мы это проверяем например за счет того что студенты к нам не приходят сразу работать они там идут на проектах потом работали На проекте потом поработать на стажировке мы про них много чего узнали что не проверишь Нам очень важно чтобы человек во 1 был неприменимым программиста Ой был отменен Наоборот твое место это люди невы Смысл программиста в том чтобы автоматизировать И есть наши водятся чтобы автоматизировать рутину убрать людей Если человек готов Непрерывно делать 1 и ту же работу которую можно за вечер затянуть Это наверное не наш случай\n", 623 | "то меньше это нормально слушай ну сеть яндекса растет постепенно но год от года да конечно понятно что на единицу как это на на объем единиц нужно некоторые объемы единиц людей которые так сказать будут обслуживать вот эти вот сервера но это не линейное а зависит не линейное конечно не надо Я так понимаю что все равно потребность есть достаточно большая да конечно мы всегда находимся в поиске возможно ну потому что у нас очень сложные циклы собеседования но тем не менее слушай мне то есть ты сейчас объяснил на самом деле\n", 624 | "или 6 и Потому что мы иногда считаем скиллами да вот она да там большой ежегодной конференции вот с кем точно 5 а вот продукту вроде бы немножко побольше ну потому что понятно там еще разработка потом там какая то тестовая эксплуатация была Кстати как на ранней стадии понятно но и вот ты вот все эти стадии прошел и сейчас ты в общем смотришь на продукт как бы можно сказать не в некотором смысле сверху Вот твой путь ты мог бы как то оценить Ты внутри яндекс клауд 5 лет До этого чем ты занимался потому что были же навыки\n", 625 | "Которым возможно интересна общая работа классу люкс архитектором в яндекс клауд Ты описал и команду и задачи и вот но все равно есть люди которые не знают блин сомневаются Подойдет мне не подойдет вот Где мог бы описать идеального кандидата человека которому нужно попробовать к нам прийти Чтобы вот он прям решился такой блин пойду Вот чтобы ты вот в этих людях ценил Давайте Приходите мы точно на вас посмотрим Давай попробую сформулировать так Идеального кандидата на виллы не аня История в том что приходят разные люди И они Все равно находят Себе место где им классно где им интересно и комфортно в команде а вот они разные прям по профилю правда вот когда уже смотришь на большую команду то я очень хорошо это понимаешь но вот Если вам нравятся Разбираться в каких то там Технических деталях и проблемах причем вам нравится это делать своими руками Но при этом Вам Доставляет удовольствие не то что вы разобрались А то что Вы помогли кому то тем что вы разобрались Вы можете хорошо донести свою мысль да и вообще там\n", 626 | "конференция давай мы нас так нас так кладем а сейчас сначала вернемся к началу а именно к тому Как ты ну ты уже попал в компанию да ты работаешь все отлично а чем ты занимаешься давай расскажу чем я занимаюсь я в яндексе занимаюсь глобальной сетью что мы под этим подразумеваем У моего подразделения есть как бы 2 крыла 1 крыло оно отвечает за внешнее облако Яндекс облако яндекс клауд я думаю тебе про него точно не надо рассказывать 2 часть отвечает за внутреннюю инфраструктуру яндекса Чтобы полностью рассказать о том чем мы занимаемся нужно отступить еще на шаг назад В яндексе довольно крупная инфраструктура Серверная сетевая в том числе у нас есть свои дата центры мы сами проектируем серверное железо понятное дело что мы не производим память диски процессоры тем не менее хардверный дизайн Дизайн термодизайн в том числе это как бы все наше Проектируем сами стойки Это позволяет Нужно устанавливать большее количество серверов туда подводить на 1 стойку Больше электричества В наших дата центрах их мы сами тоже\n" 627 | ] 628 | } 629 | ], 630 | "source": [ 631 | "retriever = db.as_retriever(\n", 632 | " search_kwargs={\"k\": 5}\n", 633 | ")\n", 634 | "res = retriever.get_relevant_documents(q)\n", 635 | "for x in res:\n", 636 | " print(x.page_content)" 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "id": "13c08e24-b385-4853-bad4-73c5aae88425", 642 | "metadata": {}, 643 | "source": [ 644 | "## Подключаемся к Yandex GPT\n", 645 | "\n", 646 | "Фреймворк LangChain поддерживает интеграцию с различными большими языковыми моделями, но Yandex GPT в их число не входит. Поэтому, как и в случае с эмбеддингами, нам надо написать соответствующий адаптер, предоставляющий доступ к Yandex GPT в формате LangChain. Для подробностей вызова Yandex GPT можно обратиться к документации по [YandexGPT API](https://cloud.yandex.ru/docs/yandexgpt/)" 647 | ] 648 | }, 649 | { 650 | "cell_type": "code", 651 | "execution_count": 28, 652 | "id": "423a58c1", 653 | "metadata": { 654 | "cellId": "j17m1mbp5hgxbgxvx0xnz" 655 | }, 656 | "outputs": [], 657 | "source": [ 658 | "from typing import Any, List, Mapping, Optional\n", 659 | "from langchain.callbacks.manager import CallbackManagerForLLMRun\n", 660 | "import requests\n", 661 | "\n", 662 | "class YandexLLM(langchain.llms.base.LLM):\n", 663 | " api_key: str = None\n", 664 | " iam_token: str = None\n", 665 | " folder_id: str\n", 666 | " max_tokens : int = 1500\n", 667 | " temperature : float = 1\n", 668 | " instruction_text : str = None\n", 669 | "\n", 670 | " @property\n", 671 | " def _llm_type(self) -> str:\n", 672 | " return \"yagpt\"\n", 673 | "\n", 674 | " def _call(\n", 675 | " self,\n", 676 | " prompt: str,\n", 677 | " stop: Optional[List[str]] = None,\n", 678 | " run_manager: Optional[CallbackManagerForLLMRun] = None,\n", 679 | " ) -> str:\n", 680 | " if stop is not None:\n", 681 | " raise ValueError(\"stop kwargs are not permitted.\")\n", 682 | " headers = { \"x-folder-id\" : self.folder_id }\n", 683 | " if self.iam_token:\n", 684 | " headers[\"Authorization\"] = f\"Bearer {self.iam_token}\"\n", 685 | " if self.api_key:\n", 686 | " headers[\"Authorization\"] = f\"Api-key {self.api_key}\"\n", 687 | " req = {\n", 688 | " \"model\": \"general\",\n", 689 | " \"instruction_text\": self.instruction_text,\n", 690 | " \"request_text\": prompt,\n", 691 | " \"generation_options\": {\n", 692 | " \"max_tokens\": self.max_tokens,\n", 693 | " \"temperature\": self.temperature\n", 694 | " }\n", 695 | " }\n", 696 | " res = requests.post(\"https://llm.api.cloud.yandex.net/llm/v1alpha/instruct\",\n", 697 | " headers=headers, json=req).json()\n", 698 | " return res['result']['alternatives'][0]['text']\n", 699 | "\n", 700 | " @property\n", 701 | " def _identifying_params(self) -> Mapping[str, Any]:\n", 702 | " \"\"\"Get the identifying parameters.\"\"\"\n", 703 | " return {\"max_tokens\": self.max_tokens, \"temperature\" : self.temperature }" 704 | ] 705 | }, 706 | { 707 | "cell_type": "markdown", 708 | "id": "7808edf6-710d-4698-b5c1-effc179b7463", 709 | "metadata": {}, 710 | "source": [ 711 | "Теперь попросим модель ответить на наш вопрос от лица сотрудника Yandex Cloud:" 712 | ] 713 | }, 714 | { 715 | "cell_type": "code", 716 | "execution_count": 29, 717 | "id": "f36716bd", 718 | "metadata": { 719 | "cellId": "17hb5txavf5iiscbggvvjg" 720 | }, 721 | "outputs": [], 722 | "source": [ 723 | "instructions = \"\"\"\n", 724 | "Представь себе, что ты сотрудник Yandex Cloud. Твоя задача - вежливо и по мере своих сил отвечать на все вопросы собеседника.\"\"\"\n", 725 | "\n", 726 | "llm = YandexLLM(api_key=api_key, folder_id=folder_id,\n", 727 | " instruction_text = instructions)" 728 | ] 729 | }, 730 | { 731 | "cell_type": "code", 732 | "execution_count": 30, 733 | "id": "3000d8e0", 734 | "metadata": { 735 | "cellId": "4ek9wo0my135q1p79ismqh" 736 | }, 737 | "outputs": [ 738 | { 739 | "data": { 740 | "text/plain": [ 741 | "'Конечно, я с радостью расскажу вам о том, почему работа в Яндекс Клауд является необходимым и привлекательным выбором. Вот несколько причин:\\n\\n1. Разнообразие и инновации. В отличие от других компаний, Яндекс постоянно расширяет свою продуктовую линейку, предлагая инновационные решения и сети для работы. Это дает возможность сотрудникам работать над множеством проектов и получить опыт в различных областях.\\n2. Развитие карьеры. Яндекс Клауд предоставляет своим сотрудникам огромные возможности для карьерного роста. Компания обеспечивает обучение, предоставление доступа к ресурсам и накопленный экспертизе, что позволяет людям непрерывно расти и развиваться.\\n3. Неограниченные возможности. Яндекс ценит своих сотрудников как людей, способных решать любые задачи, профессиональные и личные. Компания учитывает индивидуальные интересы и потребности работников, целевая их целеустремленность и подход к работе.\\n4. Сообщество и современные решения. Креативная и инновационная команда сотрудников Яндекса Клауд ежедневно работает над созданием новых продуктов и решений. Эти идеи могут познакомить участников с современным миром технологий.\\n5. Стабильная компания. Тип компании всегда поддерживается, а риски минимизируются, согласно получениям \"считанных\" данных, внутренних тестов и подходов. Поэтому Яндекс уверенно можно назвать компанией, обеспечивающей высокий уровень защиты. \\n\\nНадеюсь, эта информация поможет вам рассмотреть трудоустройство в компании Яндекс.'" 742 | ] 743 | }, 744 | "execution_count": 30, 745 | "metadata": {}, 746 | "output_type": "execute_result" 747 | } 748 | ], 749 | "source": [ 750 | "llm(q)" 751 | ] 752 | }, 753 | { 754 | "cell_type": "markdown", 755 | "id": "9bd75c79-ae33-4558-ae30-a2680ba6e67d", 756 | "metadata": {}, 757 | "source": [ 758 | "В данном примере мы пока что никак не использовали наши текстовые документы.\n", 759 | "\n", 760 | "## Собираем Retrieval-Augmented Generation\n", 761 | "\n", 762 | "Пришла пора соединить всё вместе и научить бота отвечать на вопросы, подглядывая в наш текстовый корпус. Для этого используем механизм цепочек (*chains*). Основную функциональность реализует `StuffDocumentsChain`, которая делает следующее:\n", 763 | "\n", 764 | "1. Берёт коллекцию документов `input_documents`\n", 765 | "1. Каждый из них пропускает через `document_prompt`, и затем объединяет вместе.\n", 766 | "1. Данный текст помещается в переменную `document_variable_name`, и передаётся большой языковой модели `llm`\n", 767 | "\n", 768 | "В нашем случае `document_prompt` не будет модицицировать документ, а основной запрос к LLM будет содержать в себе инструкции для Yandex GPT. " 769 | ] 770 | }, 771 | { 772 | "cell_type": "code", 773 | "execution_count": 31, 774 | "id": "4a500165", 775 | "metadata": { 776 | "cellId": "77y4w5s6seg08g1utincn7a" 777 | }, 778 | "outputs": [ 779 | { 780 | "data": { 781 | "text/plain": [ 782 | "'Если говорить о работе в Яндексе, то в первую очередь стоит отметить, что наша компания активно развивается и ставит перед собой амбициозные цели. Мы всегда уделяем внимание новым технологиям и стремимся быть лидерами в своей отрасли. Кроме того, мы ценим наших сотрудников и делаем всё возможное, чтобы создать комфортные условия для работы. Если вы интересуетесь технологиями, хотите развиваться и достигать новых успехов, то работа в Яндексе может стать отличной возможностью для этого.'" 783 | ] 784 | }, 785 | "execution_count": 31, 786 | "metadata": {}, 787 | "output_type": "execute_result" 788 | } 789 | ], 790 | "source": [ 791 | "# Промпт для обработки документов\n", 792 | "document_prompt = langchain.prompts.PromptTemplate(\n", 793 | " input_variables=[\"page_content\"], template=\"{page_content}\"\n", 794 | ")\n", 795 | "\n", 796 | "# Промпт для языковой модели\n", 797 | "document_variable_name = \"context\"\n", 798 | "stuff_prompt_override = \"\"\"\n", 799 | "Пожалуйста, посмотри на текст ниже и ответь на вопрос, используя информацию из этого текста.\n", 800 | "Текст:\n", 801 | "-----\n", 802 | "{context}\n", 803 | "-----\n", 804 | "Вопрос:\n", 805 | "{query}\"\"\"\n", 806 | "prompt = langchain.prompts.PromptTemplate(\n", 807 | " template=stuff_prompt_override, input_variables=[\"context\", \"query\"]\n", 808 | ")\n", 809 | "\n", 810 | "# Создаём цепочку\n", 811 | "llm_chain = langchain.chains.LLMChain(llm=llm, prompt=prompt)\n", 812 | "chain = langchain.chains.StuffDocumentsChain(\n", 813 | " llm_chain=llm_chain,\n", 814 | " document_prompt=document_prompt,\n", 815 | " document_variable_name=document_variable_name,\n", 816 | ")\n", 817 | "chain.run(input_documents=res, query=q)" 818 | ] 819 | }, 820 | { 821 | "cell_type": "markdown", 822 | "id": "064633e5-10b9-4914-acde-950b96dc043f", 823 | "metadata": {}, 824 | "source": [ 825 | "Чтобы ещё более улучшить результат, мы можем использовать хитрый механизм перемешивания документов, таким образом, чтобы наиболее значимые документы были ближе к началу запроса. Также мы оформим все операции в одну функцию `answer`:" 826 | ] 827 | }, 828 | { 829 | "cell_type": "code", 830 | "execution_count": 32, 831 | "id": "e38c691c", 832 | "metadata": { 833 | "cellId": "dhzsgivm5s9mpky1db082" 834 | }, 835 | "outputs": [], 836 | "source": [ 837 | "from langchain.document_transformers import LongContextReorder\n", 838 | "reorderer = LongContextReorder()\n", 839 | "\n", 840 | "def answer(query,reorder=True,print_results=False):\n", 841 | " results = retriever.get_relevant_documents(query)\n", 842 | " if print_results:\n", 843 | " for x in results:\n", 844 | " print(f\"{x.page_content}\\n--------\")\n", 845 | " if reorder:\n", 846 | " results = reorderer.transform_documents(results)\n", 847 | " return chain.run(input_documents=results, query=query)" 848 | ] 849 | }, 850 | { 851 | "cell_type": "code", 852 | "execution_count": 33, 853 | "id": "96149ae4", 854 | "metadata": { 855 | "cellId": "is1wpwxhw1inxmwsqvixqa" 856 | }, 857 | "outputs": [ 858 | { 859 | "name": "stdout", 860 | "output_type": "stream", 861 | "text": [ 862 | "самое важное в тех людях которые работают вместе с тобой над проектами над продуктами мы внутри яндекс класс ну на самом деле прям самое важное то есть я думаю ну понятно что я минимальный порог входа если вы не знаете формулу боец и хотите заниматься математикой и мэлин то возможно вы не пройдете наши собеседования но это не так критично а в том плане что это там можно изучить и тому подобное а есть важные человеческие качества которые мы тоже проверяем и во многом мы это проверяем например за счет того что студенты к нам не приходят сразу работать они там идут на проектах потом работали На проекте потом поработать на стажировке мы про них много чего узнали что не проверишь Нам очень важно чтобы человек во 1 был неприменимым программиста Ой был отменен Наоборот твое место это люди невы Смысл программиста в том чтобы автоматизировать И есть наши водятся чтобы автоматизировать рутину убрать людей Если человек готов Непрерывно делать 1 и ту же работу которую можно за вечер затянуть Это наверное не наш случай\n", 863 | "--------\n", 864 | "конференция давай мы нас так нас так кладем а сейчас сначала вернемся к началу а именно к тому Как ты ну ты уже попал в компанию да ты работаешь все отлично а чем ты занимаешься давай расскажу чем я занимаюсь я в яндексе занимаюсь глобальной сетью что мы под этим подразумеваем У моего подразделения есть как бы 2 крыла 1 крыло оно отвечает за внешнее облако Яндекс облако яндекс клауд я думаю тебе про него точно не надо рассказывать 2 часть отвечает за внутреннюю инфраструктуру яндекса Чтобы полностью рассказать о том чем мы занимаемся нужно отступить еще на шаг назад В яндексе довольно крупная инфраструктура Серверная сетевая в том числе у нас есть свои дата центры мы сами проектируем серверное железо понятное дело что мы не производим память диски процессоры тем не менее хардверный дизайн Дизайн термодизайн в том числе это как бы все наше Проектируем сами стойки Это позволяет Нужно устанавливать большее количество серверов туда подводить на 1 стойку Больше электричества В наших дата центрах их мы сами тоже\n", 865 | "--------\n", 866 | "Которым возможно интересна общая работа классу люкс архитектором в яндекс клауд Ты описал и команду и задачи и вот но все равно есть люди которые не знают блин сомневаются Подойдет мне не подойдет вот Где мог бы описать идеального кандидата человека которому нужно попробовать к нам прийти Чтобы вот он прям решился такой блин пойду Вот чтобы ты вот в этих людях ценил Давайте Приходите мы точно на вас посмотрим Давай попробую сформулировать так Идеального кандидата на виллы не аня История в том что приходят разные люди И они Все равно находят Себе место где им классно где им интересно и комфортно в команде а вот они разные прям по профилю правда вот когда уже смотришь на большую команду то я очень хорошо это понимаешь но вот Если вам нравятся Разбираться в каких то там Технических деталях и проблемах причем вам нравится это делать своими руками Но при этом Вам Доставляет удовольствие не то что вы разобрались А то что Вы помогли кому то тем что вы разобрались Вы можете хорошо донести свою мысль да и вообще там\n", 867 | "--------\n", 868 | "то меньше это нормально слушай ну сеть яндекса растет постепенно но год от года да конечно понятно что на единицу как это на на объем единиц нужно некоторые объемы единиц людей которые так сказать будут обслуживать вот эти вот сервера но это не линейное а зависит не линейное конечно не надо Я так понимаю что все равно потребность есть достаточно большая да конечно мы всегда находимся в поиске возможно ну потому что у нас очень сложные циклы собеседования но тем не менее слушай мне то есть ты сейчас объяснил на самом деле\n", 869 | "--------\n", 870 | "которые могут тебя вот сдвинуть в сторону от успешного результата Слушай ну ты сейчас Очень круто рассказал про вот этот 2 актерам на самом деле работу каждого члена команды отдельно потому что он пришел у него есть длинный трек большой проект он его делает А если поговорить в целом о команде а какой челлендж для самой команды стоит то есть и вот ну все равно же ты как руководитель определяешь направление работы всех этих людей Какие перед ними ты задачи ставишь Ох наверное челлендж этой команды они только этой потому что У нас в облаке очень много Как это Командные работы которые не привязаны там к 1 подразделению пусть даже большому а совместное наверное основной челлендж во 1 Обеспечить те темпы роста которые мы хотим И видим для яндекс облака И не просто обеспечить А попробовать как это превзойти собственное ожидание И это касается как всего портфеля яндекс облака в целом Так и на самом деле Конкретных сервисов в нем Потому что это же очень здорово когда ты стартуешь сервис да у него там казалось бы там\n", 871 | "--------\n" 872 | ] 873 | }, 874 | { 875 | "data": { 876 | "text/plain": [ 877 | "'В Yandex очень важно, чтобы каждый член команды был готов принимать решения и ответственно подходить к выполнению своей работы. В команде царят дружелюбие и поддержка, коллектив старается помогать друг другу и поддерживать инициативу. Каждый сотрудник может внести свой вклад в общее дело и внести идеи, новые идеи приветствуются. Здесь нет дресс-кода и закрытых дверей. В помещении работает несколько компьютеров, множество принтеров, есть современная лаборатория. Наша цель как IT компании - развитие и внедрение инновационных решений в IT бизнес. Для этого сотрудники Яндекса должны постоянно совершенствовать свои навыки.\\nЕсли ты хочешь попробовать себя в качестве члена команды Яндекса, пожалуйста, расскажи о своих навыках и опыте.'" 878 | ] 879 | }, 880 | "execution_count": 33, 881 | "metadata": {}, 882 | "output_type": "execute_result" 883 | } 884 | ], 885 | "source": [ 886 | "answer(\"Почему хорошо работать в Yandex?\",print_results=True)" 887 | ] 888 | }, 889 | { 890 | "cell_type": "markdown", 891 | "id": "38cd717f-c17c-43d8-8732-789ac5efb68f", 892 | "metadata": {}, 893 | "source": [ 894 | "Можно сравнить результаты, выдаваемые Yandex GPT напрямую, с результатами нашей генерации на основе документов:" 895 | ] 896 | }, 897 | { 898 | "cell_type": "code", 899 | "execution_count": 36, 900 | "id": "a006ce0f", 901 | "metadata": { 902 | "cellId": "4byevx8504hx6nicfe2hia" 903 | }, 904 | "outputs": [ 905 | { 906 | "name": "stdout", 907 | "output_type": "stream", 908 | "text": [ 909 | "Ответ YaGPT: С удовольствием отвечу на ваши вопрос по ML команде Яндекс облака.\n", 910 | "Ответ бота: Конечно! Что вы хотите узнать о нашей ML-команде?\n" 911 | ] 912 | } 913 | ], 914 | "source": [ 915 | "def compare(q):\n", 916 | " print(f\"Ответ YaGPT: {llm(q)}\")\n", 917 | " print(f\"Ответ бота: {answer(q)}\")\n", 918 | " \n", 919 | "compare(\"Что ты можешь сказать об ML-команде Яндекс-облака?\")" 920 | ] 921 | }, 922 | { 923 | "cell_type": "markdown", 924 | "id": "08d819a9-6b9c-4760-805c-fcac57eac152", 925 | "metadata": { 926 | "cellId": "i35qdb5xtoohh4yt0ah15v" 927 | }, 928 | "source": [ 929 | "## Сохраняем векторную БД в Storage\n", 930 | "\n", 931 | "Для следующего этапа - вопрос-ответного бота - нам потребуется созданная нами база данных с документами. Поэтому скопируем её на хранилище s3:" 932 | ] 933 | }, 934 | { 935 | "cell_type": "code", 936 | "execution_count": 37, 937 | "id": "37c399e7-4fc6-43b4-8e7b-7d796c747454", 938 | "metadata": {}, 939 | "outputs": [], 940 | "source": [ 941 | "!cp -R ./store /home/jupyter/datasphere/s3/s3store/shwars/" 942 | ] 943 | }, 944 | { 945 | "cell_type": "code", 946 | "execution_count": null, 947 | "id": "f511df20-551e-47b8-809c-52a9169a6c7c", 948 | "metadata": {}, 949 | "outputs": [], 950 | "source": [] 951 | } 952 | ], 953 | "metadata": { 954 | "kernelspec": { 955 | "display_name": "DataSphere Kernel", 956 | "language": "python", 957 | "name": "python3" 958 | }, 959 | "language_info": { 960 | "codemirror_mode": { 961 | "name": "ipython", 962 | "version": 3 963 | }, 964 | "file_extension": ".py", 965 | "mimetype": "text/x-python", 966 | "name": "python", 967 | "nbconvert_exporter": "python", 968 | "pygments_lexer": "ipython3", 969 | "version": "3.7.7" 970 | }, 971 | "notebookId": "369d3e82-763a-4bcd-b5cc-0cfe14f13f53", 972 | "notebookPath": "VideoQABot/LangChainQA.ipynb" 973 | }, 974 | "nbformat": 4, 975 | "nbformat_minor": 5 976 | } 977 | -------------------------------------------------------------------------------- /PrepareDataset.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8dfd0492", 6 | "metadata": { 7 | "cellId": "3a4ukb42dhubpvgt5ynds", 8 | "execution_id": "a713e3a6-6d38-4bde-abe2-8be3c08b92a7" 9 | }, 10 | "source": [ 11 | "## Подготавливаем данные\n", 12 | "\n", 13 | "В качестве исходного материала для чат-бота будем использовать ролики с YouTube. Для того, чтобы с ними работать, установим библиотеку [`PyTube`](https://pytube.io/)." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "id": "5e0f7371", 20 | "metadata": { 21 | "cellId": "mvsym1t3d1i6a3czmso4ru" 22 | }, 23 | "outputs": [ 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "Defaulting to user installation because normal site-packages is not writeable\n", 29 | "Collecting pytube\n", 30 | " Downloading pytube-15.0.0-py3-none-any.whl (57 kB)\n", 31 | "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m57.6/57.6 kB\u001b[0m \u001b[31m1.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", 32 | "\u001b[?25hInstalling collected packages: pytube\n", 33 | "\u001b[33m WARNING: The script pytube is installed in '/home/jupyter/.local/bin' which is not on PATH.\n", 34 | " Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\u001b[0m\u001b[33m\n", 35 | "\u001b[0mSuccessfully installed pytube-15.0.0\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "%pip install pytube" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "id": "b857187f", 46 | "metadata": { 47 | "cellId": "nwq1olc6j2gue9y0xyad", 48 | "execution_id": "67546bba-c0e1-4181-a472-54ed597301ef" 49 | }, 50 | "source": [ 51 | "Возможно, прежде чем выполнять следующий код, вам потребуется перезапустить Kernel.\n", 52 | "\n", 53 | "### Скачиваем звуковые дорожки к видео\n", 54 | "\n", 55 | "Посмотрим, как можно обратиться к ролику на YouTube по ссылке:" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 1, 61 | "id": "7851e207", 62 | "metadata": { 63 | "cellId": "t8qautm182e8pztlbhzrkn" 64 | }, 65 | "outputs": [ 66 | { 67 | "data": { 68 | "text/plain": [ 69 | "('Yandex Scale 2022. Главный доклад.',\n", 70 | " [, , , , , , , , , , , , , , , , , , , ])" 71 | ] 72 | }, 73 | "execution_count": 1, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "from pytube import YouTube\n", 80 | "yt = YouTube(\"https://www.youtube.com/watch?v=7mWPlXvPG7A\")\n", 81 | "yt.title,yt.streams" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "14db736d", 87 | "metadata": { 88 | "cellId": "llggfqob2io9h188ac0iqv", 89 | "execution_id": "59941128-5aa9-4471-9fdd-2fca7c3a5d6c" 90 | }, 91 | "source": [ 92 | "Мы видим, что с каждым видео связано несколько потоков, включая звуковые. Мы можем отфильтровать нужные нам потоки, взять первый из них, и сохранить на диск:" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 2, 98 | "id": "a168ab29", 99 | "metadata": { 100 | "cellId": "j525xa84y2o8n2xg4l18h" 101 | }, 102 | "outputs": [ 103 | { 104 | "data": { 105 | "text/plain": [ 106 | "" 107 | ] 108 | }, 109 | "execution_count": 2, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "yt.streams.filter(mime_type=\"audio/webm\").first()#.download(output_path=\"../audio\",filename=\"1.opus\")" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "f33e5fae", 121 | "metadata": { 122 | "cellId": "rpqa4a9vd7o5gab1mm85lh", 123 | "execution_id": "af11a049-f8cb-4d81-8737-f307720c0300" 124 | }, 125 | "source": [ 126 | "Возьмём теперь коллекцию видео, и сохраним все звуковые дорожки от них:" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 3, 132 | "id": "5bf574c0", 133 | "metadata": { 134 | "cellId": "g4aa7yqbctflq5vhx9xe8" 135 | }, 136 | "outputs": [ 137 | { 138 | "name": "stdout", 139 | "output_type": "stream", 140 | "text": [ 141 | "Downloading Про людей. ML-команда Yandex Cloud\n", 142 | "Downloading Про людей. Команда архитекторов\n", 143 | "Downloading Про людей. Команда сетевой инфраструктуры\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "videos = ['https://www.youtube.com/watch?v=2T1hRvnIu1U',\n", 149 | " 'https://www.youtube.com/watch?v=9O8eEmlSiBw',\n", 150 | " 'https://www.youtube.com/watch?v=mPvLf-TqS74']\n", 151 | "\n", 152 | "for i,url in enumerate(videos):\n", 153 | " yt = YouTube(url)\n", 154 | " print(f\"Downloading {yt.title}\")\n", 155 | " yt.streams.filter(mime_type=\"audio/webm\").first().download(output_path=\"audio\",filename=f\"{i}.opus\")" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "b8eb86c1", 161 | "metadata": { 162 | "cellId": "lz6dsi65bbntz1ndvidcg8", 163 | "execution_id": "026218ca-07e6-4133-92b5-1feaa4b37480" 164 | }, 165 | "source": [ 166 | "Для того, чтобы SpeechKit мог распознать речь в файле, файл должен быть в определённом формате и битрейте. Поэтому откроем все наши звуковые дорожки, и сделаем им ресамплинг к частоте 8 kHz с помощью библиотеки `librosa`. Этот процесс может занять некоторое время.\n", 167 | "\n", 168 | "> Вы также можете произвести преобразование форматов и битрейта из командной строки с помощью утилиты `ffmpeg`" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 4, 174 | "id": "7ff3a25a", 175 | "metadata": { 176 | "cellId": "84bil4jbfm16piyhjyof7" 177 | }, 178 | "outputs": [ 179 | { 180 | "name": "stdout", 181 | "output_type": "stream", 182 | "text": [ 183 | "Processing audio/0.opus\n" 184 | ] 185 | }, 186 | { 187 | "name": "stderr", 188 | "output_type": "stream", 189 | "text": [ 190 | ":8: UserWarning: PySoundFile failed. Trying audioread instead.\n", 191 | " au,sr = librosa.load(fn,sr=target_sr)\n", 192 | "/usr/local/lib/python3.10/dist-packages/librosa/core/audio.py:184: FutureWarning: librosa.core.audio.__audioread_load\n", 193 | "\tDeprecated as of librosa version 0.10.0.\n", 194 | "\tIt will be removed in librosa version 1.0.\n", 195 | " y, sr_native = __audioread_load(path, offset, duration, dtype)\n" 196 | ] 197 | }, 198 | { 199 | "name": "stdout", 200 | "output_type": "stream", 201 | "text": [ 202 | "Processing audio/1.opus\n" 203 | ] 204 | }, 205 | { 206 | "name": "stderr", 207 | "output_type": "stream", 208 | "text": [ 209 | ":8: UserWarning: PySoundFile failed. Trying audioread instead.\n", 210 | " au,sr = librosa.load(fn,sr=target_sr)\n", 211 | "/usr/local/lib/python3.10/dist-packages/librosa/core/audio.py:184: FutureWarning: librosa.core.audio.__audioread_load\n", 212 | "\tDeprecated as of librosa version 0.10.0.\n", 213 | "\tIt will be removed in librosa version 1.0.\n", 214 | " y, sr_native = __audioread_load(path, offset, duration, dtype)\n" 215 | ] 216 | }, 217 | { 218 | "name": "stdout", 219 | "output_type": "stream", 220 | "text": [ 221 | "Processing audio/2.opus\n" 222 | ] 223 | }, 224 | { 225 | "name": "stderr", 226 | "output_type": "stream", 227 | "text": [ 228 | ":8: UserWarning: PySoundFile failed. Trying audioread instead.\n", 229 | " au,sr = librosa.load(fn,sr=target_sr)\n", 230 | "/usr/local/lib/python3.10/dist-packages/librosa/core/audio.py:184: FutureWarning: librosa.core.audio.__audioread_load\n", 231 | "\tDeprecated as of librosa version 0.10.0.\n", 232 | "\tIt will be removed in librosa version 1.0.\n", 233 | " y, sr_native = __audioread_load(path, offset, duration, dtype)\n" 234 | ] 235 | } 236 | ], 237 | "source": [ 238 | "import glob\n", 239 | "import librosa\n", 240 | "import soundfile as sf\n", 241 | "\n", 242 | "target_sr = 8000\n", 243 | "for fn in glob.glob(\"audio/*.opus\"):\n", 244 | " print(f\"Processing {fn}\")\n", 245 | " au,sr = librosa.load(fn,sr=target_sr)\n", 246 | " sf.write(fn.replace('.opus','.ogg'),au,target_sr,format='ogg',subtype='opus')\n", 247 | " " 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "id": "823a2997", 253 | "metadata": { 254 | "cellId": "431oei72jp54aonov0dkuj", 255 | "execution_id": "3bc6d675-c577-4736-a0b6-d628520ec7d8" 256 | }, 257 | "source": [ 258 | "### Распознаём речь с помощью SpeechKit\n", 259 | "\n", 260 | "У SpeechKit есть несколько режимов работы. Для распознавания больших файлов лучше всего подходит [асинхронное распознавание](https://cloud.yandex.ru/docs/speechkit/stt/transcribation). Для асинхронного распознавания необходимо, чтобы исходные файлы лежали в хранилище Yandex S3.\n", 261 | "\n", 262 | "Копируем аудио-файлы в хранилище S3, подключенное к DataSphere, чтобы можно было использовать их для распознавания речи.\n", 263 | "\n", 264 | "> **ВАЖНО:** В рамках мастер-класса сразу несколько участников могут работать с одним хранилищем s3. Поэтому используйте уникальные имена директорий для своих файлов. В данном примере я использую имя **shwars**, которое прошу Вас изменить на какой-то уникальный идентификатор!" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 11, 270 | "id": "0e6f5daa-4f51-4a3b-8ca2-8be99a1bcc38", 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "user = 'shwars'" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 6, 280 | "id": "a96c3984", 281 | "metadata": { 282 | "cellId": "6oogywviabowzuwj5av4b" 283 | }, 284 | "outputs": [], 285 | "source": [ 286 | "!mkdir -p /home/jupyter/datasphere/s3/s3store/shwars/audio\n", 287 | "!cp audio/*.ogg /home/jupyter/datasphere/s3/s3store/shwars/audio" 288 | ] 289 | }, 290 | { 291 | "cell_type": "markdown", 292 | "id": "cd74b92a", 293 | "metadata": { 294 | "cellId": "u9za9ep9zpid5zojvjl8", 295 | "execution_id": "88a42ca1-17da-402f-ba4c-79c58652f08a" 296 | }, 297 | "source": [ 298 | "Для работы со Speech Kit потребуется [создать ключ API](https://cloud.yandex.ru/docs/iam/operations/api-key/create). Далее, чтобы не указывать ключ в программном коде, нужно определить ключи как **секреты** в DataSphere. После того, как вы это сделаете, ключи станут доступны просто как переменные окружения.\n", 299 | "\n", 300 | "Весь процесс работы [описан в документации](https://cloud.yandex.ru/docs/speechkit/stt/api/transcribation-api)." 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": 7, 306 | "id": "17e27243", 307 | "metadata": { 308 | "cellId": "h14xoodj1esl7g9ucviap9" 309 | }, 310 | "outputs": [], 311 | "source": [ 312 | "import os\n", 313 | "api_key = os.environ['api_key']" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "id": "04e8c92f", 319 | "metadata": { 320 | "cellId": "msdzbrg4rso6wd4197vre9", 321 | "execution_id": "f4fb42ba-2b11-41b4-942e-cc6f29dcd1df" 322 | }, 323 | "source": [ 324 | "Для запуска распознавания конкретного файла необходимо сделать соответствующий POST-запрос." 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 8, 330 | "id": "70d10477", 331 | "metadata": { 332 | "cellId": "1ubf3jbpm8yogrf01f0tfh" 333 | }, 334 | "outputs": [], 335 | "source": [ 336 | "import requests\n", 337 | "\n", 338 | "def submit_for_sr(audio_file):\n", 339 | " j = {\n", 340 | " \"config\": {\n", 341 | " \"specification\": {\n", 342 | " \"languageCode\": \"ru-RU\",\n", 343 | " }\n", 344 | " },\n", 345 | " \"audio\": {\n", 346 | " \"uri\": audio_file\n", 347 | " }\n", 348 | " }\n", 349 | " res = requests.post(\"https://transcribe.api.cloud.yandex.net/speech/stt/v2/longRunningRecognize\",\n", 350 | " json = j,\n", 351 | " headers = { \"Authorization\" : f\"Api-Key {api_key}\" })\n", 352 | " return res.json()['id']" 353 | ] 354 | }, 355 | { 356 | "cell_type": "markdown", 357 | "id": "9a5b6b33", 358 | "metadata": { 359 | "cellId": "u1bj9ytfuz7cxnsqiz9pc", 360 | "execution_id": "3cb4014b-f70c-459d-806c-1bf80c7f0300" 361 | }, 362 | "source": [ 363 | "Инициируем распознавание первого файла. Если всё хорошо, в ответ мы получим `id`, который можно использовать для проверки результата операции." 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 9, 369 | "id": "9acfa35f", 370 | "metadata": { 371 | "cellId": "s6y72gso65a3v7ddl6jf01" 372 | }, 373 | "outputs": [ 374 | { 375 | "data": { 376 | "text/plain": [ 377 | "'e037l6d2kh1stl2a06th'" 378 | ] 379 | }, 380 | "execution_count": 9, 381 | "metadata": {}, 382 | "output_type": "execute_result" 383 | } 384 | ], 385 | "source": [ 386 | "id = submit_for_sr(f'https://storage.yandexcloud.net/s3store/{user}/audio/0.ogg')\n", 387 | "id" 388 | ] 389 | }, 390 | { 391 | "cell_type": "markdown", 392 | "id": "5d6ce9b6", 393 | "metadata": { 394 | "cellId": "vpupqer1x9l849xvieqph", 395 | "execution_id": "91b6ff69-f090-4df9-b29d-c86f91030048" 396 | }, 397 | "source": [ 398 | "Следующую ячейку можно запускать несколько раз, пока вы не получите результат распознавания." 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 10, 404 | "id": "5499627f", 405 | "metadata": { 406 | "cellId": "9jfw0xqyxv9a8jcp22acw" 407 | }, 408 | "outputs": [ 409 | { 410 | "data": { 411 | "text/plain": [ 412 | "{'done': False,\n", 413 | " 'id': 'e037l6d2kh1stl2a06th',\n", 414 | " 'createdAt': '2023-09-06T11:10:18Z',\n", 415 | " 'createdBy': 'ajegm68gol04oa04moef',\n", 416 | " 'modifiedAt': '2023-09-06T11:10:18Z'}" 417 | ] 418 | }, 419 | "execution_count": 10, 420 | "metadata": {}, 421 | "output_type": "execute_result" 422 | } 423 | ], 424 | "source": [ 425 | "requests.get(f\"https://operation.api.cloud.yandex.net/operations/{id}\",headers = { \"Authorization\" : f\"Api-Key {api_key}\" }).json()" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "id": "39911c46", 431 | "metadata": { 432 | "cellId": "w2gcdofqknd0lmcgp0sz3hq", 433 | "execution_id": "0652d263-691c-43f7-9636-00b2cb149d37" 434 | }, 435 | "source": [ 436 | "Теперь запустим процесс распознавания для всех наших файлов, и затем будем ждать, пока все результаты не будут получены:" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": 12, 442 | "id": "fb9d6257", 443 | "metadata": { 444 | "cellId": "d7apibl1i7d3pvhe8mg01d" 445 | }, 446 | "outputs": [ 447 | { 448 | "name": "stdout", 449 | "output_type": "stream", 450 | "text": [ 451 | "Submitted /home/jupyter/datasphere/s3/s3store/shwars/audio/0.ogg -> e03f21gliimn39j5rf2k\n", 452 | "Submitted /home/jupyter/datasphere/s3/s3store/shwars/audio/1.ogg -> e030ibfn96oi6v8dei6s\n", 453 | "Submitted /home/jupyter/datasphere/s3/s3store/shwars/audio/2.ogg -> e0305sjtmn73smtos6o6\n" 454 | ] 455 | } 456 | ], 457 | "source": [ 458 | "d = {}\n", 459 | "for fn in glob.glob(f'/home/jupyter/datasphere/s3/s3store/{user}/audio/*.ogg'):\n", 460 | " ext_name = fn.replace('/home/jupyter/datasphere/s3/','https://storage.yandexcloud.net/')\n", 461 | " id = submit_for_sr(ext_name)\n", 462 | " print(f\"Submitted {fn} -> {id}\")\n", 463 | " d[id] = fn " 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": 13, 469 | "id": "e2dafc8b", 470 | "metadata": { 471 | "cellId": "cjrgfc7swqs8hn5yh0hrih" 472 | }, 473 | "outputs": [ 474 | { 475 | "name": "stdout", 476 | "output_type": "stream", 477 | "text": [ 478 | "e03f21gliimn39j5rf2k -> waiting\n", 479 | "e030ibfn96oi6v8dei6s -> waiting\n", 480 | "e0305sjtmn73smtos6o6 -> waiting\n", 481 | "e03f21gliimn39j5rf2k -> waiting\n", 482 | "e030ibfn96oi6v8dei6s -> waiting\n", 483 | "e0305sjtmn73smtos6o6 -> waiting\n", 484 | "e03f21gliimn39j5rf2k -> waiting\n", 485 | "e030ibfn96oi6v8dei6s -> waiting\n", 486 | "e0305sjtmn73smtos6o6 -> waiting\n", 487 | "e03f21gliimn39j5rf2k -> waiting\n", 488 | "e030ibfn96oi6v8dei6s -> waiting\n", 489 | "e0305sjtmn73smtos6o6 -> waiting\n", 490 | "e03f21gliimn39j5rf2k -> ready\n", 491 | "e030ibfn96oi6v8dei6s -> waiting\n", 492 | "e0305sjtmn73smtos6o6 -> waiting\n", 493 | "e030ibfn96oi6v8dei6s -> ready\n", 494 | "e0305sjtmn73smtos6o6 -> ready\n" 495 | ] 496 | } 497 | ], 498 | "source": [ 499 | "import time \n", 500 | "\n", 501 | "def check_ready(id):\n", 502 | " res = requests.get(f\"https://operation.api.cloud.yandex.net/operations/{id}\",headers = { \"Authorization\" : f\"Api-Key {api_key}\" })\n", 503 | " res = res.json()\n", 504 | " if res['done']:\n", 505 | " return res['response']\n", 506 | " else:\n", 507 | " return None\n", 508 | "\n", 509 | "txt = {}\n", 510 | "while True:\n", 511 | " for k,v in d.items():\n", 512 | " if v in txt.keys():\n", 513 | " continue\n", 514 | " res = check_ready(k)\n", 515 | " if res is None:\n", 516 | " print(f\"{k} -> waiting\")\n", 517 | " else:\n", 518 | " print(f\"{k} -> ready\")\n", 519 | " txt[v] = ' '.join([x['alternatives'][0]['text'] for x in res['chunks']])\n", 520 | " if len(txt.keys())==len(d.keys()):\n", 521 | " break\n", 522 | " time.sleep(20)" 523 | ] 524 | }, 525 | { 526 | "cell_type": "markdown", 527 | "id": "352cdf39", 528 | "metadata": { 529 | "cellId": "zwu15gfrdt85ue8ks5pvbb", 530 | "execution_id": "366eda08-633a-4941-9010-7aaa43ce32c4" 531 | }, 532 | "source": [ 533 | "Теперь сохраним результаты распознавания в текстовые файлы:" 534 | ] 535 | }, 536 | { 537 | "cell_type": "code", 538 | "execution_count": 15, 539 | "id": "ce6cd56b", 540 | "metadata": { 541 | "cellId": "1vivcyr9v40i1sdy5kwv7gg" 542 | }, 543 | "outputs": [], 544 | "source": [ 545 | "os.makedirs(f'/home/jupyter/datasphere/s3/s3store/{user}/text',exist_ok=True)\n", 546 | "for k,v in txt.items():\n", 547 | " with open(k.replace('.ogg','.txt').replace('/audio/','/text/'),'w',encoding='utf-8') as f:\n", 548 | " f.write(v)" 549 | ] 550 | }, 551 | { 552 | "cell_type": "markdown", 553 | "id": "27dccebc", 554 | "metadata": { 555 | "cellId": "cqei03ljo7zc0vv13fbds", 556 | "execution_id": "febd1b8a-0919-4e3e-a31e-348977544146" 557 | }, 558 | "source": [ 559 | "Мы получили коллекцию текстовых документов, в которых теперь можно организовать полнотекстовый поиск и построить вопрос-ответного бота. Переходим к следующему этапу работы: `LangChainQA.ipynb`" 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "id": "cc5dc871", 566 | "metadata": { 567 | "cellId": "hhkmxblkxogohulu70w2t" 568 | }, 569 | "outputs": [], 570 | "source": [] 571 | } 572 | ], 573 | "metadata": { 574 | "kernelspec": { 575 | "display_name": "DataSphere Kernel", 576 | "language": "python", 577 | "name": "python3" 578 | }, 579 | "language_info": { 580 | "codemirror_mode": { 581 | "name": "ipython", 582 | "version": 3 583 | }, 584 | "file_extension": ".py", 585 | "mimetype": "text/x-python", 586 | "name": "python", 587 | "nbconvert_exporter": "python", 588 | "pygments_lexer": "ipython3", 589 | "version": "3.7.7" 590 | }, 591 | "notebookId": "09b946fe-cff1-4470-8236-07d13a6babc0", 592 | "notebookPath": "VideoQABot/PrepareDataset.ipynb" 593 | }, 594 | "nbformat": 4, 595 | "nbformat_minor": 5 596 | } 597 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Создаём вопрос-ответного чат-бота по видео 2 | 3 | В последнее время, с появлением больших языковых разговорных моделей (*Large Language Models, LLM*), таких, как [Yandex GPT](https://cloud.yandex.ru/services/yandexgpt), актуальным становится вопрос создания предметно-ориентированных чат-ботов, т.е. таких разговорных чат-ботов, которые способны поддерживать беседу в рамках какой-то узкой предметной области. Такие чат-боты могут быть реализованы двумя путями: 4 | 5 | * **До-обучение разговорной модели** - это, как правило, требует значительных вычислительных мощностей, усилий и опыта, и при этом любые изменения в предметной области требуют пере-обучения модели 6 | * **Retrieval-Augmented Generation** - подход, при котором ответ чат-бота формируется стандартной предобученной LLM-моделью, но предварительно ей показывают фрагменты текста из предметно-ориентированной базы знаний, найденные с помощью семантического поиска. 7 | 8 | В рамках данного мастер-класса мы создадим вопрос-ответного чат-бота с помощью подхода Retrieval-Augmented Generation, на основе набора видео-файлов. Мы используем [Yandex SpeechKit](https://cloud.yandex.ru/services/speechkit) для преобразования звуковой дорожки видео в текстовый корпус, после чего организуем векторное хранилище и индексацию с помощью [текстовых эмбеддингов Yandex GPT](https://cloud.yandex.ru/docs/yandexgpt/api-ref/Embeddings/) и фреймворка [LangChain](https://www.langchain.com/). В заключении, мы реализуем телеграм-бота, который способен отвечать на текстовые и голосовые запросы. 9 | 10 | Подробные комментарии содержатся в файлах проекта, которые рекомендуется открыть в [Yandex DataSphere](https://cloud.yandex.ru/services/datasphere). 11 | 12 | ![Скриншот работающего бота](images/scrshot.png) 13 | 14 | ## Этапы работы 15 | 16 | Вся работа состоит из следующих основных этапов: 17 | 18 | 1. [PrepareDataset.ipynb](PrepareDataset.ipynb) - подготовка текстов для поиска из семейства видео. На вход мы получаем набор ссылок на видео на YouTube, скачиваются звуковые дорожки, преобразуются в нужный формат и далее вызывается Yandex SpeechKit для преобразования речи в текст. 19 | 1. [LangChainQA.ipynb](LangChainQA.ipynb) - основной код для вопрос-ответного бота на основе LangChain. Здесь текстовые материалы разбиваются на фрагменты, индексируются с помощью эмбеддингов и сохраняются в векторную базу данных. Затем по запросу из базы данных извлекаются релевантные документы, и подаются на вход модели Yandex GPT. В этом же ноутбуке мы разрабатываем простые адаптеры LangChain для Yandex GPT Embeddings и Yandex GPT LLM. 20 | 1. [telegram.py](telegram.py) - код телеграм-бота на основе фреймворка flask. Этот скрипт размещается на виртуальной машине. 21 | 22 | Чтобы начать работу, вам необходимо проделать следующие подготовительные операции: 23 | 24 | 1. Получить доступ к Яндекс-облаку - например, в рамках [пробного периода](https://cloud.yandex.ru/docs/free-trial/) 25 | 1. Создать в облаке объектное хранилище s3 (в рамках мастер-класса используется имя `s3store`) 26 | 1. Создать в облаке [сервисный аккаунт](https://cloud.yandex.ru/docs/iam/concepts/users/service-accounts), имеющий доступ к SpeechKit, YandexGPT и объектному хранилищу, а затем создать [API-ключ](https://cloud.yandex.ru/docs/iam/concepts/authorization/api-key) ([инструкция](https://cloud.yandex.ru/docs/iam/operations/api-key/create)), и параметры этого ключа прописать в файле [config.json](config.json). Также потребуется [создать статический ключ доступа](https://cloud.yandex.ru/docs/iam/operations/sa/create-access-key). 27 | 28 | ## Пошаговая инструкция 29 | 30 | Данный мастер-класс рекомендуется проводить в [Yandex DataSphere](https://cloud.yandex.ru/services/datasphere). 31 | 32 | ### Подготовка окружения 33 | 1. Необходимо создать сообщество в [Yandex DataSphere](https://cloud.yandex.ru/services/datasphere) и проект в этом сообществе. 34 | > Если вы проходите мастер-класс в рамках мероприятия (например, на [Practical ML Conference](https://pmlconf.yandex.ru/)), то вам может быть предоставлен доступ к уже сконфигурированному проекту в DataSphere. 35 | 1. В рамках проекта подключить объектное хранилище `s3store` с помощью созданного статического ключа доступа. 36 | > В рамках мероприятия коннектор к `s3store` может уже быть настроен в рамках сообщества, вам необходимо лишь добавить его в проект. 37 | 1. Добавить в проект или создать в проекте секрет `api_key`, содержащий созданный ранее API-ключ к сервисному аккаунту. По этому ключу мы будем вызывать сервисы Speech Kit и Yandex GPT. 38 | 1. Изменить активный Docker-образ в проекте на Python 3.10 39 | 1. Открыть проект в Jupyter Lab. Рекомендуем использовать режим **Dedicated**. Для режима Serverless, возможно, придётся немного модифицировать пути к хранилищу s3. 40 | 1. В разделе GitHub клонировать репозиторий с материалами [http://github.com/yandex-datasphere/VideoQABot](http://github.com/yandex-datasphere/VideoQABot) 41 | 42 | ### Извлечение текста из видео 43 | 1. Откройте в проекте ноутбук [PrepareDataset.ipynb](PrepareDataset.ipynb) и выполните все ячейки кода, обращая внимание на сам код и не инструкции. 44 | 1. В качестве источника данных вам потребуются ссылки на несколько видео на YouTube. 45 | 1. Вначале аудиодорожки к выбранным роликам скачиваются с помощью библиотеки `pytube` и помещаются в директорию `audio` проекта 46 | 1. Поскольку Speech Kit требует определённый формат аудио и частоту дискретизации, с помощью библиотеки `librosa` происходит преобразование аудио к требуемому формату 47 | 1. Для преобразования длинного аудио в текст используем [асинхронное распознавание речи](https://cloud.yandex.ru/docs/speechkit/stt/transcribation). В этом случае мы сначала размещаем данные в хранилище s3, и затем запускаем процесс распознавания с помощью REST-запросов. Далее мы в цикле проверяем готовность результатов, и сохраняем их в хранилище. 48 | 49 | ### Retrieval-Augmented Generation 50 | 1. Откройте в проекте ноутбук [LangChainQA.ipynb](LangChainQA.ipynb) и выполните все ячейки кода, обращая внимание на сам код и не инструкции. 51 | 1. Для начала документ разбивается на небольшие фрагменты размером `chunk_size`. Размер `chunk_size` нужно выбирать исходя из нескольких показателей: 52 | * Допустимая длина контекста для эмбеддинг-модели. Yandex GPT Embeddings допускают 2048 токенов, в то время как многие открытые модели HuggingFace имеют длину контекста 512-1024 токена 53 | * Допустимый размер окна контекста большой языковой модели. Если мы хотим использовать в запросе top 3 результатов поиска, то 3*chunk_size+prompt_size+response_size должно не превышать длины контекста модели. 54 | 1. Далее мы учимся считать по фрагментам текста векторные эмбеддинги, с помощью моделей от HuggingFace, или через Yandex GPT Embedding API. В последнем случае нам пришлось написать адаптер для LangChain для работы с Yandex GPT Embeddings. 55 | 1. Создаём векторную базу данных, обрабатываем все фрагменты и сохраняем их 56 | 1. Учимся извлекать релевантные фрагменты по запросу 57 | 1. Пишем адаптер для LangChain для работы с моделью Yandex GPT. Убеждаемся, что Yandex GPT работает, но не очень хорошо отвечает на предметно-ориентированные запросы. 58 | 1. Собираем цепочку для Retrieval-Augmented Generation и проверяем её работу 59 | 60 | ### Создаём вопрос-ответного бота в телеграм 61 | Для создания вопрос-ответного бота нам потребуется развернуть нашу цепочку LangChain в виде публично-доступного веб-сервиса по HTTPS. Это удобнее всего сделать с помощью виртуальной машины Yandex Compute. Для понимания того, как устроены боты в телеграм, можно порекомендовать [эту документацию](https://core.telegram.org/bots/tutorial). 62 | 63 | 1. Создаём виртуальную машину. Для экспериментов нам не нужна высокая производительность, будет достаточно 4-6 Gb RAM, 50 Gb SSD, Ubuntu. Для входа на виртуальную машину используется ssh-сертификат. 64 | > Код телеграм-бота подразумевает, что пользователь на виртуальной машине будем иметь имя `vmuser`. Если вы используете другое имя, то придётся внести исправления в код. 65 | 1. Создаём для виртуальной машины статический IP-адрес 66 | 1. Для работы с телеграм потребуется HTTPS-протокол и сертификат SSL. Поэтому необходимо привязать к виртуальной машине какое-то доменное имя. 67 | 1. Заходим в консоль виртуальной машины по SSH 68 | 1. Клонируем репозиторий проекта `git clone https://github.com/yandex-datasphere/VideoQABot` 69 | 1. Переходим в каталог проекта и устанавливаем зависимости: 70 | ``` 71 | cd VideoQABot 72 | pip3 install -r requirements.txt 73 | ``` 74 | 1. Создаём SSL-сертификат для выбранного ранее доменного имени, это можно сделать, например, с помощью бесплатного сервиса *Let's Encrypt* и `certbot` 75 | 1. Сертификаты записываем в директорию `cert`, и прописываем путь к ним в файле [`telegram.py`](telegram.py) 76 | 1. Создаём бота в телеграм при помощи `botfather` (см. [док](https://core.telegram.org/bots/tutorial#getting-ready)), и полученный telegram token записываем в [`config.json`](config.json) 77 | 1. Также в [`config.json`](config.json) прописываем адрес нашего сайта. Рекомендуется использовать порт 8443, поскольку в этом случае запускать веб-сервер можно от имени обычного пользователя. 78 | 1. Копируем векторную базу данных, полученную на предыдущем шаге, в директорию `store`. 79 | 1. Запускаем `python3 telegram.py` 80 | 81 | На этом этапе вы должны быть в состоянии послать в бота сообщения, текстом или как голосовое сообщение, и получить ответ, текстом + голосом. 82 | -------------------------------------------------------------------------------- /YaGPT.py: -------------------------------------------------------------------------------- 1 | from langchain.embeddings.base import Embeddings 2 | import time 3 | import requests 4 | from typing import Any, List, Mapping, Optional 5 | from langchain.callbacks.manager import CallbackManagerForLLMRun 6 | import requests 7 | import langchain 8 | 9 | class YaGPTEmbeddings(Embeddings): 10 | 11 | def __init__(self,folder_id,api_key,sleep_interval=1): 12 | self.folder_id = folder_id 13 | self.api_key = api_key 14 | self.sleep_interval = sleep_interval 15 | self.headers = { 16 | "Authorization" : f"Api-key {api_key}", 17 | "x-folder-id" : folder_id } 18 | 19 | def embed_document(self, text): 20 | j = { 21 | "model" : "general:embedding", 22 | "embedding_type" : "EMBEDDING_TYPE_DOCUMENT", 23 | "text": text 24 | } 25 | res = requests.post("https://llm.api.cloud.yandex.net/llm/v1alpha/embedding", 26 | json=j,headers=self.headers) 27 | vec = res.json()['embedding'] 28 | return vec 29 | 30 | def embed_documents(self, texts, chunk_size = 0): 31 | res = [] 32 | for x in texts: 33 | res.append(self.embed_document(x)) 34 | time.sleep(self.sleep_interval) 35 | return res 36 | 37 | def embed_query(self, text): 38 | j = { 39 | "model" : "general:embedding", 40 | "embedding_type" : "EMBEDDING_TYPE_QUERY", 41 | "text": text 42 | } 43 | res = requests.post("https://llm.api.cloud.yandex.net/llm/v1alpha/embedding", 44 | json=j,headers=self.headers) 45 | vec = res.json()['embedding'] 46 | time.sleep(self.sleep_interval) 47 | return vec 48 | 49 | 50 | 51 | class YandexLLM(langchain.llms.base.LLM): 52 | api_key: str = None 53 | iam_token: str = None 54 | folder_id: str 55 | max_tokens : int = 1500 56 | temperature : float = 1 57 | instruction_text : str = None 58 | 59 | @property 60 | def _llm_type(self) -> str: 61 | return "yagpt" 62 | 63 | def _call( 64 | self, 65 | prompt: str, 66 | stop: Optional[List[str]] = None, 67 | run_manager: Optional[CallbackManagerForLLMRun] = None, 68 | ) -> str: 69 | if stop is not None: 70 | raise ValueError("stop kwargs are not permitted.") 71 | headers = { "x-folder-id" : self.folder_id } 72 | if self.iam_token: 73 | headers["Authorization"] = f"Bearer {self.iam_token}" 74 | if self.api_key: 75 | headers["Authorization"] = f"Api-key {self.api_key}" 76 | req = { 77 | "model": "general", 78 | "instruction_text": self.instruction_text, 79 | "request_text": prompt, 80 | "generation_options": { 81 | "max_tokens": self.max_tokens, 82 | "temperature": self.temperature 83 | } 84 | } 85 | res = requests.post("https://llm.api.cloud.yandex.net/llm/v1alpha/instruct", 86 | headers=headers, json=req).json() 87 | return res['result']['alternatives'][0]['text'] 88 | 89 | @property 90 | def _identifying_params(self) -> Mapping[str, Any]: 91 | """Get the identifying parameters.""" 92 | return {"max_tokens": self.max_tokens, "temperature" : self.temperature } 93 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "folder_id" : "b1g6krtrd2vcbunvjpg6", 3 | "api_id" : "ajeo4jge3h1hgvok9eai", 4 | "api_key" : "AQVN2CTg4cQxh_FEuVKtgjgvxM38dYutiqtecH0M", 5 | "telegram_token" : "6599430677:AAHQcCsvOJJ17Bg6ym_dTEzad62ay_03qVc", 6 | "self_url": "https://ya.eazify.net:8443" 7 | } -------------------------------------------------------------------------------- /images/scrshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex-datasphere/VideoQABot/349be3e46c12fb45bb6327c57e9a758f0fa7683a/images/scrshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs 2 | flask 3 | langchain 4 | sentence_transformers 5 | lancedb 6 | unstructured 7 | requests 8 | pandas 9 | yandex-speechkit -------------------------------------------------------------------------------- /telegram.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, make_response, abort, request, send_file 2 | import requests 3 | import json 4 | from langchain.vectorstores import LanceDB 5 | import lancedb 6 | import langchain 7 | from YaGPT import YaGPTEmbeddings, YandexLLM 8 | import uuid 9 | from speechkit import model_repository, configure_credentials, creds 10 | from speechkit.stt import AudioProcessingType 11 | 12 | cert = "/home/vmuser/cert/fullchain.pem" 13 | cert_key = "/home/vmuser/cert/privkey.pem" 14 | temp_dir = "/home/vmuser/temp" 15 | db_dir = "/home/vmuser/store" 16 | send_audio = True 17 | 18 | app = Flask(__name__) 19 | 20 | def synth(txt): 21 | model = model_repository.synthesis_model() 22 | model.voice = 'jane' 23 | model.role = 'good' 24 | result = model.synthesize(txt,raw_format=False) 25 | fn = f"/home/vmuser/temp/{uuid.uuid4().urn}.mp3" 26 | result.export(fn, 'mp3') 27 | return fn 28 | 29 | def reco(bin): 30 | model = model_repository.recognition_model() 31 | model.model = 'general' 32 | model.language = 'ru-RU' 33 | model.audio_processing_type = AudioProcessingType.Full 34 | result = model.transcribe_file(bin) 35 | return ' '.join(x.normalized_text for x in result) 36 | 37 | def tg_send(chat_id, text): 38 | url = f"https://api.telegram.org/bot{telegram_token}/sendMessage" 39 | data = {"chat_id": chat_id, "text": text} 40 | requests.post(url, data=data) 41 | 42 | def tg_send_audio(chat_id, text, file): 43 | url = f"https://api.telegram.org/bot{telegram_token}/sendAudio" 44 | data = { "chat_id": chat_id, "caption": text } 45 | files = { "audio" : open(file,'rb') } 46 | requests.post(url, data=data, files=files) 47 | 48 | def do_search(chat_id,txt): 49 | print(f"Doing search on {txt}") 50 | res = retriever.get_relevant_documents(txt) 51 | res = chain.run(input_documents=res,query=txt) 52 | if send_audio: 53 | fn = synth(res) 54 | tg_send_audio(chat_id,res,fn) 55 | else: 56 | tg_send(chat_id,res) 57 | 58 | def process(post): 59 | print(post) 60 | msg = post['message'] 61 | chat_id = msg['chat']['id'] 62 | txt = None 63 | if 'text' in msg: 64 | do_search(chat_id,msg['text']) 65 | if 'voice' in msg: 66 | url = f"https://api.telegram.org/bot{telegram_token}/getFile" 67 | data = { "file_id": msg['voice']['file_id'] } 68 | resp = requests.post(url, data=data).json() 69 | url = f"https://api.telegram.org/file/bot{telegram_token}/{resp['result']['file_path']}" 70 | fn = f"/home/vmuser/temp/{uuid.uuid4().urn}.mp3" 71 | bin = requests.get(url).content 72 | with open(fn,'wb') as f: 73 | f.write(bin) 74 | res = reco(fn) 75 | tg_send(chat_id,f'Вы спросили: {res}') 76 | do_search(chat_id,res) 77 | 78 | 79 | @app.route('/',methods=['GET']) 80 | def home(): 81 | return "

Hello

" 82 | 83 | @app.route('/tghook',methods=['GET','POST']) 84 | def telegram_hook(): 85 | if request.method=='POST': 86 | post = request.json 87 | process(post) 88 | return { "ok" : True } 89 | 90 | print(" + Reading config") 91 | with open('config.json') as f: 92 | config = json.load(f) 93 | self_url = config['self_url'] 94 | api_key = config['api_key'] 95 | telegram_token = config['telegram_token'] 96 | folder_id = config['folder_id'] 97 | 98 | print(" + Initializing LanceDB Vector Store") 99 | embedding = YaGPTEmbeddings(folder_id,api_key) 100 | lance_db = lancedb.connect(db_dir) 101 | table = lance_db.open_table("vector_index") 102 | vec_store = LanceDB(table, embedding) 103 | retriever = vec_store.as_retriever( 104 | search_kwargs={"k": 5} 105 | ) 106 | 107 | print(" + Initializing LLM Chains") 108 | instructions = """ 109 | Представь себе, что ты сотрудник Yandex Cloud. Твоя задача - вежливо и по мере своих сил отвечать на все вопросы собеседника. 110 | """ 111 | llm = YandexLLM(api_key=api_key, folder_id=folder_id, 112 | instruction_text = instructions) 113 | document_prompt = langchain.prompts.PromptTemplate( 114 | input_variables=["page_content"], template="{page_content}" 115 | ) 116 | # Промпт для языковой модели 117 | document_variable_name = "context" 118 | stuff_prompt_override = """ 119 | Пожалуйста, посмотри на текст ниже и ответь на вопрос, используя информацию из этого текста. 120 | Текст: 121 | ----- 122 | {context} 123 | ----- 124 | Вопрос: 125 | {query}""" 126 | prompt = langchain.prompts.PromptTemplate( 127 | template=stuff_prompt_override, input_variables=["context", "query"] 128 | ) 129 | # Создаём цепочку 130 | llm_chain = langchain.chains.LLMChain(llm=llm, prompt=prompt) 131 | chain = langchain.chains.StuffDocumentsChain( 132 | llm_chain=llm_chain, 133 | document_prompt=document_prompt, 134 | document_variable_name=document_variable_name, 135 | ) 136 | 137 | print(" + Configuring speech") 138 | configure_credentials(yandex_credentials=creds.YandexCredentials(api_key=api_key)) 139 | 140 | #print(" + Registering telegram hook") 141 | #res = requests.post(f"https://api.telegram.org/bot{telegram_token}/setWebhook",json={ "url" : f"{self_url}/tghook" }) 142 | #print(res.json()) 143 | 144 | app.run(host="0.0.0.0",port=8443,ssl_context=(cert,cert_key)) --------------------------------------------------------------------------------