From d60ef6ab2bcaad2337d37cc963afefd6a0848eec Mon Sep 17 00:00:00 2001 From: kpj2006 <24ucs074@lnmiit.ac.in> Date: Thu, 11 Jun 2026 10:31:26 +0530 Subject: [PATCH 1/3] feat: add initial bot implementation with environment configuration and skill rules --- .clinerules | 83 ++++++++++++++++++++++++++ .env.example | 4 ++ .gitignore | 3 + bot.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 5 files changed, 242 insertions(+) create mode 100644 .clinerules create mode 100644 .env.example create mode 100644 bot.py create mode 100644 requirements.txt diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..da8e3fb --- /dev/null +++ b/.clinerules @@ -0,0 +1,83 @@ +# === ROLE === +You are an Open Source Mentor Bot helping users work with an AOSSIE project template. + +# === PRIMARY BEHAVIOR === +- Always ask at least 1 clarifying question before answering (unless trivial) +- Guide users step-by-step instead of dumping full answers +- Help fill TODO sections in project templates + +# === CONTEXT AWARENESS === +You are working with a GitHub template repo that includes: +- README with TODO sections (project name, description, user flow) +- Setup instructions (install, run, env config) +- Contribution guidelines + +# === QUESTION STRATEGY === +When user asks something: +1. Ask what exactly they want to build +2. Ask their tech stack (Node / Python / Flutter etc.) +3. Ask their goal (GSoC / learning / hackathon / production) + +Examples: +- "What are you building using this template?" +- "Which tech stack are you planning to use?" +- "Is this for GSoC or a personal project?" + +# === TASK-SPECIFIC BEHAVIOR === + +## If user says "setup" +- Ask: + - "Which language/framework are you using?" +- Then guide: + - Clone repo + - Install dependencies + - Setup .env + - Run dev server + +## If user says "README" +- Ask: + - "What is your project idea?" +- Then help fill: + - Project name + - Description + - User flow + - Features + +## If user says "contribute" +- Ask: + - "Are you contributing or creating your own project?" +- Then guide: + - Fork repo + - Create branch + - Follow CONTRIBUTING.md + +## If user says "error" +- Ask: + - "What error are you getting?" + - "Share logs/code snippet" +- Then debug step-by-step + +# === RESPONSE STYLE === +- Keep answers short (max 6 lines) +- Prefer bullet points +- Ask → then guide → then suggest next step + +# === EXAMPLES === + +User: "help me setup" +Bot: +- "Which tech stack are you using?" +- Then guide setup steps + +User: "write README" +Bot: +- "What is your project idea?" +- Then generate structured README + +# === RESTRICTIONS === +- Do not assume missing info +- Always ask before generating full solutions +- Avoid long explanations unless asked + +# === GOAL === +Act like a mentor helping users complete an AOSSIE template repo step-by-step. \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..598b488 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DISCORD_TOKEN=your_discord_bot_token_here +DISCORD_CHANNEL_ID=your_channel_id_here +OLLAMA_MODEL=llama3.2 +SKILL_FILE_PATH=.clinerules \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9308a4b..8b4e782 100644 --- a/.gitignore +++ b/.gitignore @@ -324,3 +324,6 @@ TSWLatexianTemp* # option is specified. Footnotes are the stored in a file with suffix Notes.bib. # Uncomment the next line to have this generated file ignored. #*Notes.bib + +venv/ +.env \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..7f15b24 --- /dev/null +++ b/bot.py @@ -0,0 +1,149 @@ +import os +import logging +import discord +import httpx +import asyncio +from dotenv import load_dotenv + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('aossie-bot') + +# Load environment variables +load_dotenv() + +DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') +DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID') +OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'llama3.2') +SKILL_FILE_PATH = os.getenv('SKILL_FILE_PATH', '.clinerules') +OLLAMA_URL = "http://localhost:11434/api/generate" + +# Initialize bot with intents +intents = discord.Intents.default() +intents.message_content = True +client = discord.Client(intents=intents) + +# Lock to prevent Ollama requests from clashing +ollama_lock = asyncio.Lock() + +def load_skill_context() -> str: + """Load context from the local skill file.""" + try: + if os.path.exists(SKILL_FILE_PATH): + with open(SKILL_FILE_PATH, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + logger.error(f"Error loading skill file {SKILL_FILE_PATH}: {e}") + return "" + +async def generate_ollama_response(prompt: str, context: str) -> str: + """Send prompt to local Ollama instance and return the response.""" + if context: + system_prompt = f"You are a helpful contributor assistant for AOSSIE.\n\nContext guidelines:\n{context}" + else: + system_prompt = "You are a helpful contributor assistant for AOSSIE." + + payload = { + "model": OLLAMA_MODEL, + "prompt": prompt, + "system": system_prompt, + "stream": False + } + + try: + async with httpx.AsyncClient(timeout=120.0) as http_client: + response = await http_client.post(OLLAMA_URL, json=payload) + response.raise_for_status() + data = response.json() + return data.get("response", "Error: No response text found in Ollama reply.") + except httpx.TimeoutException: + logger.error("Ollama request timed out.") + return "I'm sorry, the local AI model timed out while thinking. Please try again later." + except httpx.RequestError as e: + logger.error(f"Ollama request error: {e}") + return f"I'm sorry, I couldn't reach the local AI engine. Ensure Ollama is running at localhost:11434." + except Exception as e: + logger.error(f"Unexpected error during Ollama generation: {e}") + return "An unexpected error occurred while generating the response." + +async def process_message(message: discord.Message): + """Process a single message and generate a reply safely.""" + if message.author.bot or str(message.channel.id) != DISCORD_CHANNEL_ID: + return + + # Use lock to ensure only one message is processed by Ollama at a time + async with ollama_lock: + async with message.channel.typing(): + skill_context = load_skill_context() + response_text = await generate_ollama_response(message.content, skill_context) + + if len(response_text) > 1900: + response_text = response_text[:1896] + "..." + + await message.reply(response_text) + +async def wait_for_ollama(): + """Wait until Ollama is up and responding.""" + logger.info("Waiting for Ollama to be ready...") + while True: + try: + async with httpx.AsyncClient(timeout=5.0) as http_client: + response = await http_client.get("http://localhost:11434/") + if response.status_code == 200: + logger.info("Ollama is ready!") + return + except httpx.RequestError: + pass + logger.info("Ollama not reachable yet. Retrying in 10 seconds...") + await asyncio.sleep(10) + +@client.event +async def on_ready(): + logger.info(f"Logged in as {client.user.name} ({client.user.id})") + + # Wait for Ollama to be ready before processing the backlog + await wait_for_ollama() + + logger.info("Checking for missed messages...") + + try: + channel = await client.fetch_channel(int(DISCORD_CHANNEL_ID)) + + # Find the last message sent by the bot + last_bot_msg = None + async for msg in channel.history(limit=50): + if msg.author.id == client.user.id: + last_bot_msg = msg + break + + messages_to_process = [] + if last_bot_msg: + # Fetch messages after the bot's last message + async for msg in channel.history(after=last_bot_msg, oldest_first=True): + if not msg.author.bot: + messages_to_process.append(msg) + else: + # If no bot message found, just process the last 5 user messages + async for msg in channel.history(limit=5, oldest_first=True): + if not msg.author.bot: + messages_to_process.append(msg) + + logger.info(f"Found {len(messages_to_process)} missed messages. Processing...") + for msg in messages_to_process: + await process_message(msg) + + except Exception as e: + logger.error(f"Error fetching missed messages: {e}") + + logger.info("AOSSIE Contributor Assistant MVP is fully ready.") + +@client.event +async def on_message(message: discord.Message): + await process_message(message) + +if __name__ == "__main__": + if not DISCORD_TOKEN or not DISCORD_CHANNEL_ID: + logger.error("Critical missing config: DISCORD_TOKEN and DISCORD_CHANNEL_ID must be set in .env") + else: + logger.info("Starting bot...") + client.run(DISCORD_TOKEN) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d0d2f6a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py==2.3.2 +httpx==0.27.0 +python-dotenv==1.0.1 \ No newline at end of file From 469d336f153f3639bb9f4a4aebcb5484133b99fc Mon Sep 17 00:00:00 2001 From: kpj2006 <24ucs074@lnmiit.ac.in> Date: Thu, 11 Jun 2026 10:54:11 +0530 Subject: [PATCH 2/3] Update requirements.txt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d0d2f6a..66958f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ discord.py==2.3.2 httpx==0.27.0 -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.2.2 +idna==3.15 \ No newline at end of file From 0f166c5a745540ba77e93942591c53e50c9bed7b Mon Sep 17 00:00:00 2001 From: kpj2006 <24ucs074@lnmiit.ac.in> Date: Thu, 11 Jun 2026 11:07:40 +0530 Subject: [PATCH 3/3] fix: ensure DISCORD_CHANNEL_ID is an integer and improve error handling for environment variables --- bot.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/bot.py b/bot.py index 7f15b24..e576997 100644 --- a/bot.py +++ b/bot.py @@ -14,6 +14,7 @@ DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID') +DISCORD_CHANNEL_ID_INT = None OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'llama3.2') SKILL_FILE_PATH = os.getenv('SKILL_FILE_PATH', '.clinerules') OLLAMA_URL = "http://localhost:11434/api/generate" @@ -68,7 +69,7 @@ async def generate_ollama_response(prompt: str, context: str) -> str: async def process_message(message: discord.Message): """Process a single message and generate a reply safely.""" - if message.author.bot or str(message.channel.id) != DISCORD_CHANNEL_ID: + if message.author.bot or message.channel.id != DISCORD_CHANNEL_ID_INT: return # Use lock to ensure only one message is processed by Ollama at a time @@ -107,7 +108,7 @@ async def on_ready(): logger.info("Checking for missed messages...") try: - channel = await client.fetch_channel(int(DISCORD_CHANNEL_ID)) + channel = await client.fetch_channel(DISCORD_CHANNEL_ID_INT) # Find the last message sent by the bot last_bot_msg = None @@ -142,8 +143,21 @@ async def on_message(message: discord.Message): await process_message(message) if __name__ == "__main__": - if not DISCORD_TOKEN or not DISCORD_CHANNEL_ID: - logger.error("Critical missing config: DISCORD_TOKEN and DISCORD_CHANNEL_ID must be set in .env") - else: - logger.info("Starting bot...") - client.run(DISCORD_TOKEN) \ No newline at end of file + if not DISCORD_TOKEN: + logger.critical("DISCORD_TOKEN is missing from environment. Exiting.") + exit(1) + + if not DISCORD_CHANNEL_ID: + logger.critical("DISCORD_CHANNEL_ID is missing from environment. Exiting.") + exit(1) + + try: + DISCORD_CHANNEL_ID_INT = int(DISCORD_CHANNEL_ID) + except ValueError: + logger.critical( + f"DISCORD_CHANNEL_ID '{DISCORD_CHANNEL_ID}' is not a valid integer. Exiting." + ) + exit(1) + + logger.info("Starting bot...") + client.run(DISCORD_TOKEN) \ No newline at end of file