167 lines
6.9 KiB
Python
167 lines
6.9 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
from typing import Final, Optional, List
|
||
|
||
import discord
|
||
from dotenv import load_dotenv
|
||
import re
|
||
from pool import PlaywrightPool, ArticleRepository
|
||
import io
|
||
from ollama import chat
|
||
from ollama import ChatResponse
|
||
from ollama import Client
|
||
from ollama import AsyncClient
|
||
|
||
load_dotenv()
|
||
|
||
DISCORD_TOKEN: Final[str] = os.getenv("DISCORD_TOKEN")
|
||
|
||
ROLE_NAME = "Newsulizer"
|
||
|
||
intents = discord.Intents.default()
|
||
intents.message_content = True
|
||
|
||
bot = discord.Client(intents=intents)
|
||
|
||
LOGGER = logging.getLogger("main")
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
)
|
||
|
||
article_repository = ArticleRepository()
|
||
|
||
async def send_chat(model, messages):
|
||
# return await AsyncClient(host="192.168.69.3:11434").chat(
|
||
# # model="deepseek-r1:1.5b",
|
||
# model="gemma3:12b-it-qat",
|
||
# messages=messages,
|
||
# stream=False,
|
||
# options={
|
||
# 'temperature': 0.5,
|
||
# # "num_ctx": 128000
|
||
# },
|
||
# think=False)
|
||
return await AsyncClient(host="192.168.69.3:11434").generate(model=model, prompt=messages, stream=False)
|
||
|
||
async def send_text_file(channel: discord.abc.Messageable, content: str, message: str = "📄 Full article attached:", filename: str = "article.md") -> None:
|
||
fp = io.BytesIO(content.encode("utf-8"))
|
||
file = discord.File(fp, filename=filename)
|
||
await channel.send(message, file=file)
|
||
|
||
|
||
async def handle_article_url(message: discord.Message, url: str) -> None:
|
||
"""
|
||
Placeholder: download + analyse the article here.
|
||
|
||
Currently just acknowledges receipt so you can verify the event flow.
|
||
"""
|
||
LOGGER.info("Received URL from %s: %s", message.author, url)
|
||
|
||
try:
|
||
title, processed_html = await article_repository.get_article(url)
|
||
paragraphs = processed_html.split("\n")
|
||
paragraphs = [f"\"Paragraph ({i + 1})\": {paragraph.strip()}" for i, paragraph in enumerate(paragraphs)]
|
||
processed_graphs = [{"role": "user", "content": paragraph} for paragraph in paragraphs]
|
||
# print(paragraphs)
|
||
# print(processed_graphs)
|
||
|
||
# messages = [
|
||
# {"role": "system", "content": "You are an expert article-analysis assistant."
|
||
# # "You WILL respond in JSON format."
|
||
# "Your job is to analyse paragraphs in the article and look for provocative, emotionally charged, and loaded language"
|
||
# "You WILL analyse the paragraphs, determine if they are provocative, and if so, output a rating between 1 and 100, 100 being the most provocative."
|
||
# "you WILL NOT output a summary of the article or the paragraphs."
|
||
# "Questions you should ask yourself while reading the paragraph:"
|
||
# "1. What is the literal meaning of the questionable word or phrase?"
|
||
# "2. What is the emotional or social context of the questionable word or phrase?"
|
||
# "3. Does that word or phrase have any connotations, that is, associations that are positive or negative?"
|
||
# "4. What group (sometimes called a “discourse community”) favors one locution over another, and why?"
|
||
# "5. Is the word or phrase “loaded”? How far does it steer us from neutral?"
|
||
# "6. Does the word or phrase help me see, or does it prevent me from seeing? (This is important)"
|
||
# "You will now be provided with the headline of the article then a paragraph from the article."
|
||
# "The headline (title of the page) will be provided as \"Headline\": \"EXAMPLE HEADLINE\"."
|
||
# "The paragraphs will be provided as \"Paragraph (numbered index)\": \"EXAMPLE PARAGRAPH\"."},
|
||
# {"role": "user", "content": f"\"Headline\": \"{title}\""}
|
||
# ]
|
||
# messages.extend(processed_graphs)
|
||
social = await send_chat("social", processed_html)
|
||
capital = await send_chat("capital", processed_html)
|
||
facts = await send_chat("facts", processed_html)
|
||
print(social)
|
||
print(capital)
|
||
print(facts)
|
||
# TODO: parse `html`, summarise, etc.
|
||
await message.channel.send(f"✅ Article downloaded – {len(processed_html):,} bytes.")
|
||
await send_text_file(message.channel, processed_html)
|
||
await send_text_file(message.channel, social.response, "Social calculations:")
|
||
await send_text_file(message.channel, capital.response, "capital calculations:")
|
||
await send_text_file(message.channel, facts.response, "facts calculations:")
|
||
except Exception as exc:
|
||
await message.channel.send("❌ Sorry, an internal error has occurred. Please try again later or contact an administrator.")
|
||
await message.channel.send(f"```\n{exc}\n```")
|
||
|
||
|
||
def extract_first_url(text: str) -> Optional[str]:
|
||
"""Return the first http(s)://… substring found in *text*, or None."""
|
||
|
||
match = re.search(r"https?://\S+", text)
|
||
return match.group(0) if match else None
|
||
|
||
@bot.event
|
||
async def on_ready() -> None:
|
||
LOGGER.info("Logged in as %s (id=%s)", bot.user, bot.user.id)
|
||
await PlaywrightPool.start()
|
||
LOGGER.info("Playwright pool ready")
|
||
LOGGER.info("------")
|
||
|
||
@bot.event
|
||
async def on_message(message: discord.Message) -> None:
|
||
# Ignore our own messages
|
||
if message.author == bot.user:
|
||
return
|
||
|
||
is_dm = message.guild is None
|
||
|
||
overwrite = False
|
||
if not is_dm:
|
||
role = discord.utils.get(message.guild.roles, name=ROLE_NAME)
|
||
if role is None:
|
||
# The role doesn't even exist in this server
|
||
await message.channel.send(f"Warning! Role **{ROLE_NAME}** not found in this server.")
|
||
return
|
||
|
||
overwrite = role in message.channel.overwrites
|
||
|
||
# Either a DM or a channel message that mentions the bot
|
||
|
||
is_mention = (bot.user in message.mentions if message.guild else False) or overwrite
|
||
|
||
if not (is_dm or is_mention):
|
||
return
|
||
|
||
url = extract_first_url(message.content)
|
||
if not url:
|
||
await message.channel.send("Please send me a link to a news article.")
|
||
return
|
||
|
||
await message.channel.send(f"🔗 Thanks, <@{message.author.id}>! I’ve queued that article for analysis.")
|
||
|
||
# Launch the processing task without blocking Discord’s event loop
|
||
asyncio.create_task(handle_article_url(message, url))
|
||
|
||
def main() -> None:
|
||
if DISCORD_TOKEN is None:
|
||
raise RuntimeError("Set the DISCORD_TOKEN environment variable or add it to a .env file.")
|
||
|
||
try:
|
||
bot.run(DISCORD_TOKEN)
|
||
finally:
|
||
asyncio.run(PlaywrightPool.stop())
|
||
|
||
if __name__ == "__main__":
|
||
main()
|