Vladislav.Novikov commited on
Commit
e5e9860
·
1 Parent(s): 6e6bba4

first release

Browse files
.gitignore ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ ### VisualStudioCode ###
177
+ .vscode/*
178
+ !.vscode/settings.json
179
+ !.vscode/tasks.json
180
+ !.vscode/launch.json
181
+ !.vscode/extensions.json
182
+ !.vscode/*.code-snippets
183
+
184
+ # Local History for Visual Studio Code
185
+ .history/
186
+
187
+ # Built Visual Studio Code Extensions
188
+ *.vsix
189
+
190
+ ### VisualStudioCode Patch ###
191
+ # Ignore all local history of files
192
+ .history
193
+ .ionide
194
+
195
+ # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "python-learning-bot"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["Vladislav.Novikov <mb1te.comcis@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "python_learning_bot"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.11"
11
+ llama-cpp-python = "^0.1.63"
12
+ pydantic = {extras = ["dotenv"], version = "^1.10.9"}
13
+ aiogram = {url = "https://github.com/aiogram/aiogram/archive/refs/tags/v3.0.0b7.zip"}
14
+ redis = "^4.5.5"
15
+ beautifulsoup4 = "^4.12.2"
16
+ lxml = "^4.9.2"
17
+ requests = "^2.31.0"
18
+ sqlalchemy = "^2.0.16"
19
+
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ mypy = "^1.3.0"
23
+ pre-commit = "^3.3.3"
24
+ ruff = "^0.0.272"
25
+ isort = "^5.12.0"
26
+
27
+ [build-system]
28
+ requires = ["poetry-core"]
29
+ build-backend = "poetry.core.masonry.api"
python-learning-bot.db ADDED
Binary file (61.4 kB). View file
 
src/bot.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+
5
+ from aiogram import Bot, Dispatcher
6
+ from config import settings
7
+ from router import router
8
+ from storage import SQLiteStorage
9
+
10
+
11
+ async def main():
12
+ bot = Bot(token=settings.TELEGRAM_TOKEN)
13
+ storage = SQLiteStorage()
14
+ dp = Dispatcher(storage=storage)
15
+ dp.include_router(router)
16
+
17
+ await dp.start_polling(bot)
18
+
19
+
20
+ if __name__ == '__main__':
21
+ logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
22
+ asyncio.run(main())
src/config.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ TELEGRAM_TOKEN: str
6
+ MODEL_PATH: str
7
+ DB_PATH: str
8
+
9
+ class Config:
10
+ env_file = ".env"
11
+
12
+
13
+ settings = Settings()
src/db/__init__.py ADDED
File without changes
src/db/repositories/__init__.py ADDED
File without changes
src/db/repositories/question.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from sqlalchemy import select, update
3
+ from sqlalchemy.orm.session import Session
4
+
5
+ from db.tables.question import QuestionModel
6
+
7
+
8
+ class QuestionRepository:
9
+ def __init__(self, session: Session):
10
+ self.session = session
11
+
12
+ def get(self, **kwargs) -> Optional[QuestionModel]:
13
+ query = select(QuestionModel).filter_by(**kwargs)
14
+ return self.session.execute(query).unique().scalar_one_or_none()
15
+
16
+ def get_all(self, **kwargs) -> list[QuestionModel]:
17
+ query = select(QuestionModel).filter_by(**kwargs)
18
+ return self.session.execute(query).unique().scalars()
19
+
20
+ def update(self, question_id: int, **kwargs):
21
+ query = update(QuestionModel).filter_by(id=question_id).values(**kwargs)
22
+ self.session.execute(query)
src/db/repositories/user.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from sqlalchemy import select, update
3
+ from sqlalchemy.orm.session import Session
4
+
5
+ from db.tables.user import UserModel
6
+
7
+
8
+ class UserRepository:
9
+ def __init__(self, session: Session):
10
+ self.session = session
11
+
12
+ def get(self, **kwargs) -> Optional[UserModel]:
13
+ query = select(UserModel).filter_by(**kwargs)
14
+ return self.session.execute(query).scalar_one_or_none()
15
+
16
+ def create(self, **kwargs) -> UserModel:
17
+ obj = UserModel(**kwargs)
18
+ self.session.add(obj)
19
+ return obj
20
+
21
+ def update(self, user_id: int, **kwargs) -> UserModel:
22
+ query = update(UserModel).filter_by(user_id=user_id).values(**kwargs)
23
+ self.session.execute(query)
src/db/session.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker
3
+
4
+ from config import settings
5
+
6
+ engine = create_engine(settings.DB_PATH)
7
+ create_session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
src/db/tables/__init__.py ADDED
File without changes
src/db/tables/answer.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+ from sqlalchemy import ForeignKey
3
+ from sqlalchemy.orm import Mapped, mapped_column
4
+
5
+ from db.tables.base import BaseModel
6
+
7
+
8
+ class AnswerModel(BaseModel):
9
+ __tablename__ = "answer"
10
+
11
+ id: Mapped[int] = mapped_column(primary_key=True)
12
+ answer_text: Mapped[str] = mapped_column(nullable=False)
13
+ question_id: Mapped[int] = mapped_column(ForeignKey("question.id"), nullable=False)
14
+ is_correct: Mapped[int] = mapped_column(nullable=False)
src/db/tables/base.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ BaseModel = declarative_base()
src/db/tables/question.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
3
+
4
+ from db.tables.base import BaseModel
5
+
6
+ from db.tables.answer import AnswerModel
7
+
8
+
9
+ class QuestionModel(BaseModel):
10
+ __tablename__ = "question"
11
+
12
+ id: Mapped[int] = mapped_column(primary_key=True)
13
+ rating: Mapped[int] = mapped_column(nullable=False)
14
+ question: Mapped[str] = mapped_column(nullable=False)
15
+ explanation: Mapped[str] = mapped_column(nullable=False)
16
+
17
+ answers: Mapped[list[AnswerModel]] = relationship(lazy="joined")
src/db/tables/user.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING, Optional
2
+ from sqlalchemy import ForeignKey
3
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
4
+
5
+ from db.tables.base import BaseModel
6
+
7
+ if TYPE_CHECKING:
8
+ from db.tables.question import QuestionModel
9
+
10
+ class UserModel(BaseModel):
11
+ __tablename__ = "user"
12
+
13
+ user_id: Mapped[int] = mapped_column(primary_key=True)
14
+ rating: Mapped[int] = mapped_column(nullable=False)
15
+ state: Mapped[str] = mapped_column(nullable=False)
16
+ last_question_id: Mapped[Optional[int]] = mapped_column(ForeignKey("question.id"))
17
+
18
+ # last_question: Mapped[Optional["QuestionModel"]] = relationship(lazy="joined")
src/dependencies.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import contextmanager
2
+ import logging
3
+ from db.repositories.user import UserRepository
4
+ from db.repositories.question import QuestionRepository
5
+
6
+ from db.session import create_session
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @contextmanager
13
+ def get_db_session():
14
+ try:
15
+ session = create_session()
16
+ yield session
17
+ except Exception as e:
18
+ logger.exception(e)
19
+ finally:
20
+ session.close()
21
+
22
+
23
+ @contextmanager
24
+ def get_user_repository():
25
+ try:
26
+ with get_db_session() as session:
27
+ yield UserRepository(session=session)
28
+ except Exception as e:
29
+ logger.exception(e)
30
+
31
+
32
+ @contextmanager
33
+ def get_question_repository():
34
+ try:
35
+ with get_db_session() as session:
36
+ yield QuestionRepository(session=session)
37
+ except Exception as e:
38
+ logger.exception(e)
src/find_rating.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from saiga.llama_cpu import LlamaCpu
2
+
3
+ model = LlamaCpu()
4
+ prompt = (
5
+ "I have python question. Detect complexity of it as Elo rating. "
6
+ "Return only one number.\n"
7
+ "Question: 1. Who developed Python Programming Language?\n"
8
+ "a) Wick van Rossum\n"
9
+ "b) Rasmus Lerdorf\n"
10
+ "c) Guido van Rossum\n"
11
+ "d) Niene Stom\n"
12
+ "Answer: c) Guido van Rossum"
13
+ )
14
+ print(model.answer(prompt))
src/keyboard.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.types import KeyboardButton
2
+
3
+ START_MENU_BUTTON = "Главное меню"
4
+ QUIZ_BUTTON = "Ответить на вопрос"
5
+ QUESTION_BUTTON = "Задать вопрос"
6
+
7
+ BOT_KEYBOARD = [
8
+ [
9
+ KeyboardButton(text=START_MENU_BUTTON),
10
+ KeyboardButton(text=QUIZ_BUTTON),
11
+ KeyboardButton(text=QUESTION_BUTTON)
12
+ ]
13
+ ]
src/router.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import F, Router
2
+ from aiogram.filters import Command
3
+ from aiogram.fsm.context import FSMContext
4
+ from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup
5
+ from keyboard import BOT_KEYBOARD, QUESTION_BUTTON, QUIZ_BUTTON
6
+ from dependencies import get_question_repository, get_user_repository
7
+
8
+ import random
9
+ from saiga.llama_cpu import LlamaCpu
10
+
11
+ from state import UserState
12
+ from utils import escape_all, update_elo
13
+
14
+ router = Router()
15
+ llm = LlamaCpu()
16
+
17
+
18
+ @router.message(Command("start"))
19
+ @router.message(UserState.start_menu)
20
+ async def command_start(message: Message, state: FSMContext):
21
+ await state.set_state(UserState.await_tap)
22
+ await message.answer(
23
+ "Привет! Для использования моих функций используй следущие команды:",
24
+ reply_markup=ReplyKeyboardMarkup(keyboard=BOT_KEYBOARD, resize_keyboard=True)
25
+ )
26
+
27
+
28
+ @router.message(UserState.await_tap, F.text == QUESTION_BUTTON)
29
+ async def get_user_question(message: Message, state: FSMContext):
30
+ await state.set_state(UserState.question)
31
+ await message.answer("Задайте ваш вопрос")
32
+
33
+
34
+ @router.message(UserState.question)
35
+ async def answer_user_question(message: Message, state: FSMContext):
36
+ await state.set_state(UserState.await_tap)
37
+ answer = llm.answer(message.text)
38
+ return await message.answer(
39
+ answer,
40
+ reply_markup=ReplyKeyboardMarkup(keyboard=BOT_KEYBOARD, resize_keyboard=True)
41
+ )
42
+
43
+
44
+ @router.message(UserState.await_tap, F.text == QUIZ_BUTTON)
45
+ async def send_quiz(message: Message, state: FSMContext):
46
+ with get_user_repository() as user_repo, get_question_repository() as question_repo:
47
+ user = user_repo.get(user_id=state.key.user_id)
48
+ questions = question_repo.get_all()
49
+ questions = sorted(questions, key=lambda question: abs(question.rating - user.rating))
50
+ min_abs = abs(questions[0].rating - user.rating)
51
+ questions = [q for q in questions if abs(q.rating - user.rating) == min_abs]
52
+ selected_question = random.choice(questions)
53
+ user_repo.update(user.user_id, last_question_id=selected_question.id)
54
+ user_repo.session.commit()
55
+
56
+ await state.set_state(UserState.quiz)
57
+ return await message.answer(
58
+ selected_question.question,
59
+ # parse_mode="MarkdownV2",
60
+ reply_markup=ReplyKeyboardMarkup(
61
+ keyboard=[
62
+ [KeyboardButton(text=answer.answer_text)]
63
+ for answer in selected_question.answers
64
+ ],
65
+ resize_keyboard=True
66
+ )
67
+ )
68
+
69
+
70
+ @router.message(UserState.quiz)
71
+ async def check_answer(message: Message, state: FSMContext):
72
+ with get_user_repository() as user_repo, get_question_repository() as question_repo:
73
+ user = user_repo.get(user_id=state.key.user_id)
74
+ question = question_repo.get(id=user.last_question_id)
75
+ correct_answers = [
76
+ answer.answer_text.strip()
77
+ for answer in question.answers
78
+ if answer.is_correct == 1
79
+ ]
80
+
81
+ is_correct = int(message.text.strip() in correct_answers)
82
+ old_user_rating, old_question_rating = user.rating, question.rating
83
+ new_user_rating, new_question_rating = update_elo(
84
+ rating_user=old_user_rating,
85
+ rating_task=old_question_rating,
86
+ is_correct=is_correct
87
+ )
88
+
89
+ user_repo.update(user.user_id, rating=new_user_rating)
90
+ user_repo.session.commit()
91
+ question_repo.update(question.id, rating=new_question_rating)
92
+ question_repo.session.commit()
93
+
94
+ await state.set_state(UserState.await_tap)
95
+ answer = (
96
+ f"{question.explanation}\n"
97
+ f"{is_correct=}\n"
98
+ f"answer={message.text.strip()}\n"
99
+ f"{correct_answers=}\n"
100
+ f"User rating: {old_user_rating} => {new_user_rating}\n"
101
+ f"Question rating: {old_question_rating} => {new_question_rating}"
102
+ )
103
+
104
+ return await message.answer(
105
+ answer,
106
+ # parse_mode="MarkdownV2",
107
+ reply_markup=ReplyKeyboardMarkup(keyboard=BOT_KEYBOARD, resize_keyboard=True)
108
+ )
src/saiga/__init__.py ADDED
File without changes
src/saiga/bin/saiga_7b_lora_q41.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1fbf7c4d1c6986082325fcdb54eb446bb3e99b20bfaf9a5ca87ca9967a4db31d
3
+ size 4212859520
src/saiga/llama_cpu.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError
3
+
4
+ from llama_cpp import Llama
5
+
6
+ from config import settings
7
+
8
+
9
+ class LlamaCpu:
10
+ def __init__(
11
+ self, n_ctx=2000, top_k=30, top_p=0.9, temperature=0.2, repeat_penalty=1.1
12
+ ):
13
+ self.SYSTEM_PROMPT = (
14
+ "Твоя задача отвечать на вопросы, связанные с языком "
15
+ "программирования Python."
16
+ )
17
+ self.SYSTEM_TOKEN = 1788
18
+ self.USER_TOKEN = 1404
19
+ self.BOT_TOKEN = 9225
20
+ self.LINEBREAK_TOKEN = 13
21
+
22
+ self.ROLE_TOKENS = {
23
+ "user": self.USER_TOKEN,
24
+ "bot": self.BOT_TOKEN,
25
+ "system": self.SYSTEM_TOKEN,
26
+ }
27
+
28
+ self.top_k = top_k
29
+ self.top_p = top_p
30
+ self.temperature = temperature
31
+ self.repeat_penalty = repeat_penalty
32
+
33
+ self.model = Llama(
34
+ model_path=settings.MODEL_PATH,
35
+ n_ctx=n_ctx,
36
+ n_parts=1,
37
+ )
38
+
39
+ self.system_tokens = self.get_system_tokens()
40
+ self.model.eval(self.system_tokens)
41
+
42
+ def get_message_tokens(self, role, content):
43
+ message_tokens = self.model.tokenize(content.encode("utf-8"))
44
+ message_tokens.insert(1, self.ROLE_TOKENS[role])
45
+ message_tokens.insert(2, self.LINEBREAK_TOKEN)
46
+ message_tokens.append(self.model.token_eos())
47
+ return message_tokens
48
+
49
+ def get_system_tokens(self):
50
+ return self.get_message_tokens(
51
+ role="system",
52
+ content=self.SYSTEM_PROMPT,
53
+ )
54
+
55
+ def answer(self, question: str) -> str:
56
+ message_tokens = self.get_message_tokens(role="user", content=question)
57
+ role_tokens = [self.model.token_bos(), self.BOT_TOKEN, self.LINEBREAK_TOKEN]
58
+ cur_tokens = self.system_tokens + message_tokens + role_tokens
59
+ generator = self.model.generate(
60
+ cur_tokens,
61
+ top_k=self.top_k,
62
+ top_p=self.top_p,
63
+ temp=self.temperature,
64
+ repeat_penalty=self.repeat_penalty,
65
+ )
66
+
67
+ answer = ""
68
+
69
+ for token in generator:
70
+ token_str = self.model.detokenize([token]).decode("utf-8")
71
+ if token == self.model.token_eos():
72
+ break
73
+
74
+ answer += token_str
75
+
76
+ return answer
77
+
78
+ async def async_answer(self, question: str) -> str:
79
+ loop = asyncio.get_event_loop()
80
+ with ThreadPoolExecutor() as executor:
81
+ future = loop.run_in_executor(executor, self.answer, question)
82
+ try:
83
+ return await asyncio.wait_for(future, timeout=settings.MODEL_TIMEOUT)
84
+ except TimeoutError:
85
+ return "Простите, мне удалось сочинить ответ за 60 секунд :("
src/state.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from aiogram.fsm.state import State, StatesGroup
2
+
3
+
4
+ class UserState(StatesGroup):
5
+ start_menu = State()
6
+ await_tap = State()
7
+ quiz = State()
8
+ question = State()
src/storage.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, Optional
2
+ from aiogram import Bot
3
+ from aiogram.fsm.storage.base import BaseStorage, StateType, StorageKey
4
+
5
+ from dependencies import get_user_repository
6
+ from state import UserState
7
+
8
+
9
+ class SQLiteStorage(BaseStorage):
10
+ async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None):
11
+ with get_user_repository() as user_repo:
12
+ user_repo.update(key.user_id, state=state.state)
13
+ user_repo.session.commit()
14
+
15
+ async def get_state(self, bot: Bot, key: StorageKey) -> Optional[str]:
16
+ with get_user_repository() as user_repo:
17
+ user = user_repo.get(user_id=key.user_id)
18
+ if user is None:
19
+ user = user_repo.create(
20
+ user_id=key.user_id,
21
+ rating=0,
22
+ state=UserState.start_menu.state
23
+ )
24
+ user_repo.session.commit()
25
+ return user.state
26
+
27
+ async def set_data(self, bot: Bot, key: StorageKey, data: Dict[str, Any]):
28
+ pass
29
+
30
+ async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]:
31
+ pass
32
+
33
+ async def close(self) -> None:
34
+ pass
src/utils.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+
4
+ def update_elo(rating_user, rating_task, is_correct, K=20):
5
+ E_user = 1 / (1 + 10 ** ((rating_task - rating_user) / 400))
6
+ E_task = 1 / (1 + 10 ** ((rating_user - rating_task) / 400))
7
+
8
+ S_user = 1 if is_correct else 0
9
+ S_task = 0 if is_correct else 1
10
+
11
+ rating_user += K * (S_user - E_user)
12
+ rating_task += K * (S_task - E_task)
13
+
14
+ return rating_user, rating_task
15
+
16
+
17
+ def escape_all(message: str) -> str:
18
+ return re.sub(r"([\.\-\=\<\>\(\)])", r"\\\1", message)