Add Ollama bot
This commit is contained in:
parent
d59e31205b
commit
3e47d5a7ee
3
catch-all/06_bots_telegram/09_ollama_bot/.dockerignore
Normal file
3
catch-all/06_bots_telegram/09_ollama_bot/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
.github
|
15
catch-all/06_bots_telegram/09_ollama_bot/.env.example
Normal file
15
catch-all/06_bots_telegram/09_ollama_bot/.env.example
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
TOKEN=0123
|
||||||
|
ADMIN_IDS=000,111
|
||||||
|
USER_IDS=000,111
|
||||||
|
ALLOW_ALL_USERS_IN_GROUPS=0
|
||||||
|
INITMODEL=llama-2
|
||||||
|
TIMEOUT=3000
|
||||||
|
|
||||||
|
# UNCOMMENT ONE OF THE FOLLOWING LINES:
|
||||||
|
# OLLAMA_BASE_URL=localhost # to run ollama without docker, using run.py
|
||||||
|
# OLLAMA_BASE_URL=ollama-server # to run ollama in a docker container
|
||||||
|
# OLLAMA_BASE_URL=host.docker.internal # to run ollama locally
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
# https://docs.python.org/3/library/logging.html#logging-levels
|
||||||
|
LOG_LEVEL=DEBUG
|
203
catch-all/06_bots_telegram/09_ollama_bot/.gitignore
vendored
Normal file
203
catch-all/06_bots_telegram/09_ollama_bot/.gitignore
vendored
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# .ollama temp
|
||||||
|
/ollama
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
*.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# Additional patches
|
||||||
|
.idea/
|
||||||
|
### MacOS ###
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
*.icloud
|
||||||
|
### Windows ###
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
*.stackdump
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# OpenSSH Keys
|
||||||
|
id_*
|
29
catch-all/06_bots_telegram/09_ollama_bot/Dockerfile
Normal file
29
catch-all/06_bots_telegram/09_ollama_bot/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
|
ARG APPHOMEDIR=code
|
||||||
|
ARG USERNAME=user
|
||||||
|
ARG USER_UID=1001
|
||||||
|
ARG USER_GID=1001
|
||||||
|
ARG PYTHONPATH_=${APPHOMEDIR}
|
||||||
|
|
||||||
|
WORKDIR /${APPHOMEDIR}
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
COPY ./bot /${APPHOMEDIR}
|
||||||
|
|
||||||
|
# Configure app home directory
|
||||||
|
RUN \
|
||||||
|
addgroup -g "$USER_GID" "$USERNAME" \
|
||||||
|
&& adduser --disabled-password -u "$USER_UID" -G "$USERNAME" -h /"$APPHOMEDIR" "$USERNAME" \
|
||||||
|
&& chown "$USERNAME:$USERNAME" -R /"$APPHOMEDIR"
|
||||||
|
|
||||||
|
# Install dependency packages, upgrade pip and then install requirements
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache gcc g++ \
|
||||||
|
&& python -m pip install --upgrade pip \
|
||||||
|
&& pip install --no-cache-dir -r requirements.txt \
|
||||||
|
&& apk del --no-cache gcc g++
|
||||||
|
|
||||||
|
USER ${USERNAME}
|
||||||
|
|
||||||
|
CMD [ "python3", "-u", "run.py"]
|
110
catch-all/06_bots_telegram/09_ollama_bot/README.md
Normal file
110
catch-all/06_bots_telegram/09_ollama_bot/README.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
# 🦙 Ollama Telegram Bot
|
||||||
|
|
||||||
|
> Repo original: https://github.com/ruecat/ollama-telegram/tree/main
|
||||||
|
|
||||||
|
## Prerrequisitos
|
||||||
|
- [Token de Telegram-Bot](https://core.telegram.org/bots#6-botfather)
|
||||||
|
|
||||||
|
## Instalación (Sin Docker)
|
||||||
|
+ Instala la última versión de [Python](https://python.org/downloads)
|
||||||
|
+ Clona el repositorio
|
||||||
|
```
|
||||||
|
git clone https://github.com/ruecat/ollama-telegram
|
||||||
|
```
|
||||||
|
+ Instala los requisitos desde requirements.txt
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
+ Ingresa todos los valores en .env.example
|
||||||
|
|
||||||
|
+ Renombra .env.example a .env
|
||||||
|
|
||||||
|
+ Inicia el bot
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 run.py
|
||||||
|
```
|
||||||
|
## Instalación (Imagen Docker)
|
||||||
|
La imagen oficial está disponible en dockerhub: [ruecat/ollama-telegram](https://hub.docker.com/r/ruecat/ollama-telegram)
|
||||||
|
|
||||||
|
+ Descarga el archivo [.env.example](https://github.com/ruecat/ollama-telegram/blob/main/.env.example), renómbralo a .env y completa las variables.
|
||||||
|
+ Crea un archivo `docker-compose.yml` (opcional: descomenta la parte de GPU en el archivo para habilitar la GPU de Nvidia)
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
ollama-telegram:
|
||||||
|
image: ruecat/ollama-telegram
|
||||||
|
container_name: ollama-telegram
|
||||||
|
restart: on-failure
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
|
||||||
|
ollama-server:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: ollama-server
|
||||||
|
volumes:
|
||||||
|
- ./ollama:/root/.ollama
|
||||||
|
|
||||||
|
# Descomenta para habilitar la GPU de NVIDIA
|
||||||
|
# De lo contrario, se ejecuta solo en la CPU:
|
||||||
|
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# reservations:
|
||||||
|
# devices:
|
||||||
|
# - driver: nvidia
|
||||||
|
# count: all
|
||||||
|
# capabilities: [gpu]
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '11434:11434'
|
||||||
|
```
|
||||||
|
|
||||||
|
+ Inicia los contenedores
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instalación (Construye tu propia imagen Docker)
|
||||||
|
+ Clona el repositorio
|
||||||
|
```
|
||||||
|
git clone https://github.com/ruecat/ollama-telegram
|
||||||
|
```
|
||||||
|
|
||||||
|
+ Ingresa todos los valores en .env.example
|
||||||
|
|
||||||
|
+ Renombra .env.example a .env
|
||||||
|
|
||||||
|
+ Ejecuta UNO de los siguientes comandos de docker compose para iniciar:
|
||||||
|
1. Para ejecutar ollama en un contenedor de docker (opcional: descomenta la parte de GPU en el archivo docker-compose.yml para habilitar la GPU de Nvidia)
|
||||||
|
```
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Para ejecutar ollama desde una instancia instalada localmente (principalmente para **MacOS**, ya que la imagen de docker aún no soporta la aceleración de GPU de Apple):
|
||||||
|
```
|
||||||
|
docker compose up --build -d ollama-telegram
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuración del Entorno
|
||||||
|
| Parámetro | Descripción | ¿Requerido? | Valor por Defecto | Ejemplo |
|
||||||
|
|:----------------------------:|:------------------------------------------------------------------------------------------------------------------------:|:-----------:|:-----------------:|:---------------------------------------------------------:|
|
||||||
|
| `TOKEN` | Tu **token de bot de Telegram**.<br/>[[¿Cómo obtener el token?]](https://core.telegram.org/bots/tutorial#obtain-your-bot-token) | Sí | `yourtoken` | MTA0M****.GY5L5F.****g*****5k |
|
||||||
|
| `ADMIN_IDS` | IDs de usuarios de Telegram de los administradores.<br/>Pueden cambiar el modelo y controlar el bot. | Sí | | 1234567890<br/>**O**<br/>1234567890,0987654321, etc. |
|
||||||
|
| `USER_IDS` | IDs de usuarios de Telegram de los usuarios regulares.<br/>Solo pueden chatear con el bot. | Sí | | 1234567890<br/>**O**<br/>1234567890,0987654321, etc. |
|
||||||
|
| `INITMODEL` | LLM predeterminado | No | `llama2` | mistral:latest<br/>mistral:7b-instruct |
|
||||||
|
| `OLLAMA_BASE_URL` | Tu URL de OllamaAPI | No | | localhost<br/>host.docker.internal |
|
||||||
|
| `OLLAMA_PORT` | Tu puerto de OllamaAPI | No | 11434 | |
|
||||||
|
| `TIMEOUT` | El tiempo de espera en segundos para generar respuestas | No | 3000 | |
|
||||||
|
| `ALLOW_ALL_USERS_IN_GROUPS` | Permite que todos los usuarios en chats grupales interactúen con el bot sin agregarlos a la lista USER_IDS | No | 0 | |
|
||||||
|
|
||||||
|
## Créditos
|
||||||
|
+ [Ollama](https://github.com/jmorganca/ollama)
|
||||||
|
|
||||||
|
## Librerías utilizadas
|
||||||
|
+ [Aiogram 3.x](https://github.com/aiogram/aiogram)
|
||||||
|
|
@ -0,0 +1,199 @@
|
|||||||
|
# >> interactions
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
|
||||||
|
from aiogram import types
|
||||||
|
from aiohttp import ClientTimeout
|
||||||
|
from asyncio import Lock
|
||||||
|
from functools import wraps
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv('.env')
|
||||||
|
token = os.getenv("TOKEN")
|
||||||
|
|
||||||
|
allowed_ids = list(map(int, os.getenv("USER_IDS", "").split(",")))
|
||||||
|
admin_ids = list(map(int, os.getenv("ADMIN_IDS", "").split(",")))
|
||||||
|
|
||||||
|
ollama_base_url = os.getenv("OLLAMA_BASE_URL")
|
||||||
|
ollama_port = os.getenv("OLLAMA_PORT", "11434")
|
||||||
|
|
||||||
|
log_level_str = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
allow_all_users_in_groups = bool(
|
||||||
|
int(os.getenv("ALLOW_ALL_USERS_IN_GROUPS", "0")))
|
||||||
|
|
||||||
|
log_levels = list(logging._levelToName.values())
|
||||||
|
|
||||||
|
timeout = os.getenv("TIMEOUT", "3000")
|
||||||
|
|
||||||
|
if log_level_str not in log_levels:
|
||||||
|
|
||||||
|
log_level = logging.DEBUG
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
log_level = logging.getLevelName(log_level_str)
|
||||||
|
|
||||||
|
logging.basicConfig(level=log_level)
|
||||||
|
|
||||||
|
|
||||||
|
async def model_list():
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
|
||||||
|
url = f"http://{ollama_base_url}:{ollama_port}/api/tags"
|
||||||
|
|
||||||
|
async with session.get(url) as response:
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
|
||||||
|
data = await response.json()
|
||||||
|
return data["models"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def generate(payload: dict, modelname: str, prompt: str):
|
||||||
|
|
||||||
|
client_timeout = ClientTimeout(total=int(timeout))
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(timeout=client_timeout) as session:
|
||||||
|
|
||||||
|
url = f"http://{ollama_base_url}:{ollama_port}/api/chat"
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
async with session.post(url, json=payload) as response:
|
||||||
|
|
||||||
|
if response.status != 200:
|
||||||
|
|
||||||
|
raise aiohttp.ClientResponseError(
|
||||||
|
|
||||||
|
status=response.status, message=response.reason
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
buffer = b""
|
||||||
|
|
||||||
|
async for chunk in response.content.iter_any():
|
||||||
|
|
||||||
|
buffer += chunk
|
||||||
|
|
||||||
|
while b"\n" in buffer:
|
||||||
|
|
||||||
|
line, buffer = buffer.split(b"\n", 1)
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
if line:
|
||||||
|
|
||||||
|
yield json.loads(line)
|
||||||
|
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
|
||||||
|
print(f"Error during request: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def perms_allowed(func):
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(message: types.Message = None, query: types.CallbackQuery = None):
|
||||||
|
|
||||||
|
user_id = message.from_user.id if message else query.from_user.id
|
||||||
|
|
||||||
|
if user_id in admin_ids or user_id in allowed_ids:
|
||||||
|
|
||||||
|
if message:
|
||||||
|
|
||||||
|
return await func(message)
|
||||||
|
|
||||||
|
elif query:
|
||||||
|
|
||||||
|
return await func(query=query)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
if message:
|
||||||
|
|
||||||
|
if message and message.chat.type in ["supergroup", "group"]:
|
||||||
|
|
||||||
|
if allow_all_users_in_groups:
|
||||||
|
|
||||||
|
return await func(message)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer("Access Denied")
|
||||||
|
|
||||||
|
elif query:
|
||||||
|
|
||||||
|
if message and message.chat.type in ["supergroup", "group"]:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
await query.answer("Access Denied")
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def perms_admins(func):
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(message: types.Message = None, query: types.CallbackQuery = None):
|
||||||
|
|
||||||
|
user_id = message.from_user.id if message else query.from_user.id
|
||||||
|
|
||||||
|
if user_id in admin_ids:
|
||||||
|
|
||||||
|
if message:
|
||||||
|
|
||||||
|
return await func(message)
|
||||||
|
|
||||||
|
elif query:
|
||||||
|
|
||||||
|
return await func(query=query)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
if message:
|
||||||
|
|
||||||
|
if message and message.chat.type in ["supergroup", "group"]:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer("Access Denied")
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"[MSG] {message.from_user.first_name} {
|
||||||
|
message.from_user.last_name}({message.from_user.id}) is not allowed to use this bot."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif query:
|
||||||
|
|
||||||
|
if message and message.chat.type in ["supergroup", "group"]:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
await query.answer("Access Denied")
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"[QUERY] {message.from_user.first_name} {
|
||||||
|
message.from_user.last_name}({message.from_user.id}) is not allowed to use this bot."
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class contextLock:
|
||||||
|
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
await self.lock.acquire()
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_value, exc_traceback):
|
||||||
|
self.lock.release()
|
426
catch-all/06_bots_telegram/09_ollama_bot/bot/run.py
Normal file
426
catch-all/06_bots_telegram/09_ollama_bot/bot/run.py
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
from aiogram import Bot, Dispatcher, types
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.filters.command import Command, CommandStart
|
||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from func.interactions import *
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
|
bot = Bot(token=token)
|
||||||
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
start_kb = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
settings_kb = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
start_kb.row(
|
||||||
|
types.InlineKeyboardButton(text="ℹ️ About", callback_data="about"),
|
||||||
|
types.InlineKeyboardButton(text="⚙️ Settings", callback_data="settings"),
|
||||||
|
)
|
||||||
|
|
||||||
|
settings_kb.row(
|
||||||
|
types.InlineKeyboardButton(text="🔄 Switch LLM", callback_data="switchllm"),
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="✏️ Edit system prompt", callback_data="editsystemprompt"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
types.BotCommand(command="start", description="Start"),
|
||||||
|
types.BotCommand(command="reset", description="Reset Chat"),
|
||||||
|
types.BotCommand(command="history", description="Look through messages"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ACTIVE_CHATS = {}
|
||||||
|
ACTIVE_CHATS_LOCK = contextLock()
|
||||||
|
|
||||||
|
modelname = os.getenv("INITMODEL")
|
||||||
|
mention = None
|
||||||
|
|
||||||
|
CHAT_TYPE_GROUP = "group"
|
||||||
|
CHAT_TYPE_SUPERGROUP = "supergroup"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_bot_info():
|
||||||
|
|
||||||
|
global mention
|
||||||
|
|
||||||
|
if mention is None:
|
||||||
|
|
||||||
|
get = await bot.get_me()
|
||||||
|
mention = f"@{get.username}"
|
||||||
|
|
||||||
|
return mention
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message(CommandStart())
|
||||||
|
async def command_start_handler(message: Message) -> None:
|
||||||
|
|
||||||
|
start_message = f"Welcome, <b>{message.from_user.full_name}</b>!"
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
start_message,
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=start_kb.as_markup(),
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message(Command("reset"))
|
||||||
|
async def command_reset_handler(message: Message) -> None:
|
||||||
|
|
||||||
|
if message.from_user.id in allowed_ids:
|
||||||
|
|
||||||
|
if message.from_user.id in ACTIVE_CHATS:
|
||||||
|
|
||||||
|
async with ACTIVE_CHATS_LOCK:
|
||||||
|
|
||||||
|
ACTIVE_CHATS.pop(message.from_user.id)
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"Chat has been reset for {message.from_user.first_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text="Chat has been reset",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message(Command("history"))
|
||||||
|
async def command_get_context_handler(message: Message) -> None:
|
||||||
|
|
||||||
|
if message.from_user.id in allowed_ids:
|
||||||
|
|
||||||
|
if message.from_user.id in ACTIVE_CHATS:
|
||||||
|
|
||||||
|
messages = ACTIVE_CHATS.get(message.chat.id)["messages"]
|
||||||
|
context = ""
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
|
||||||
|
context += f"*{msg['role'].capitalize()}*: {msg['content']}\n"
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text=context,
|
||||||
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text="No chat history available for this user",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.callback_query(lambda query: query.data == "settings")
|
||||||
|
async def settings_callback_handler(query: types.CallbackQuery):
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=query.message.chat.id,
|
||||||
|
text=f"Choose the right option.",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
reply_markup=settings_kb.as_markup()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.callback_query(lambda query: query.data == "switchllm")
|
||||||
|
async def switchllm_callback_handler(query: types.CallbackQuery):
|
||||||
|
|
||||||
|
models = await model_list()
|
||||||
|
switchllm_builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
|
||||||
|
modelname = model["name"]
|
||||||
|
modelfamilies = ""
|
||||||
|
|
||||||
|
if model["details"]["families"]:
|
||||||
|
|
||||||
|
modelicon = {"llama": "🦙", "clip": "📷"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
modelfamilies = "".join(
|
||||||
|
[modelicon[family]
|
||||||
|
for family in model["details"]["families"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
|
||||||
|
modelfamilies = f"✨"
|
||||||
|
|
||||||
|
switchllm_builder.row(
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text=f"{modelname} {modelfamilies}",
|
||||||
|
callback_data=f"model_{modelname}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await query.message.edit_text(
|
||||||
|
f"{len(models)} models available.\n🦙 = Regular\n🦙📷 = Multimodal", reply_markup=switchllm_builder.as_markup(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.callback_query(lambda query: query.data.startswith("model_"))
|
||||||
|
async def model_callback_handler(query: types.CallbackQuery):
|
||||||
|
|
||||||
|
global modelname
|
||||||
|
global modelfamily
|
||||||
|
|
||||||
|
modelname = query.data.split("model_")[1]
|
||||||
|
|
||||||
|
await query.answer(f"Chosen model: {modelname}")
|
||||||
|
|
||||||
|
|
||||||
|
@dp.callback_query(lambda query: query.data == "about")
|
||||||
|
@perms_admins
|
||||||
|
async def about_callback_handler(query: types.CallbackQuery):
|
||||||
|
|
||||||
|
dotenv_model = os.getenv("INITMODEL")
|
||||||
|
|
||||||
|
global modelname
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=query.message.chat.id,
|
||||||
|
text=f"""<b>Your LLMs</b>
|
||||||
|
Currently using: <code>{modelname}</code>
|
||||||
|
Default in .env: <code>{dotenv_model}</code>
|
||||||
|
This project is under <a href='https://github.com/ruecat/ollama-telegram/blob/main/LICENSE'>MIT License.</a>
|
||||||
|
<a href='https://github.com/ruecat/ollama-telegram'>Source Code</a>
|
||||||
|
""",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message()
|
||||||
|
@perms_allowed
|
||||||
|
async def handle_message(message: types.Message):
|
||||||
|
|
||||||
|
await get_bot_info()
|
||||||
|
|
||||||
|
if message.chat.type == "private":
|
||||||
|
|
||||||
|
await ollama_request(message)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if await is_mentioned_in_group_or_supergroup(message):
|
||||||
|
|
||||||
|
thread = await collect_message_thread(message)
|
||||||
|
prompt = format_thread_for_prompt(thread)
|
||||||
|
|
||||||
|
await ollama_request(message, prompt)
|
||||||
|
|
||||||
|
|
||||||
|
async def is_mentioned_in_group_or_supergroup(message: types.Message):
|
||||||
|
|
||||||
|
if message.chat.type not in ["group", "supergroup"]:
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_mentioned = (
|
||||||
|
(message.text and message.text.startswith(mention)) or
|
||||||
|
(message.caption and message.caption.startswith(mention))
|
||||||
|
)
|
||||||
|
|
||||||
|
is_reply_to_bot = (
|
||||||
|
message.reply_to_message and
|
||||||
|
message.reply_to_message.from_user.id == bot.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_mentioned or is_reply_to_bot
|
||||||
|
|
||||||
|
|
||||||
|
async def collect_message_thread(message: types.Message, thread=None):
|
||||||
|
|
||||||
|
if thread is None:
|
||||||
|
|
||||||
|
thread = []
|
||||||
|
|
||||||
|
thread.insert(0, message)
|
||||||
|
|
||||||
|
if message.reply_to_message:
|
||||||
|
|
||||||
|
await collect_message_thread(message.reply_to_message, thread)
|
||||||
|
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
def format_thread_for_prompt(thread):
|
||||||
|
|
||||||
|
prompt = "Conversation thread:\n\n"
|
||||||
|
|
||||||
|
for msg in thread:
|
||||||
|
|
||||||
|
sender = "User" if msg.from_user.id != bot.id else "Bot"
|
||||||
|
content = msg.text or msg.caption or "[No text content]"
|
||||||
|
prompt += f"{sender}: {content}\n\n"
|
||||||
|
|
||||||
|
prompt += "History:"
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
|
async def process_image(message):
|
||||||
|
|
||||||
|
image_base64 = ""
|
||||||
|
|
||||||
|
if message.content_type == "photo":
|
||||||
|
|
||||||
|
image_buffer = io.BytesIO()
|
||||||
|
|
||||||
|
await bot.download(message.photo[-1], destination=image_buffer)
|
||||||
|
|
||||||
|
image_base64 = base64.b64encode(
|
||||||
|
image_buffer.getvalue()
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
return image_base64
|
||||||
|
|
||||||
|
|
||||||
|
async def add_prompt_to_active_chats(message, prompt, image_base64, modelname):
|
||||||
|
|
||||||
|
async with ACTIVE_CHATS_LOCK:
|
||||||
|
|
||||||
|
if ACTIVE_CHATS.get(message.from_user.id) is None:
|
||||||
|
|
||||||
|
ACTIVE_CHATS[message.from_user.id] = {
|
||||||
|
"model": modelname,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt,
|
||||||
|
"images": ([image_base64] if image_base64 else []),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stream": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
ACTIVE_CHATS[message.from_user.id]["messages"].append(
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt,
|
||||||
|
"images": ([image_base64] if image_base64 else []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_response(message, response_data, full_response):
|
||||||
|
|
||||||
|
full_response_stripped = full_response.strip()
|
||||||
|
|
||||||
|
if full_response_stripped == "":
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if response_data.get("done"):
|
||||||
|
|
||||||
|
text = f"{full_response_stripped}\n\n⚙️ {modelname}\nGenerated in {
|
||||||
|
response_data.get('total_duration') / 1e9:.2f}s."
|
||||||
|
|
||||||
|
await send_response(message, text)
|
||||||
|
|
||||||
|
async with ACTIVE_CHATS_LOCK:
|
||||||
|
|
||||||
|
if ACTIVE_CHATS.get(message.from_user.id) is not None:
|
||||||
|
|
||||||
|
ACTIVE_CHATS[message.from_user.id]["messages"].append(
|
||||||
|
{"role": "assistant", "content": full_response_stripped}
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"[Response]: '{full_response_stripped}' for {
|
||||||
|
message.from_user.first_name} {message.from_user.last_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def send_response(message, text):
|
||||||
|
|
||||||
|
# A negative message.chat.id is a group message
|
||||||
|
if message.chat.id < 0 or message.chat.id == message.from_user.id:
|
||||||
|
|
||||||
|
await bot.send_message(chat_id=message.chat.id, text=text)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
text=text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def ollama_request(message: types.Message, prompt: str = None):
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
full_response = ""
|
||||||
|
await bot.send_chat_action(message.chat.id, "typing")
|
||||||
|
image_base64 = await process_image(message)
|
||||||
|
|
||||||
|
if prompt is None:
|
||||||
|
|
||||||
|
prompt = message.text or message.caption
|
||||||
|
|
||||||
|
await add_prompt_to_active_chats(message, prompt, image_base64, modelname)
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f"[OllamaAPI]: Processing '{prompt}' for {
|
||||||
|
message.from_user.first_name} {message.from_user.last_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = ACTIVE_CHATS.get(message.from_user.id)
|
||||||
|
|
||||||
|
async for response_data in generate(payload, modelname, prompt):
|
||||||
|
|
||||||
|
msg = response_data.get("message")
|
||||||
|
|
||||||
|
if msg is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
chunk = msg.get("content", "")
|
||||||
|
full_response += chunk
|
||||||
|
|
||||||
|
if any([c in chunk for c in ".\n!?"]) or response_data.get("done"):
|
||||||
|
|
||||||
|
if await handle_response(message, response_data, full_response):
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
print(f"""-----
|
||||||
|
[OllamaAPI-ERR] CAUGHT FAULT!
|
||||||
|
{traceback.format_exc()}
|
||||||
|
-----""")
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text=f"Something went wrong.",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
await bot.set_my_commands(commands)
|
||||||
|
await dp.start_polling(bot, skip_update=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
asyncio.run(main())
|
30
catch-all/06_bots_telegram/09_ollama_bot/docker-compose.yml
Normal file
30
catch-all/06_bots_telegram/09_ollama_bot/docker-compose.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# WORK IN PROGRESS
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
ollama-tg:
|
||||||
|
build: .
|
||||||
|
container_name: ollama-tg
|
||||||
|
restart: on-failure
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
|
||||||
|
ollama-api:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: ollama-server
|
||||||
|
volumes:
|
||||||
|
- ./ollama:/root/.ollama
|
||||||
|
|
||||||
|
# Uncomment to enable NVIDIA GPU
|
||||||
|
# Otherwise runs on CPU only:
|
||||||
|
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# reservations:
|
||||||
|
# devices:
|
||||||
|
# - driver: nvidia
|
||||||
|
# count: all
|
||||||
|
# capabilities: [gpu]
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '11434:11434'
|
@ -0,0 +1,3 @@
|
|||||||
|
python-dotenv==1.0.0
|
||||||
|
aiogram==3.2.0
|
||||||
|
ollama
|
@ -16,6 +16,7 @@
|
|||||||
| [Bot de películas](./06_movie_bot/) | Bot que devuelve información de películas | intermedio |
|
| [Bot de películas](./06_movie_bot/) | Bot que devuelve información de películas | intermedio |
|
||||||
| [Bot trivial de películas](./07_movie2_bot/README.md) | Bot que devuelve información de series | avanzado |
|
| [Bot trivial de películas](./07_movie2_bot/README.md) | Bot que devuelve información de series | avanzado |
|
||||||
| [Bot de chatgpt](./08_chatgpt_bot/README.md) | Bot que mantiene conversaciones con GPT-3 | avanzado |
|
| [Bot de chatgpt](./08_chatgpt_bot/README.md) | Bot que mantiene conversaciones con GPT-3 | avanzado |
|
||||||
|
| [Bot con Ollama](./09_ollama_bot/README.md) | Bot que mantiene conversaciones con Ollama | intermedio |
|
||||||
| **Bot de recetas** (próximamente) | Bot que devuelve recetas de cocina | avanzado |
|
| **Bot de recetas** (próximamente) | Bot que devuelve recetas de cocina | avanzado |
|
||||||
| **Bot de deportes** (próximamente) | Bot que devuelve información de deportes | avanzado |
|
| **Bot de deportes** (próximamente) | Bot que devuelve información de deportes | avanzado |
|
||||||
| **Bot de mareas** (próximamente) | Bot que devuelve información de mareas | avanzado |
|
| **Bot de mareas** (próximamente) | Bot que devuelve información de mareas | avanzado |
|
||||||
|
Loading…
Reference in New Issue
Block a user