Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .clinerules
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,10 @@ Bot:
- Avoid long explanations unless asked

# === GOAL ===
Act like a mentor helping users complete an AOSSIE template repo step-by-step.
Act like a mentor helping users complete an AOSSIE template repo step-by-step.

# === CLARIFYING FLOW FOR OOD/UNSUPPORTED QUERIES ===
If the user's question does not match setup, README, contribute, or error topics:
- Acknowledge that the query does not directly match the standard template tasks.
- Ask a friendly, concise clarifying question to guide them back to one of the supported tasks (setup, README, contributing, or error debugging).
- Under no circumstances should you generate answers outside these core categories.
97 changes: 91 additions & 6 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import json
import logging
import discord
Expand Down Expand Up @@ -104,6 +105,20 @@ async def generate_ollama_response(prompt: str, context: str) -> tuple[str, bool
logger.warning(f"Empty Ollama response (attempt {attempt}/{MAX_RETRIES})")
except httpx.TimeoutException:
logger.error(f"Ollama timed out (attempt {attempt}/{MAX_RETRIES})")
except httpx.HTTPStatusError as e:
logger.error(f"Ollama HTTP error {e.response.status_code} (attempt {attempt}/{MAX_RETRIES}): {e}")
if e.response.status_code == 404:
err_msg = (
f"I'm sorry, the local Ollama model '{OLLAMA_MODEL}' was not found (HTTP 404).\n"
f"Please contact @kpj2006 or run `ollama pull {OLLAMA_MODEL}` on your machine."
)
return err_msg, True
elif 400 <= e.response.status_code < 500:
err_msg = (
f"Local Ollama configuration or client error (HTTP {e.response.status_code}).\n"
f"Details: {e.response.text}"
)
Comment on lines +117 to +120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not return raw Ollama response bodies to end users.

e.response.text can expose backend/internal details in public bot replies. Keep full details in logs; return a sanitized user-facing message.

Suggested patch
             elif 400 <= e.response.status_code < 500:
-                err_msg = (
-                    f"Local Ollama configuration or client error (HTTP {e.response.status_code}).\n"
-                    f"Details: {e.response.text}"
-                )
+                logger.error(
+                    "Local Ollama client/config error HTTP %s: %s",
+                    e.response.status_code,
+                    e.response.text[:500],
+                )
+                err_msg = (
+                    f"Local Ollama configuration or client error (HTTP {e.response.status_code}). "
+                    "Please contact a maintainer."
+                )
                 return err_msg, True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bot.py` around lines 117 - 120, The err_msg variable in the Ollama client
error handling is directly including e.response.text which exposes internal
backend details to end users. Instead, log the full response details
(e.response.text) to the internal logger for debugging purposes, but modify the
err_msg to return only a sanitized, user-friendly error message that does not
include the raw response body details. This way, users see a safe generic error
message while developers can still access the full details in the logs.

return err_msg, True
except httpx.RequestError as e:
logger.error(f"Ollama unreachable (attempt {attempt}/{MAX_RETRIES}): {e}")
except Exception as e:
Expand Down Expand Up @@ -146,6 +161,18 @@ async def _get_or_create_thread(message: discord.Message, channel: discord.TextC
logger.warning(f"Thread {thread.id} is archived/locked — creating a new one")
return None # cannot create thread from message already in a thread

# If the message already has a thread attached to it, fetch and use it
if message.flags.has_thread:
try:
thread = message.guild.get_thread(message.id) if message.guild else None
if not thread:
thread = await client.fetch_channel(message.id)
if isinstance(thread, discord.Thread) and not thread.archived and not thread.locked:
logger.info(f"Reusing existing active thread {thread.id} from message object")
return thread
except Exception as fetch_err:
logger.error(f"Failed to fetch existing thread for message {message.id}: {fetch_err}")

try:
author = message.author
thread = await message.create_thread(
Expand All @@ -157,22 +184,59 @@ async def _get_or_create_thread(message: discord.Message, channel: discord.TextC
except discord.Forbidden:
logger.error(f"Cannot create thread — missing permissions in channel {channel.id}")
except discord.HTTPException as e:
logger.error(f"Discord API error creating thread: {e}")
if e.code == 160004:
logger.info(f"Thread already exists for message {message.id}. Attempting to retrieve it...")
try:
# Thread ID equals the message ID it was created from
thread = message.guild.get_thread(message.id) if message.guild else None
if not thread:
thread = await client.fetch_channel(message.id)
if isinstance(thread, discord.Thread):
logger.info(f"Successfully retrieved existing thread {thread.id}")
return thread
except Exception as fetch_err:
logger.error(f"Failed to fetch existing thread for message {message.id}: {fetch_err}")
else:
logger.error(f"Discord API error creating thread: {e}")
except Exception as e:
logger.error(f"Unexpected error creating thread for {message.author.id}: {e}")
return None


def is_query_covered(query: str) -> bool:
"""Check if the query contains keywords covered in .clinerules using word boundaries."""
q = query.lower()

# Predefined keyword maps based on .clinerules
categories = {
"setup": ["setup", "install", "run", "build", "clone", "docker", "env", "start", "dev server", "npm run dev"],
"readme": ["readme", "read me", "documentation", "project name", "description", "user flow", "feature"],
"contribute": ["contribute", "contributor", "fork", "pr", "pull request", "issue", "branch", "git", "onboarding"],
"error": ["error", "exception", "bug", "fail", "crash", "issue", "logs", "broken", "debug", "not working"]
}

for cat, keywords in categories.items():
for kw in keywords:
# Use raw pattern and re.escape for safety, matching word boundaries for the keyword/phrase
pattern = r'\b' + re.escape(kw) + r'\b'
if re.search(pattern, q):
return True
return False


async def process_message(message: discord.Message):
"""Process a single message: new messages in the main channel spawn a thread,
messages in existing threads continue the conversation there."""
if message.author.bot:
return

is_in_thread = isinstance(message.channel, discord.Thread)
is_in_configured_channel = message.channel.id == DISCORD_CHANNEL_ID_INT
is_in_configured_channel = (
(message.channel.parent_id if is_in_thread else message.channel.id)
== DISCORD_CHANNEL_ID_INT
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if not is_in_thread and not is_in_configured_channel:
if not is_in_configured_channel:
return

author = message.author
Expand All @@ -186,7 +250,7 @@ async def process_message(message: discord.Message):
channel = message.channel
thread = await _get_or_create_thread(message, channel)
if not thread:
_log_gap(message.content, "thread_creation_failed")
await _log_gap(message.content, "thread_creation_failed")
try:
await message.reply(
"I couldn't create a thread to answer your question. Please ask a maintainer for help."
Expand All @@ -212,18 +276,39 @@ async def process_message(message: discord.Message):
else:
full_prompt = message.content

# Check if the query has sufficient information/context based on .clinerules
if not is_query_covered(message.content):
# Pass conversation context explicitly to the LLM so it has thread history for the clarifying question
history_str = f"Previous conversation history:\n{conversation_context}\n\n" if conversation_context else ""
full_prompt = (
f"{history_str}"
f"The user is asking: '{message.content}'. "
f"This query is not covered by the standard guidelines in .clinerules. "
f"Generate a polite response asking the user to clarify if they need help with: "
f"1. Setting up the project template\n"
f"2. Writing or updating the README\n"
f"3. Contributing to the repository\n"
f"4. Debugging an error\n"
f"Keep the response short, friendly, and under 5 lines."
)
await _log_gap(
message.content,
"insufficient_info",
thread_id=thread.id,
)

response_text, used_fallback = await generate_ollama_response(full_prompt, skill_context)

if used_fallback or not skill_context:
_log_gap(
await _log_gap(
message.content,
"ollama_unavailable" if used_fallback else "no_skill_context",
thread_id=thread.id,
)
except Exception as e:
logger.error(f"Unexpected error processing message from {author.name}: {e}")
response_text = "An unexpected error occurred. Please try again or ask a maintainer."
_log_gap(message.content, f"processing_error: {e}", thread_id=thread.id)
await _log_gap(message.content, f"processing_error: {e}", thread_id=thread.id)

if len(response_text) > 1900:
response_text = response_text[:1896] + "..."
Expand Down
Loading