Madras1 commited on
Commit
32f0aea
·
verified ·
1 Parent(s): ed918db

Upload 7 files

Browse files
Files changed (7) hide show
  1. README.md +104 -12
  2. app.py +115 -0
  3. core.py +174 -0
  4. hf_storage.py +136 -0
  5. main.py +114 -0
  6. requirements.txt +5 -0
  7. tools.py +106 -0
README.md CHANGED
@@ -1,12 +1,104 @@
1
- ---
2
- title: Jade Assistant
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.2.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Jade Personal Assistant
3
+ emoji: 🌿
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # 🌿 Jade - 24h Personal Assistant
13
+
14
+ Jade is a Telegram bot powered by **Hermes 4 405B** (via Chutes.ai) that acts as your personal assistant.
15
+
16
+ ## Features
17
+
18
+ - 💬 **Natural conversation** - Chat naturally with Jade
19
+ - ⏰ **Reminders** - Set reminders that persist across restarts
20
+ - 🔍 **Web search** - Ask Jade to search the internet
21
+ - 📅 **Time awareness** - Jade knows the current date/time
22
+
23
+ ## Setup on HuggingFace Spaces
24
+
25
+ ### 1. Create a new Space
26
+
27
+ 1. Go to [huggingface.co/new-space](https://huggingface.co/new-space)
28
+ 2. Choose **Gradio** as the SDK
29
+ 3. Select **Free** tier (CPU basic)
30
+
31
+ ### 2. Configure Secrets
32
+
33
+ Go to **Settings > Repository secrets** and add:
34
+
35
+ | Secret Name | Description |
36
+ |------------|-------------|
37
+ | `TELEGRAM_BOT_TOKEN` | Your Telegram bot token from @BotFather |
38
+ | `CHUTES_API_KEY` | Your Chutes.ai API key |
39
+ | `HF_TOKEN` | (Optional) Your HuggingFace token for persistent storage |
40
+ | `HF_STORAGE_REPO` | (Optional) Dataset repo for storage, e.g., `username/jade-data` |
41
+
42
+ ### 3. Upload Files
43
+
44
+ Upload all files from this directory to your Space:
45
+ - `app.py` (entry point)
46
+ - `main.py`
47
+ - `core.py`
48
+ - `tools.py`
49
+ - `hf_storage.py`
50
+ - `requirements.txt`
51
+
52
+ ### 4. Create Storage Dataset (Optional)
53
+
54
+ For persistent reminders across Space restarts:
55
+
56
+ 1. Create a new private dataset at [huggingface.co/new-dataset](https://huggingface.co/new-dataset)
57
+ 2. Name it something like `jade-data`
58
+ 3. Set `HF_STORAGE_REPO` secret to `yourusername/jade-data`
59
+ 4. Make sure `HF_TOKEN` has write access to the dataset
60
+
61
+ ## Usage
62
+
63
+ 1. Find your bot on Telegram
64
+ 2. Send `/start` to begin
65
+ 3. Chat naturally!
66
+
67
+ ### Example commands:
68
+ - "Remind me to call mom in 30 minutes"
69
+ - "What's the weather in São Paulo?"
70
+ - "What time is it?"
71
+ - "List my reminders"
72
+
73
+ ## ⚠️ Important Notes
74
+
75
+ - **Free tier hibernation**: The Space may sleep after ~15 min of inactivity. Reminders scheduled during sleep may be delayed.
76
+ - **Wakeup**: Just access the Space URL or send a message to wake it up.
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ ┌────────────────────────────────────────────┐
82
+ │ HuggingFace Space │
83
+ │ ┌──────────────┐ ┌─────────────────┐ │
84
+ │ │ Gradio │ │ Telegram Bot │ │
85
+ │ │ (Status UI) │ │ (main.py) │ │
86
+ │ └──────────────┘ └────────┬────────┘ │
87
+ │ │ │
88
+ │ ┌────────▼────────┐ │
89
+ │ │ Jade Agent │ │
90
+ │ │ (core.py) │ │
91
+ │ └────────┬────────┘ │
92
+ │ │ │
93
+ │ ┌──────────────────────┼───────┐ │
94
+ │ ▼ ▼ ▼ │
95
+ │ ┌──────────┐ ┌──────────┐ ┌───┐ │
96
+ │ │ Chutes.ai│ │HF Storage│ │DDG│ │
97
+ │ │(Hermes4) │ │(Dataset) │ │Web│ │
98
+ │ └──────────┘ └──────────┘ └───┘ │
99
+ └────────────────────────────────────────────┘
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
app.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Spaces Entry Point for Jade Bot
3
+
4
+ This creates a minimal Gradio interface alongside the Telegram bot.
5
+ The Gradio interface helps keep the Space awake and provides a status page.
6
+ """
7
+ import os
8
+ import sys
9
+ import logging
10
+ import threading
11
+ import gradio as gr
12
+ from datetime import datetime
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ level=logging.INFO
18
+ )
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Bot status tracking
22
+ bot_status = {
23
+ "started_at": None,
24
+ "last_activity": None,
25
+ "messages_processed": 0,
26
+ "is_running": False
27
+ }
28
+
29
+
30
+ def run_telegram_bot():
31
+ """Run the Telegram bot in a separate thread."""
32
+ global bot_status
33
+
34
+ try:
35
+ # Import here to avoid circular imports
36
+ from main import application, reminder_manager, agent
37
+
38
+ bot_status["started_at"] = datetime.now().isoformat()
39
+ bot_status["is_running"] = True
40
+
41
+ logger.info("Starting Telegram bot...")
42
+ application.run_polling()
43
+
44
+ except Exception as e:
45
+ logger.error(f"Bot error: {e}")
46
+ bot_status["is_running"] = False
47
+
48
+
49
+ def get_status():
50
+ """Return current bot status as formatted string."""
51
+ if not bot_status["is_running"]:
52
+ return "🔴 Bot is not running"
53
+
54
+ status_text = f"""
55
+ ## 🟢 Jade Bot Status
56
+
57
+ - **Started at**: {bot_status.get('started_at', 'N/A')}
58
+ - **Last activity**: {bot_status.get('last_activity', 'N/A')}
59
+ - **Status**: Running ✅
60
+
61
+ ### Configuration
62
+ - **Model**: NousResearch/Hermes-4-405B-FP8-TEE
63
+ - **API**: Chutes.ai
64
+ - **Persistence**: HuggingFace Datasets
65
+
66
+ ---
67
+ *Refresh this page to update status*
68
+ """
69
+ return status_text
70
+
71
+
72
+ def create_gradio_interface():
73
+ """Create the Gradio status interface."""
74
+ with gr.Blocks(title="Jade Bot Status", theme=gr.themes.Soft()) as demo:
75
+ gr.Markdown("# 🌿 Jade - 24h Personal Assistant")
76
+ gr.Markdown("This is the status page for Jade, your Telegram assistant.")
77
+
78
+ status_display = gr.Markdown(get_status)
79
+
80
+ refresh_btn = gr.Button("🔄 Refresh Status")
81
+ refresh_btn.click(fn=get_status, outputs=status_display)
82
+
83
+ gr.Markdown("""
84
+ ### How to use
85
+ 1. Find Jade on Telegram (your bot username)
86
+ 2. Send `/start` to begin
87
+ 3. Chat naturally - Jade can set reminders, search the web, and more!
88
+ """)
89
+
90
+ return demo
91
+
92
+
93
+ def main():
94
+ """Main entry point."""
95
+ # Check required environment variables
96
+ required_vars = ["TELEGRAM_BOT_TOKEN", "CHUTES_API_KEY"]
97
+ missing = [v for v in required_vars if not os.getenv(v)]
98
+
99
+ if missing:
100
+ logger.error(f"Missing required environment variables: {missing}")
101
+ logger.error("Please set these in HuggingFace Space Secrets")
102
+ sys.exit(1)
103
+
104
+ # Start Telegram bot in background thread
105
+ bot_thread = threading.Thread(target=run_telegram_bot, daemon=True)
106
+ bot_thread.start()
107
+ logger.info("Bot thread started")
108
+
109
+ # Create and launch Gradio interface
110
+ demo = create_gradio_interface()
111
+ demo.launch(server_name="0.0.0.0", server_port=7860)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
core.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import re
4
+ import os
5
+ from openai import OpenAI
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
+ # Chutes.ai API with Hermes 405B model
20
+ self.model = self.config.get("model", "NousResearch/Hermes-4-405B-FP8-TEE")
21
+ self.client = OpenAI(
22
+ api_key=os.getenv("CHUTES_API_KEY"),
23
+ base_url="https://api.chutes.ai/v1"
24
+ )
25
+
26
+ self.reminder_manager = reminder_manager if reminder_manager else ReminderManager()
27
+
28
+ # Initialize storage for conversation history
29
+ try:
30
+ from .hf_storage import get_storage
31
+ except ImportError:
32
+ from hf_storage import get_storage
33
+ self.storage = get_storage()
34
+
35
+ # Load persisted conversation history
36
+ self.histories = self.storage.load_json("conversation_history.json", default={})
37
+
38
+ def _get_system_prompt(self):
39
+ current_time = get_current_datetime()
40
+ return f"""You are Jade, a 24h personal assistant on Telegram.
41
+ Current Time: {current_time}
42
+
43
+ Your goal is to be helpful, manage reminders, and search the web when needed.
44
+ You have a personality: friendly, efficient, and slightly witty.
45
+
46
+ AVAILABLE TOOLS:
47
+ You can call tools by responding ONLY with a JSON block in this format:
48
+ {{"tool": "tool_name", "args": {{"arg1": "value1"}}}}
49
+
50
+ Tools:
51
+ 1. get_current_datetime() -> Returns current date/time.
52
+ 2. search_web(query: str) -> Search internet for info.
53
+ 3. schedule_reminder(time_str: str, message: str) -> Schedule a reminder.
54
+ - time_str MUST be in "YYYY-MM-DD HH:MM:SS" format.
55
+ - If the user gives a relative time (e.g., "in 10 mins"), CALCULATE the absolute time based on Current Time.
56
+ 4. list_reminders() -> List pending reminders for the user.
57
+ 5. delete_reminder(reminder_id: int) -> Delete a reminder.
58
+
59
+ RULES:
60
+ - Always check the Current Time before scheduling.
61
+ - If the user asks for a reminder, you MUST calculate the exact "YYYY-MM-DD HH:MM:SS" and use `schedule_reminder`.
62
+ - If you need to answer a question about recent events, use `search_web`.
63
+ - If you are just chatting, reply with plain text.
64
+ """
65
+
66
+ def _get_history(self, chat_id):
67
+ chat_id_str = str(chat_id) # JSON keys must be strings
68
+ if chat_id_str not in self.histories:
69
+ self.histories[chat_id_str] = [{"role": "system", "content": self._get_system_prompt()}]
70
+ else:
71
+ # Update system prompt with fresh time
72
+ self.histories[chat_id_str][0]["content"] = self._get_system_prompt()
73
+ return self.histories[chat_id_str]
74
+
75
+ def _process_tool_call(self, response_text):
76
+ try:
77
+ match = re.search(r'\{.*\}', response_text, re.DOTALL)
78
+ if not match:
79
+ return None
80
+ data = json.loads(match.group(0))
81
+ if "tool" in data and "args" in data:
82
+ return data
83
+ except:
84
+ pass
85
+ return None
86
+
87
+ def _run_tool(self, tool_data, chat_id):
88
+ name = tool_data["tool"]
89
+ args = tool_data["args"]
90
+
91
+ logger.info(f"Executing tool {name} for chat {chat_id}")
92
+
93
+ if name == "get_current_datetime":
94
+ return get_current_datetime()
95
+ elif name == "search_web":
96
+ return search_web(args.get("query"))
97
+ elif name == "schedule_reminder":
98
+ # We need to hook this into the actual scheduler in main.py.
99
+ # Ideally, the Agent should return this intent to the Main loop,
100
+ # or the ReminderManager handles the DB and Main loop watches the DB.
101
+ # For simplicity: Agent updates DB via ReminderManager.
102
+ # Main loop (Scheduler) should refresh or listen to changes.
103
+ # BUT: `schedule_reminder` here just updates the JSON.
104
+ # The Scheduler in `main.py` needs to be aware of new jobs.
105
+ # We will return a special signal or just update the DB and assume Main reloads or we return a callback result.
106
+ return self.reminder_manager.add_reminder(chat_id, args.get("time_str"), args.get("message"))
107
+ elif name == "list_reminders":
108
+ return self.reminder_manager.list_reminders(chat_id)
109
+ elif name == "delete_reminder":
110
+ return self.reminder_manager.delete_reminder(chat_id, int(args.get("reminder_id")))
111
+ else:
112
+ return f"Unknown tool: {name}"
113
+
114
+ def chat(self, chat_id, user_input):
115
+ history = self._get_history(chat_id)
116
+ history.append({"role": "user", "content": user_input})
117
+
118
+ # Simple ReAct loop
119
+ for _ in range(5):
120
+ try:
121
+ completion = self.client.chat.completions.create(
122
+ messages=history,
123
+ model=self.model,
124
+ temperature=0.5
125
+ )
126
+ response = completion.choices[0].message.content
127
+ except Exception as e:
128
+ return f"Error calling Chutes.ai API: {e}"
129
+
130
+ tool_data = self._process_tool_call(response)
131
+
132
+ if tool_data:
133
+ history.append({"role": "assistant", "content": response})
134
+
135
+ tool_result = self._run_tool(tool_data, chat_id)
136
+
137
+ history.append({"role": "system", "content": f"TOOL_RESULT: {tool_result}"})
138
+
139
+ # If it was a reminder scheduling, we might want to inform the caller (main.py)
140
+ # to update the scheduler.
141
+ # For now, we just continue the conversation loop.
142
+ else:
143
+ history.append({"role": "assistant", "content": response})
144
+
145
+ # Limit history size
146
+ if len(history) > 20:
147
+ history = [history[0]] + history[-19:]
148
+ self.histories[str(chat_id)] = history
149
+
150
+ # Persist conversation history
151
+ self.storage.save_json("conversation_history.json", self.histories)
152
+
153
+ return response
154
+
155
+ return "I'm getting confused. Let's stop here."
156
+
157
+ def generate_reminder_message(self, chat_id, reminder_message):
158
+ """
159
+ Called when a scheduled reminder triggers.
160
+ Generates a friendly notification message.
161
+ """
162
+ prompt = [
163
+ {"role": "system", "content": "You are Jade. It is time to remind the user of something."},
164
+ {"role": "user", "content": f"The reminder is: '{reminder_message}'. Write a friendly message to send to the user now."}
165
+ ]
166
+ try:
167
+ completion = self.client.chat.completions.create(
168
+ messages=prompt,
169
+ model=self.model,
170
+ temperature=0.7
171
+ )
172
+ return completion.choices[0].message.content
173
+ except Exception as e:
174
+ return f"Reminder: {reminder_message}"
hf_storage.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Datasets Storage Module
3
+
4
+ Provides persistent storage for Jade bot data using HuggingFace Datasets.
5
+ This allows data to persist across Space restarts.
6
+ """
7
+ import os
8
+ import json
9
+ import logging
10
+ from typing import Optional, Dict, Any
11
+ from datetime import datetime
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Try to import huggingface_hub, fallback to local storage if not available
16
+ try:
17
+ from huggingface_hub import HfApi, hf_hub_download, upload_file
18
+ HF_AVAILABLE = True
19
+ except ImportError:
20
+ HF_AVAILABLE = False
21
+ logger.warning("huggingface_hub not installed. Using local storage only.")
22
+
23
+
24
+ class HFStorage:
25
+ """
26
+ Storage class that persists data to HuggingFace Hub.
27
+ Falls back to local JSON files if HF is not configured.
28
+ """
29
+
30
+ def __init__(self, repo_id: Optional[str] = None, local_dir: str = "jade_data"):
31
+ """
32
+ Initialize storage.
33
+
34
+ Args:
35
+ repo_id: HuggingFace dataset repo ID (e.g., "username/jade-data")
36
+ If None, will try to get from HF_STORAGE_REPO env var.
37
+ local_dir: Local directory for fallback storage
38
+ """
39
+ self.repo_id = repo_id or os.getenv("HF_STORAGE_REPO")
40
+ self.local_dir = local_dir
41
+ self.token = os.getenv("HF_TOKEN")
42
+
43
+ # Create local dir if needed
44
+ os.makedirs(self.local_dir, exist_ok=True)
45
+
46
+ # Check if HF storage is properly configured
47
+ self.use_hf = HF_AVAILABLE and self.repo_id and self.token
48
+
49
+ if self.use_hf:
50
+ self.api = HfApi(token=self.token)
51
+ logger.info(f"HF Storage initialized with repo: {self.repo_id}")
52
+ else:
53
+ logger.info("Using local storage (HF not configured)")
54
+
55
+ def _local_path(self, filename: str) -> str:
56
+ """Get local file path for a data file."""
57
+ return os.path.join(self.local_dir, filename)
58
+
59
+ def load_json(self, filename: str, default: Any = None) -> Any:
60
+ """
61
+ Load JSON data from storage.
62
+
63
+ First tries HF Hub, falls back to local file.
64
+ """
65
+ if default is None:
66
+ default = {}
67
+
68
+ # Try HF Hub first
69
+ if self.use_hf:
70
+ try:
71
+ local_file = hf_hub_download(
72
+ repo_id=self.repo_id,
73
+ filename=filename,
74
+ repo_type="dataset",
75
+ token=self.token
76
+ )
77
+ with open(local_file, 'r', encoding='utf-8') as f:
78
+ return json.load(f)
79
+ except Exception as e:
80
+ logger.debug(f"Could not load {filename} from HF: {e}")
81
+
82
+ # Fallback to local
83
+ local_path = self._local_path(filename)
84
+ if os.path.exists(local_path):
85
+ try:
86
+ with open(local_path, 'r', encoding='utf-8') as f:
87
+ return json.load(f)
88
+ except json.JSONDecodeError:
89
+ logger.error(f"Error decoding {local_path}")
90
+
91
+ return default
92
+
93
+ def save_json(self, filename: str, data: Any) -> bool:
94
+ """
95
+ Save JSON data to storage.
96
+
97
+ Saves locally first, then uploads to HF Hub.
98
+ """
99
+ local_path = self._local_path(filename)
100
+
101
+ # Always save locally first
102
+ try:
103
+ with open(local_path, 'w', encoding='utf-8') as f:
104
+ json.dump(data, f, indent=2, ensure_ascii=False)
105
+ except Exception as e:
106
+ logger.error(f"Error saving {filename} locally: {e}")
107
+ return False
108
+
109
+ # Upload to HF Hub if configured
110
+ if self.use_hf:
111
+ try:
112
+ upload_file(
113
+ path_or_fileobj=local_path,
114
+ path_in_repo=filename,
115
+ repo_id=self.repo_id,
116
+ repo_type="dataset",
117
+ token=self.token
118
+ )
119
+ logger.debug(f"Uploaded {filename} to HF Hub")
120
+ except Exception as e:
121
+ logger.warning(f"Could not upload {filename} to HF: {e}")
122
+ # Still return True since local save worked
123
+
124
+ return True
125
+
126
+
127
+ # Global storage instance
128
+ _storage: Optional[HFStorage] = None
129
+
130
+
131
+ def get_storage() -> HFStorage:
132
+ """Get the global storage instance."""
133
+ global _storage
134
+ if _storage is None:
135
+ _storage = HFStorage()
136
+ return _storage
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,5 @@
 
 
 
 
 
 
1
+ python-telegram-bot[job-queue]
2
+ openai
3
+ duckduckgo-search
4
+ huggingface_hub
5
+ gradio
tools.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=None):
33
+ # Use HFStorage for persistence
34
+ if storage is None:
35
+ try:
36
+ from .hf_storage import get_storage
37
+ except ImportError:
38
+ from hf_storage import get_storage
39
+ storage = get_storage()
40
+
41
+ self.storage = storage
42
+ self.reminders = []
43
+ self._load_reminders()
44
+
45
+ def _load_reminders(self):
46
+ self.reminders = self.storage.load_json("reminders.json", default=[])
47
+
48
+ def _save_reminders(self):
49
+ self.storage.save_json("reminders.json", self.reminders)
50
+
51
+ def add_reminder(self, chat_id: int, time_str: str, message: str):
52
+ """
53
+ Adds a reminder.
54
+ time_str format: "YYYY-MM-DD HH:MM:SS"
55
+ """
56
+ # Generate ID safely
57
+ if self.reminders:
58
+ new_id = max(r["id"] for r in self.reminders) + 1
59
+ else:
60
+ new_id = 1
61
+
62
+ reminder = {
63
+ "id": new_id,
64
+ "chat_id": chat_id,
65
+ "time": time_str,
66
+ "message": message,
67
+ "status": "pending"
68
+ }
69
+ self.reminders.append(reminder)
70
+ self._save_reminders()
71
+ return f"Reminder set for {time_str}: {message}"
72
+
73
+ def list_reminders(self, chat_id: int):
74
+ """Lists pending reminders for a specific chat."""
75
+ user_reminders = [r for r in self.reminders if r["chat_id"] == chat_id and r["status"] == "pending"]
76
+ if not user_reminders:
77
+ return "No pending reminders."
78
+
79
+ result = "Pending Reminders:\n"
80
+ for r in user_reminders:
81
+ result += f"- [{r['time']}] {r['message']} (ID: {r['id']})\n"
82
+ return result
83
+
84
+ def delete_reminder(self, chat_id: int, reminder_id: int):
85
+ """Deletes a reminder by ID."""
86
+ for i, r in enumerate(self.reminders):
87
+ if r["id"] == reminder_id and r["chat_id"] == chat_id:
88
+ del self.reminders[i]
89
+ self._save_reminders()
90
+ return f"Reminder ID {reminder_id} deleted."
91
+ return f"Reminder ID {reminder_id} not found."
92
+
93
+ def get_due_reminders(self):
94
+ """
95
+ Returns a list of reminders that are due now or in the past and mark them as sent?
96
+ Actually, the scheduler will likely handle the trigger logic, but we need a way to
97
+ retrieve 'active' reminders to schedule them on startup.
98
+ """
99
+ return [r for r in self.reminders if r["status"] == "pending"]
100
+
101
+ def mark_as_sent(self, reminder_id: int):
102
+ for r in self.reminders:
103
+ if r["id"] == reminder_id:
104
+ r["status"] = "sent"
105
+ self._save_reminders()
106
+ return