diff --git a/catch-all/06_bots_telegram/09_ollama_bot/.dockerignore b/catch-all/06_bots_telegram/09_ollama_bot/.dockerignore
new file mode 100644
index 0000000..075da0a
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/.dockerignore
@@ -0,0 +1,3 @@
+README.md
+*.md
+.github
\ No newline at end of file
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/.env.example b/catch-all/06_bots_telegram/09_ollama_bot/.env.example
new file mode 100644
index 0000000..7a25e27
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/.env.example
@@ -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
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/.gitignore b/catch-all/06_bots_telegram/09_ollama_bot/.gitignore
new file mode 100644
index 0000000..c53024a
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/.gitignore
@@ -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_*
\ No newline at end of file
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/Dockerfile b/catch-all/06_bots_telegram/09_ollama_bot/Dockerfile
new file mode 100644
index 0000000..e518990
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/Dockerfile
@@ -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"]
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/README.md b/catch-all/06_bots_telegram/09_ollama_bot/README.md
new file mode 100644
index 0000000..4a891f8
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/README.md
@@ -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**.
[[¿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.
Pueden cambiar el modelo y controlar el bot. | Sí | | 1234567890
**O**
1234567890,0987654321, etc. |
+| `USER_IDS` | IDs de usuarios de Telegram de los usuarios regulares.
Solo pueden chatear con el bot. | Sí | | 1234567890
**O**
1234567890,0987654321, etc. |
+| `INITMODEL` | LLM predeterminado | No | `llama2` | mistral:latest
mistral:7b-instruct |
+| `OLLAMA_BASE_URL` | Tu URL de OllamaAPI | No | | localhost
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)
+
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/bot/func/interactions.py b/catch-all/06_bots_telegram/09_ollama_bot/bot/func/interactions.py
new file mode 100644
index 0000000..d157e9c
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/bot/func/interactions.py
@@ -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()
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/bot/run.py b/catch-all/06_bots_telegram/09_ollama_bot/bot/run.py
new file mode 100644
index 0000000..9b6500d
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/bot/run.py
@@ -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, {message.from_user.full_name}!"
+
+ 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"""Your LLMs
+Currently using: {modelname}
+Default in .env: {dotenv_model}
+This project is under MIT License.
+Source Code
+""",
+ 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())
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/docker-compose.yml b/catch-all/06_bots_telegram/09_ollama_bot/docker-compose.yml
new file mode 100644
index 0000000..18bf4fd
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/docker-compose.yml
@@ -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'
diff --git a/catch-all/06_bots_telegram/09_ollama_bot/requirements.txt b/catch-all/06_bots_telegram/09_ollama_bot/requirements.txt
new file mode 100644
index 0000000..ae4f875
--- /dev/null
+++ b/catch-all/06_bots_telegram/09_ollama_bot/requirements.txt
@@ -0,0 +1,3 @@
+python-dotenv==1.0.0
+aiogram==3.2.0
+ollama
\ No newline at end of file
diff --git a/catch-all/06_bots_telegram/README.md b/catch-all/06_bots_telegram/README.md
index 3724375..a12b9f0 100644
--- a/catch-all/06_bots_telegram/README.md
+++ b/catch-all/06_bots_telegram/README.md
@@ -16,6 +16,7 @@
| [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 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 deportes** (próximamente) | Bot que devuelve información de deportes | avanzado |
| **Bot de mareas** (próximamente) | Bot que devuelve información de mareas | avanzado |