Madras1 commited on
Commit
26dc96c
·
verified ·
1 Parent(s): dfe3a9f

Upload 4 files

Browse files
Files changed (4) hide show
  1. core.py +159 -0
  2. main.py +114 -0
  3. requirements.txt +4 -0
  4. tools.py +109 -0
core.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import re
4
+ import os
5
+ from groq import Groq
6
+ from .tools import get_current_datetime, search_web, ReminderManager
7
+
8
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class TelegramJadeAgent:
12
+ def __init__(self, reminder_manager=None, config_path="telegram_jade/config.json"):
13
+ # Simple config loading
14
+ self.config = {}
15
+ if os.path.exists(config_path):
16
+ with open(config_path) as f:
17
+ self.config = json.load(f)
18
+
19
+ self.model = self.config.get("groq_model", "llama3-70b-8192")
20
+ self.client = Groq(api_key=os.getenv("GROQ_API_KEY"))
21
+
22
+ self.reminder_manager = reminder_manager if reminder_manager else ReminderManager()
23
+
24
+ # We keep history per chat_id in memory for now (could be persisted later)
25
+ self.histories = {}
26
+
27
+ def _get_system_prompt(self):
28
+ current_time = get_current_datetime()
29
+ return f"""You are Jade, a 24h personal assistant on Telegram.
30
+ Current Time: {current_time}
31
+
32
+ Your goal is to be helpful, manage reminders, and search the web when needed.
33
+ You have a personality: friendly, efficient, and slightly witty.
34
+
35
+ AVAILABLE TOOLS:
36
+ You can call tools by responding ONLY with a JSON block in this format:
37
+ {{"tool": "tool_name", "args": {{"arg1": "value1"}}}}
38
+
39
+ Tools:
40
+ 1. get_current_datetime() -> Returns current date/time.
41
+ 2. search_web(query: str) -> Search internet for info.
42
+ 3. schedule_reminder(time_str: str, message: str) -> Schedule a reminder.
43
+ - time_str MUST be in "YYYY-MM-DD HH:MM:SS" format.
44
+ - If the user gives a relative time (e.g., "in 10 mins"), CALCULATE the absolute time based on Current Time.
45
+ 4. list_reminders() -> List pending reminders for the user.
46
+ 5. delete_reminder(reminder_id: int) -> Delete a reminder.
47
+
48
+ RULES:
49
+ - Always check the Current Time before scheduling.
50
+ - If the user asks for a reminder, you MUST calculate the exact "YYYY-MM-DD HH:MM:SS" and use `schedule_reminder`.
51
+ - If you need to answer a question about recent events, use `search_web`.
52
+ - If you are just chatting, reply with plain text.
53
+ """
54
+
55
+ def _get_history(self, chat_id):
56
+ if chat_id not in self.histories:
57
+ self.histories[chat_id] = [{"role": "system", "content": self._get_system_prompt()}]
58
+ else:
59
+ # Update system prompt with fresh time
60
+ self.histories[chat_id][0]["content"] = self._get_system_prompt()
61
+ return self.histories[chat_id]
62
+
63
+ def _process_tool_call(self, response_text):
64
+ try:
65
+ match = re.search(r'\{.*\}', response_text, re.DOTALL)
66
+ if not match:
67
+ return None
68
+ data = json.loads(match.group(0))
69
+ if "tool" in data and "args" in data:
70
+ return data
71
+ except:
72
+ pass
73
+ return None
74
+
75
+ def _run_tool(self, tool_data, chat_id):
76
+ name = tool_data["tool"]
77
+ args = tool_data["args"]
78
+
79
+ logger.info(f"Executing tool {name} for chat {chat_id}")
80
+
81
+ if name == "get_current_datetime":
82
+ return get_current_datetime()
83
+ elif name == "search_web":
84
+ return search_web(args.get("query"))
85
+ elif name == "schedule_reminder":
86
+ # We need to hook this into the actual scheduler in main.py.
87
+ # Ideally, the Agent should return this intent to the Main loop,
88
+ # or the ReminderManager handles the DB and Main loop watches the DB.
89
+ # For simplicity: Agent updates DB via ReminderManager.
90
+ # Main loop (Scheduler) should refresh or listen to changes.
91
+ # BUT: `schedule_reminder` here just updates the JSON.
92
+ # The Scheduler in `main.py` needs to be aware of new jobs.
93
+ # We will return a special signal or just update the DB and assume Main reloads or we return a callback result.
94
+ return self.reminder_manager.add_reminder(chat_id, args.get("time_str"), args.get("message"))
95
+ elif name == "list_reminders":
96
+ return self.reminder_manager.list_reminders(chat_id)
97
+ elif name == "delete_reminder":
98
+ return self.reminder_manager.delete_reminder(chat_id, int(args.get("reminder_id")))
99
+ else:
100
+ return f"Unknown tool: {name}"
101
+
102
+ def chat(self, chat_id, user_input):
103
+ history = self._get_history(chat_id)
104
+ history.append({"role": "user", "content": user_input})
105
+
106
+ # Simple ReAct loop
107
+ for _ in range(5):
108
+ try:
109
+ completion = self.client.chat.completions.create(
110
+ messages=history,
111
+ model=self.model,
112
+ temperature=0.5
113
+ )
114
+ response = completion.choices[0].message.content
115
+ except Exception as e:
116
+ return f"Error calling Groq: {e}"
117
+
118
+ tool_data = self._process_tool_call(response)
119
+
120
+ if tool_data:
121
+ history.append({"role": "assistant", "content": response})
122
+
123
+ tool_result = self._run_tool(tool_data, chat_id)
124
+
125
+ history.append({"role": "system", "content": f"TOOL_RESULT: {tool_result}"})
126
+
127
+ # If it was a reminder scheduling, we might want to inform the caller (main.py)
128
+ # to update the scheduler.
129
+ # For now, we just continue the conversation loop.
130
+ else:
131
+ history.append({"role": "assistant", "content": response})
132
+
133
+ # Limit history size
134
+ if len(history) > 20:
135
+ history = [history[0]] + history[-19:]
136
+ self.histories[chat_id] = history
137
+
138
+ return response
139
+
140
+ return "I'm getting confused. Let's stop here."
141
+
142
+ def generate_reminder_message(self, chat_id, reminder_message):
143
+ """
144
+ Called when a scheduled reminder triggers.
145
+ Generates a friendly notification message.
146
+ """
147
+ prompt = [
148
+ {"role": "system", "content": "You are Jade. It is time to remind the user of something."},
149
+ {"role": "user", "content": f"The reminder is: '{reminder_message}'. Write a friendly message to send to the user now."}
150
+ ]
151
+ try:
152
+ completion = self.client.chat.completions.create(
153
+ messages=prompt,
154
+ model=self.model,
155
+ temperature=0.7
156
+ )
157
+ return completion.choices[0].message.content
158
+ except Exception as e:
159
+ return f"Reminder: {reminder_message}"
main.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import asyncio
4
+ from datetime import datetime
5
+ from telegram import Update
6
+ from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
7
+
8
+ # Import Agent and ReminderManager
9
+ try:
10
+ from .core import TelegramJadeAgent
11
+ from .tools import ReminderManager
12
+ except ImportError:
13
+ from core import TelegramJadeAgent
14
+ from tools import ReminderManager
15
+
16
+ # Logging
17
+ logging.basicConfig(
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ level=logging.INFO
20
+ )
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Global instances
24
+ reminder_manager = ReminderManager()
25
+ agent = TelegramJadeAgent(reminder_manager=reminder_manager)
26
+
27
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
28
+ await context.bot.send_message(chat_id=update.effective_chat.id, text="Hi! I'm Jade, your 24h assistant. How can I help you?")
29
+
30
+ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
31
+ chat_id = update.effective_chat.id
32
+ user_text = update.message.text
33
+
34
+ # 1. Get response from Agent
35
+ # agent.chat is blocking (sync), so we wrap it.
36
+ response = await asyncio.to_thread(agent.chat, chat_id, user_text)
37
+
38
+ await context.bot.send_message(chat_id=chat_id, text=response)
39
+
40
+ async def send_reminder_job(context: ContextTypes.DEFAULT_TYPE):
41
+ """Callback function for the scheduled job."""
42
+ job_data = context.job.data
43
+ chat_id = job_data["chat_id"]
44
+ reminder_message = job_data["message"]
45
+ reminder_id = job_data["id"]
46
+
47
+ # Check if reminder is still valid (not deleted)
48
+ current_reminders = reminder_manager.get_due_reminders()
49
+ if not any(r["id"] == reminder_id for r in current_reminders):
50
+ logger.info(f"Reminder {reminder_id} skipped (deleted or sent).")
51
+ return
52
+
53
+ logger.info(f"Triggering reminder {reminder_id} for chat {chat_id}")
54
+
55
+ # Generate message via Agent
56
+ text = await asyncio.to_thread(agent.generate_reminder_message, chat_id, reminder_message)
57
+
58
+ await context.bot.send_message(chat_id=chat_id, text=text)
59
+
60
+ # Mark as sent
61
+ reminder_manager.mark_as_sent(reminder_id)
62
+
63
+ async def check_reminders_loop(context: ContextTypes.DEFAULT_TYPE):
64
+ """
65
+ Background task running every X seconds to check for new reminders in DB
66
+ and schedule them in the JobQueue.
67
+ """
68
+ pending = reminder_manager.get_due_reminders()
69
+ job_queue = context.job_queue
70
+
71
+ # Get current scheduled jobs by name
72
+ current_jobs = [j.name for j in job_queue.jobs()]
73
+
74
+ for r in pending:
75
+ job_name = f"reminder_{r['id']}"
76
+ if job_name not in current_jobs:
77
+ try:
78
+ run_date = datetime.strptime(r["time"], "%Y-%m-%d %H:%M:%S")
79
+
80
+ # If time is in past, schedule for immediate execution
81
+ if run_date < datetime.now():
82
+ run_date = datetime.now()
83
+
84
+ logger.info(f"Scheduling {job_name} for {run_date}")
85
+
86
+ job_queue.run_once(
87
+ send_reminder_job,
88
+ when=run_date,
89
+ data=r,
90
+ name=job_name
91
+ )
92
+ except Exception as e:
93
+ logger.error(f"Failed to schedule {job_name}: {e}")
94
+
95
+ if __name__ == '__main__':
96
+ token = os.getenv("TELEGRAM_BOT_TOKEN")
97
+ if not token:
98
+ print("Error: TELEGRAM_BOT_TOKEN not found.")
99
+ exit(1)
100
+
101
+ application = ApplicationBuilder().token(token).build()
102
+
103
+ start_handler = CommandHandler('start', start)
104
+ msg_handler = MessageHandler(filters.TEXT & (~filters.COMMAND), handle_message)
105
+
106
+ application.add_handler(start_handler)
107
+ application.add_handler(msg_handler)
108
+
109
+ # Add a repeating job to sync reminders (e.g. every 10 seconds)
110
+ if application.job_queue:
111
+ application.job_queue.run_repeating(check_reminders_loop, interval=10, first=1)
112
+
113
+ print("Jade Telegram Bot is running...")
114
+ application.run_polling()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ python-telegram-bot
2
+ groq
3
+ APScheduler
4
+ duckduckgo-search
tools.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import logging
4
+ from datetime import datetime
5
+ from duckduckgo_search import DDGS
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def get_current_datetime() -> str:
12
+ """Returns the current date and time as a formatted string."""
13
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
14
+
15
+ def search_web(query: str) -> str:
16
+ """Searches the web using DuckDuckGo and returns the top results."""
17
+ try:
18
+ with DDGS() as ddgs:
19
+ results = list(ddgs.text(query, max_results=3))
20
+ if not results:
21
+ return "No results found."
22
+
23
+ formatted_results = []
24
+ for i, res in enumerate(results, 1):
25
+ formatted_results.append(f"{i}. {res['title']}: {res['body']} (URL: {res['href']})")
26
+
27
+ return "\n".join(formatted_results)
28
+ except Exception as e:
29
+ return f"Error searching web: {str(e)}"
30
+
31
+ class ReminderManager:
32
+ def __init__(self, storage_file="telegram_jade/reminders.json"):
33
+ self.storage_file = storage_file
34
+ self.reminders = []
35
+ self._load_reminders()
36
+
37
+ def _load_reminders(self):
38
+ if os.path.exists(self.storage_file):
39
+ try:
40
+ with open(self.storage_file, 'r') as f:
41
+ self.reminders = json.load(f)
42
+ except json.JSONDecodeError:
43
+ logger.error(f"Error decoding {self.storage_file}. Starting with empty reminders.")
44
+ self.reminders = []
45
+ else:
46
+ self.reminders = []
47
+
48
+ def _save_reminders(self):
49
+ # Ensure directory exists
50
+ os.makedirs(os.path.dirname(self.storage_file), exist_ok=True)
51
+ with open(self.storage_file, 'w') as f:
52
+ json.dump(self.reminders, f, indent=2)
53
+
54
+ def add_reminder(self, chat_id: int, time_str: str, message: str):
55
+ """
56
+ Adds a reminder.
57
+ time_str format: "YYYY-MM-DD HH:MM:SS"
58
+ """
59
+ # Generate ID safely
60
+ if self.reminders:
61
+ new_id = max(r["id"] for r in self.reminders) + 1
62
+ else:
63
+ new_id = 1
64
+
65
+ reminder = {
66
+ "id": new_id,
67
+ "chat_id": chat_id,
68
+ "time": time_str,
69
+ "message": message,
70
+ "status": "pending"
71
+ }
72
+ self.reminders.append(reminder)
73
+ self._save_reminders()
74
+ return f"Reminder set for {time_str}: {message}"
75
+
76
+ def list_reminders(self, chat_id: int):
77
+ """Lists pending reminders for a specific chat."""
78
+ user_reminders = [r for r in self.reminders if r["chat_id"] == chat_id and r["status"] == "pending"]
79
+ if not user_reminders:
80
+ return "No pending reminders."
81
+
82
+ result = "Pending Reminders:\n"
83
+ for r in user_reminders:
84
+ result += f"- [{r['time']}] {r['message']} (ID: {r['id']})\n"
85
+ return result
86
+
87
+ def delete_reminder(self, chat_id: int, reminder_id: int):
88
+ """Deletes a reminder by ID."""
89
+ for i, r in enumerate(self.reminders):
90
+ if r["id"] == reminder_id and r["chat_id"] == chat_id:
91
+ del self.reminders[i]
92
+ self._save_reminders()
93
+ return f"Reminder ID {reminder_id} deleted."
94
+ return f"Reminder ID {reminder_id} not found."
95
+
96
+ def get_due_reminders(self):
97
+ """
98
+ Returns a list of reminders that are due now or in the past and mark them as sent?
99
+ Actually, the scheduler will likely handle the trigger logic, but we need a way to
100
+ retrieve 'active' reminders to schedule them on startup.
101
+ """
102
+ return [r for r in self.reminders if r["status"] == "pending"]
103
+
104
+ def mark_as_sent(self, reminder_id: int):
105
+ for r in self.reminders:
106
+ if r["id"] == reminder_id:
107
+ r["status"] = "sent"
108
+ self._save_reminders()
109
+ return