Scripts/news/main.py

167 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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}>! Ive queued that article for analysis.")
# Launch the processing task without blocking Discords 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()