Skip to content

Commit ba49d03

Browse files
authored
Merge pull request #4 from kpj2006/main
feat: add initial bot implementation with environment configuration a…
2 parents bb3294f + c223c9b commit ba49d03

5 files changed

Lines changed: 257 additions & 0 deletions

File tree

.clinerules

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# === ROLE ===
2+
You are an Open Source Mentor Bot helping users work with an AOSSIE project template.
3+
4+
# === PRIMARY BEHAVIOR ===
5+
- Always ask at least 1 clarifying question before answering (unless trivial)
6+
- Guide users step-by-step instead of dumping full answers
7+
- Help fill TODO sections in project templates
8+
9+
# === CONTEXT AWARENESS ===
10+
You are working with a GitHub template repo that includes:
11+
- README with TODO sections (project name, description, user flow)
12+
- Setup instructions (install, run, env config)
13+
- Contribution guidelines
14+
15+
# === QUESTION STRATEGY ===
16+
When user asks something:
17+
1. Ask what exactly they want to build
18+
2. Ask their tech stack (Node / Python / Flutter etc.)
19+
3. Ask their goal (GSoC / learning / hackathon / production)
20+
21+
Examples:
22+
- "What are you building using this template?"
23+
- "Which tech stack are you planning to use?"
24+
- "Is this for GSoC or a personal project?"
25+
26+
# === TASK-SPECIFIC BEHAVIOR ===
27+
28+
## If user says "setup"
29+
- Ask:
30+
- "Which language/framework are you using?"
31+
- Then guide:
32+
- Clone repo
33+
- Install dependencies
34+
- Setup .env
35+
- Run dev server
36+
37+
## If user says "README"
38+
- Ask:
39+
- "What is your project idea?"
40+
- Then help fill:
41+
- Project name
42+
- Description
43+
- User flow
44+
- Features
45+
46+
## If user says "contribute"
47+
- Ask:
48+
- "Are you contributing or creating your own project?"
49+
- Then guide:
50+
- Fork repo
51+
- Create branch
52+
- Follow CONTRIBUTING.md
53+
54+
## If user says "error"
55+
- Ask:
56+
- "What error are you getting?"
57+
- "Share logs/code snippet"
58+
- Then debug step-by-step
59+
60+
# === RESPONSE STYLE ===
61+
- Keep answers short (max 6 lines)
62+
- Prefer bullet points
63+
- Ask → then guide → then suggest next step
64+
65+
# === EXAMPLES ===
66+
67+
User: "help me setup"
68+
Bot:
69+
- "Which tech stack are you using?"
70+
- Then guide setup steps
71+
72+
User: "write README"
73+
Bot:
74+
- "What is your project idea?"
75+
- Then generate structured README
76+
77+
# === RESTRICTIONS ===
78+
- Do not assume missing info
79+
- Always ask before generating full solutions
80+
- Avoid long explanations unless asked
81+
82+
# === GOAL ===
83+
Act like a mentor helping users complete an AOSSIE template repo step-by-step.

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DISCORD_TOKEN=your_discord_bot_token_here
2+
DISCORD_CHANNEL_ID=your_channel_id_here
3+
OLLAMA_MODEL=llama3.2
4+
SKILL_FILE_PATH=.clinerules

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,6 @@ TSWLatexianTemp*
324324
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
325325
# Uncomment the next line to have this generated file ignored.
326326
#*Notes.bib
327+
328+
venv/
329+
.env

bot.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import os
2+
import logging
3+
import discord
4+
import httpx
5+
import asyncio
6+
from dotenv import load_dotenv
7+
8+
# Configure logging
9+
logging.basicConfig(level=logging.INFO)
10+
logger = logging.getLogger('aossie-bot')
11+
12+
# Load environment variables
13+
load_dotenv()
14+
15+
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
16+
DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
17+
DISCORD_CHANNEL_ID_INT = None
18+
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'llama3.2')
19+
SKILL_FILE_PATH = os.getenv('SKILL_FILE_PATH', '.clinerules')
20+
OLLAMA_URL = "http://localhost:11434/api/generate"
21+
22+
# Initialize bot with intents
23+
intents = discord.Intents.default()
24+
intents.message_content = True
25+
client = discord.Client(intents=intents)
26+
27+
# Lock to prevent Ollama requests from clashing
28+
ollama_lock = asyncio.Lock()
29+
30+
def load_skill_context() -> str:
31+
"""Load context from the local skill file."""
32+
try:
33+
if os.path.exists(SKILL_FILE_PATH):
34+
with open(SKILL_FILE_PATH, 'r', encoding='utf-8') as f:
35+
return f.read()
36+
except Exception as e:
37+
logger.error(f"Error loading skill file {SKILL_FILE_PATH}: {e}")
38+
return ""
39+
40+
async def generate_ollama_response(prompt: str, context: str) -> str:
41+
"""Send prompt to local Ollama instance and return the response."""
42+
if context:
43+
system_prompt = f"You are a helpful contributor assistant for AOSSIE.\n\nContext guidelines:\n{context}"
44+
else:
45+
system_prompt = "You are a helpful contributor assistant for AOSSIE."
46+
47+
payload = {
48+
"model": OLLAMA_MODEL,
49+
"prompt": prompt,
50+
"system": system_prompt,
51+
"stream": False
52+
}
53+
54+
try:
55+
async with httpx.AsyncClient(timeout=120.0) as http_client:
56+
response = await http_client.post(OLLAMA_URL, json=payload)
57+
response.raise_for_status()
58+
data = response.json()
59+
return data.get("response", "Error: No response text found in Ollama reply.")
60+
except httpx.TimeoutException:
61+
logger.error("Ollama request timed out.")
62+
return "I'm sorry, the local AI model timed out while thinking. Please try again later."
63+
except httpx.RequestError as e:
64+
logger.error(f"Ollama request error: {e}")
65+
return f"I'm sorry, I couldn't reach the local AI engine. Ensure Ollama is running at localhost:11434."
66+
except Exception as e:
67+
logger.error(f"Unexpected error during Ollama generation: {e}")
68+
return "An unexpected error occurred while generating the response."
69+
70+
async def process_message(message: discord.Message):
71+
"""Process a single message and generate a reply safely."""
72+
if message.author.bot or message.channel.id != DISCORD_CHANNEL_ID_INT:
73+
return
74+
75+
# Use lock to ensure only one message is processed by Ollama at a time
76+
async with ollama_lock:
77+
async with message.channel.typing():
78+
skill_context = load_skill_context()
79+
response_text = await generate_ollama_response(message.content, skill_context)
80+
81+
if len(response_text) > 1900:
82+
response_text = response_text[:1896] + "..."
83+
84+
await message.reply(response_text)
85+
86+
async def wait_for_ollama():
87+
"""Wait until Ollama is up and responding."""
88+
logger.info("Waiting for Ollama to be ready...")
89+
while True:
90+
try:
91+
async with httpx.AsyncClient(timeout=5.0) as http_client:
92+
response = await http_client.get("http://localhost:11434/")
93+
if response.status_code == 200:
94+
logger.info("Ollama is ready!")
95+
return
96+
except httpx.RequestError:
97+
pass
98+
logger.info("Ollama not reachable yet. Retrying in 10 seconds...")
99+
await asyncio.sleep(10)
100+
101+
@client.event
102+
async def on_ready():
103+
logger.info(f"Logged in as {client.user.name} ({client.user.id})")
104+
105+
# Wait for Ollama to be ready before processing the backlog
106+
await wait_for_ollama()
107+
108+
logger.info("Checking for missed messages...")
109+
110+
try:
111+
channel = await client.fetch_channel(DISCORD_CHANNEL_ID_INT)
112+
113+
# Find the last message sent by the bot
114+
last_bot_msg = None
115+
async for msg in channel.history(limit=50):
116+
if msg.author.id == client.user.id:
117+
last_bot_msg = msg
118+
break
119+
120+
messages_to_process = []
121+
if last_bot_msg:
122+
# Fetch messages after the bot's last message
123+
async for msg in channel.history(after=last_bot_msg, oldest_first=True):
124+
if not msg.author.bot:
125+
messages_to_process.append(msg)
126+
else:
127+
# If no bot message found, just process the last 5 user messages
128+
async for msg in channel.history(limit=5, oldest_first=True):
129+
if not msg.author.bot:
130+
messages_to_process.append(msg)
131+
132+
logger.info(f"Found {len(messages_to_process)} missed messages. Processing...")
133+
for msg in messages_to_process:
134+
await process_message(msg)
135+
136+
except Exception as e:
137+
logger.error(f"Error fetching missed messages: {e}")
138+
139+
logger.info("AOSSIE Contributor Assistant MVP is fully ready.")
140+
141+
@client.event
142+
async def on_message(message: discord.Message):
143+
await process_message(message)
144+
145+
if __name__ == "__main__":
146+
if not DISCORD_TOKEN:
147+
logger.critical("DISCORD_TOKEN is missing from environment. Exiting.")
148+
exit(1)
149+
150+
if not DISCORD_CHANNEL_ID:
151+
logger.critical("DISCORD_CHANNEL_ID is missing from environment. Exiting.")
152+
exit(1)
153+
154+
try:
155+
DISCORD_CHANNEL_ID_INT = int(DISCORD_CHANNEL_ID)
156+
except ValueError:
157+
logger.critical(
158+
f"DISCORD_CHANNEL_ID '{DISCORD_CHANNEL_ID}' is not a valid integer. Exiting."
159+
)
160+
exit(1)
161+
162+
logger.info("Starting bot...")
163+
client.run(DISCORD_TOKEN)

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
discord.py==2.3.2
2+
httpx==0.27.0
3+
python-dotenv==1.2.2
4+
idna==3.15

0 commit comments

Comments
 (0)