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()