Scripts/news/main.py

226 lines
9.7 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
import time
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()
social_system_prompt = ("You are a specialized analysis program designed to determine if articles are pro-social. "
"Pro-social text contains topics such as raising concerns about the negative effects on workers, the environment, "
"or on society as a whole (as in the concerns of the 99%, or the proletariat). "
"You WILL give rating of this article by calling the increment tool if you read a paragraph (seperated by newlines) which is pro-society, and decrement if it is anti-society. "
"You WILL respond explaining why you have called the tools you did. "
"You ARE allowed to answer with \"this article doesn't require social analysis\" if the topic is not relevant to social interests. "
"You ARE allowed to not make calls to your tools if the topic is not relevant to social interests. ")
capital_system_prompt = ("You are a specialized analysis program designed to determine if articles are pro-capital. "
"Pro-capital text is concerned but not limited to the interests of business, or of the rich and elite. "
"You WILL give rating of this article by calling the increment tool if you read a paragraph (seperated by newlines) which is pro-capital, and decrement if it is anti-capital. "
"you ARE allowed to call the tools multiple times. "
"You WILL respond explaining why you have called the tools you did. "
"You ARE allowed to answer with \"this article doesn't require capital analysis\" if the topic is not relevant to capital interests. "
"You ARE allowed to not make calls to your tools if the topic is not relevant to capital interests. ")
facts_system_prompt = ("You are a specialized analysis program designed to determine if articles are attempting to accurately represent facts. "
"You are not checking if the facts presented in the article are correct, "
"rather you are to determine if an attempt was made to represent multiple sides of the issue. "
"This can range from only presenting the news in the form of events that happened or expert comments "
"(therefore not introducing the writer's opinion into the article), to not using too much emotional language "
"(emotional language can be fine, if for example the article is trying to communicate that one side has commited genocide, "
"it is okay to be emotional over that topic and should probably be encouraged. "
"If the article only presents opinions about genocide, then it is not accurately representing what happened). "
"You WILL give rating of this article by calling the increment tool if you read a paragraph (seperated by newlines) which is accurately representing facts, and decrement if it is not.")
async def send_chat(model, messages, tools = None):
return await AsyncClient(host="192.168.69.3:11434").chat(
model=model,
messages=messages,
stream=False,
tools=tools,
options={
'temperature': 0.5,
# "num_ctx": 128000
})
# return await AsyncClient(host="192.168.69.3:11434").generate(model=model, prompt=messages, stream=False)
async def send_chat_with_system(model, message, system, tools = None):
messages = [{'role': 'system', 'content': system}, {'role': 'user', 'content': message}]
return await send_chat(model, messages, tools)
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)
def tally_responses(tools):
increment = 0
decrement = 0
if tools:
for tool in tools:
if tool['function']['name'] == "increment":
increment += 1
elif tool['function']['name'] == "decrement":
decrement += 1
else:
LOGGER.warning(f"Unknown tool: {tool}")
return increment, decrement
async def handle_article_url(message: discord.Message, url: str) -> None:
LOGGER.info("Received URL from %s: %s", message.author, url)
try:
title, processed_html = await article_repository.get_article(url)
tools = [
{
'type': 'function',
'function': {
'name': 'increment',
'description': 'increment internal counter by 1',
'parameters': {
'type': 'object',
'properties': {},
'required': []
}
}
},
{
'type': 'function',
'function': {
'name': 'decrement',
'description': 'decrement internal counter by 1',
'parameters': {
'type': 'object',
'properties': {},
'required': []
}
}
}
]
social = await send_chat_with_system("social", processed_html, social_system_prompt, tools)
capital = await send_chat_with_system("capital", processed_html, capital_system_prompt, tools)
facts = await send_chat_with_system("facts", processed_html, facts_system_prompt, tools)
print(social)
print(capital)
print(facts)
social_increment, social_decrement = tally_responses(social['message']["tool_calls"])
capital_increment, capital_decrement = tally_responses(capital['message']["tool_calls"])
facts_increment, facts_decrement = tally_responses(facts['message']["tool_calls"])
# TODO: parse `html`, summarise, etc.
await message.channel.send(f"✅ Article downloaded {len(processed_html):,} bytes.")
time.sleep(0.1)
await send_text_file(message.channel, processed_html)
time.sleep(0.1)
await send_text_file(message.channel, social["message"]["content"], "Social calculations:")
time.sleep(0.1)
await message.channel.send(f"Social+ {social_increment} | Social- {social_decrement}")
time.sleep(0.1)
await send_text_file(message.channel, capital["message"]["content"], "capital calculations:")
time.sleep(0.1)
await message.channel.send(f"Capital+ {capital_increment} | Capital- {capital_decrement}")
time.sleep(0.1)
await send_text_file(message.channel, facts["message"]["content"], "facts calculations:")
time.sleep(0.1)
await message.channel.send(f"Facts+ {facts_increment} | Facts- {facts_decrement}")
time.sleep(0.1)
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```")
LOGGER.error(exc, exc_info=True)
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()