Chandima Prabhath commited on
Commit
3380d86
·
1 Parent(s): 40babb7

gemini flash image edit

Browse files
app.py CHANGED
@@ -28,7 +28,7 @@ import uvicorn
28
  from FLUX import generate_image
29
  from VoiceReply import generate_voice_reply
30
  from polLLM import generate_llm, LLMBadRequestError
31
- import flux_kontext_lib
32
 
33
  # --- Configuration ---------------------------------------------------------
34
 
@@ -601,7 +601,7 @@ class WhatsAppBot:
601
  else:
602
  raise ValueError(f"Unknown source_type: {source_type}")
603
 
604
- flux_kontext_lib.generate_image(prompt, edit_input_path, download_path=output_path)
605
 
606
  if os.path.exists(output_path):
607
  caption = f"✨ Edited: {prompt}"
 
28
  from FLUX import generate_image
29
  from VoiceReply import generate_voice_reply
30
  from polLLM import generate_llm, LLMBadRequestError
31
+ import gemini_flash_lib
32
 
33
  # --- Configuration ---------------------------------------------------------
34
 
 
601
  else:
602
  raise ValueError(f"Unknown source_type: {source_type}")
603
 
604
+ gemini_flash_lib.generate_image(prompt, edit_input_path, download_path=output_path)
605
 
606
  if os.path.exists(output_path):
607
  caption = f"✨ Edited: {prompt}"
gemini_flash_lib/README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flux Kontext Image Generator Library
2
+
3
+ A Python library for interacting with the Kontext Chat image generation API.
4
+
5
+ ## Installation
6
+ ```bash
7
+ pip install requests
8
+ ```
9
+
10
+ ## Usage
11
+ ```python
12
+ from flux_kontext_lib import generate_image
13
+
14
+ # Using file path
15
+ result = generate_image("close her eyes", "path/to/image.jpg")
16
+
17
+ # Using image bytes
18
+ with open("path/to/image.jpg", "rb") as f:
19
+ image_bytes = f.read()
20
+ result = generate_image("add sunglasses", image_bytes)
21
+
22
+ # Custom headers
23
+ custom_headers = {"Authorization": "Bearer YOUR_TOKEN"}
24
+ result = generate_image("make it sunny", "path/to/image.jpg", headers=custom_headers)
25
+ ```
26
+
27
+ ## Parameters
28
+ - `prompt_text` (str): Text prompt for image modification
29
+ - `image_input` (str or bytes): Image file path or bytes content
30
+ - `headers` (dict, optional): Custom request headers
31
+
32
+ ## Returns
33
+ - dict: API response on success
34
+ - None: On request failure
35
+
36
+ ## Error Handling
37
+ Raises:
38
+ - `FileNotFoundError`: If image file doesn't exist
39
+ - `ValueError`: For unsupported input types
40
+
41
+ ## Example
42
+ See [example_usage.py](example_usage.py) for a complete usage example.
gemini_flash_lib/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .image_generator import generate_image
2
+
3
+ __all__ = ['generate_image']
gemini_flash_lib/example_usage.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flux_kontext_lib import generate_image
2
+
3
+ # Example usage
4
+ if __name__ == '__main__':
5
+ try:
6
+ # Replace with your actual image path
7
+ image_path = "./image.jpg"
8
+ prompt = "close his eyes"
9
+
10
+ # Call the library function
11
+ result = generate_image(prompt, image_path)
12
+
13
+ if result:
14
+ print("API Response:")
15
+ print(result)
16
+ else:
17
+ print("Request failed. Check error messages for details.")
18
+ except Exception as e:
19
+ print(f"Error: {e}")
gemini_flash_lib/image_generator.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import os
4
+ from io import BytesIO
5
+ from typing import Optional, Dict, Any
6
+
7
+ # --- New Dependency ---
8
+ # The google-generativeai library is now required.
9
+ # Please install it using: pip install google-generativeai
10
+ try:
11
+ from google import genai
12
+ from google.genai.types import RawReferenceImage
13
+ from google.genai import types
14
+ except ImportError:
15
+ print("google-generativeai library not found. Please install it using: pip install google-generativeai")
16
+ exit()
17
+
18
+ # Pillow is required for image format conversion and normalization.
19
+ # Please install it using: pip install Pillow
20
+ try:
21
+ from PIL import Image
22
+ except ImportError:
23
+ print("Pillow library not found. Please install it using: pip install Pillow")
24
+ exit()
25
+
26
+ # --- Configuration ---
27
+ # It is recommended to set these as environment variables for security.
28
+ # For ImgBB: used to upload the final image and get a public URL.
29
+ IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")
30
+ # For Google AI: your API key for accessing the Gemini model.
31
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
32
+
33
+ # Configure the Gemini client library
34
+ if GEMINI_API_KEY:
35
+ client = genai.Client(api_key=GEMINI_API_KEY)
36
+
37
+ def upload_to_imgbb(image_path: str, file_name: str) -> Optional[str]:
38
+ """
39
+ Uploads the image at image_path to ImgBB.
40
+ Returns the public URL or None on failure.
41
+ (This function is unchanged from the original script)
42
+ """
43
+ if not IMGBB_API_KEY:
44
+ print("Warning: IMGBB_API_KEY not set, skipping upload to ImgBB.")
45
+ return None
46
+
47
+ # requests is now only used for ImgBB uploads, so we import it here.
48
+ import requests
49
+ print(f"Uploading {file_name} to ImgBB...")
50
+ try:
51
+ with open(image_path, 'rb') as f:
52
+ files = {"image": (file_name, f.read())}
53
+ resp = requests.post(
54
+ "https://api.imgbb.com/1/upload",
55
+ params={"key": IMGBB_API_KEY},
56
+ files=files,
57
+ timeout=30
58
+ )
59
+ resp.raise_for_status()
60
+ data = resp.json().get("data", {})
61
+ url = data.get("url")
62
+ if url:
63
+ print(f"Successfully uploaded to ImgBB: {url}")
64
+ return url
65
+ else:
66
+ print(f"Error: ImgBB API response missing 'url'. Response: {resp.json()}")
67
+ return None
68
+ except requests.exceptions.RequestException as e:
69
+ print(f"Error: ImgBB upload failed: {e}")
70
+ return None
71
+
72
+ def _save_image_bytes(image_bytes: bytes, save_path: str) -> bool:
73
+ """
74
+ Saves raw image bytes to a file.
75
+
76
+ Args:
77
+ image_bytes: The raw bytes of the image data.
78
+ save_path: The local path to save the image.
79
+
80
+ Returns:
81
+ True if saving was successful, False otherwise.
82
+ """
83
+ print(f"Saving generated image to: {save_path}")
84
+ try:
85
+ with open(save_path, 'wb') as f:
86
+ f.write(image_bytes)
87
+ print("Image successfully saved.")
88
+ return True
89
+ except IOError as e:
90
+ print(f"Error saving image file: {e}")
91
+ return False
92
+
93
+
94
+ def generate_image(
95
+ prompt_text: str,
96
+ image_path: str,
97
+ download_path: Optional[str] = None
98
+ ) -> Optional[Dict[str, Any]]:
99
+ """
100
+ Sends a request to the Gemini API using the Python SDK to modify an image,
101
+ optionally saves the result, and uploads it to ImgBB.
102
+
103
+ Args:
104
+ prompt_text: The instructional text for image modification.
105
+ image_path: The file path to the input image (any common format).
106
+ download_path: If provided, the path to save the generated image.
107
+
108
+ Returns:
109
+ A dictionary containing a simplified API response and the ImgBB URL,
110
+ or None on error.
111
+ """
112
+ if not GEMINI_API_KEY:
113
+ print("Error: GEMINI_API_KEY environment variable not set.")
114
+ return None
115
+
116
+ try:
117
+ # --- Image Loading ---
118
+ print(f"Processing image: {image_path}")
119
+ img = Image.open(image_path)
120
+ except FileNotFoundError:
121
+ print(f"Error: Image file not found at {image_path}")
122
+ return None
123
+ except Exception as e:
124
+ print(f"Error processing image file. Ensure it's a valid image. Details: {e}")
125
+ return None
126
+
127
+ try:
128
+ # --- API Call via Python SDK ---
129
+ print("Initializing Gemini model...")
130
+ print("Sending request to Gemini API via Python SDK...")
131
+ # --- Process Gemini API Response ---
132
+ # Convert the PIL Image to bytes (JPEG format)
133
+ img_byte_arr = BytesIO()
134
+ img.save(img_byte_arr, format='JPEG')
135
+ img_bytes = img_byte_arr.getvalue()
136
+
137
+ response = client.models.generate_content(
138
+ model='gemini-2.0-flash-preview-image-generation',
139
+ contents=[
140
+ types.Part.from_text(text=prompt_text),
141
+ types.Part.from_bytes(data=img_bytes, mime_type='image/jpeg'),
142
+ ],
143
+ config=types.GenerateContentConfig(
144
+ response_modalities=["TEXT", "IMAGE"]
145
+ )
146
+ )
147
+ # DEBUG: print the response parts to inspect structure
148
+ # print(response.candidates[0].content.parts)
149
+
150
+ # Extract the image bytes from the response parts
151
+ generated_image_bytes = None
152
+ for part in response.candidates[0].content.parts:
153
+ if hasattr(part, "inline_data") and part.inline_data and hasattr(part.inline_data, "data"):
154
+ generated_image_bytes = part.inline_data.data
155
+ break
156
+ print(generated_image_bytes)
157
+ imgbb_url = None
158
+ # --- Download & Upload Logic ---
159
+ if download_path and generated_image_bytes:
160
+ if _save_image_bytes(generated_image_bytes, download_path):
161
+ # If save is successful, upload the saved file to ImgBB
162
+ file_name = os.path.basename(download_path)
163
+ imgbb_url = upload_to_imgbb(download_path, file_name)
164
+
165
+ # Prepare a final dictionary similar to the original script's output
166
+ final_result = {
167
+ "api_response": {
168
+ "candidates": [{
169
+ "finish_reason": response.candidates[0].finish_reason.name
170
+ }]
171
+ },
172
+ "imgbb_url": imgbb_url
173
+ }
174
+ return final_result
175
+
176
+ except Exception as e:
177
+ print(f"Gemini API request failed: {e}")
178
+ return None
requirements.txt CHANGED
@@ -5,4 +5,5 @@ pillow
5
  requests
6
  supabase
7
  pydantic
8
- python-telegram-bot
 
 
5
  requests
6
  supabase
7
  pydantic
8
+ python-telegram-bot
9
+ google-genai
telebot.py DELETED
@@ -1,496 +0,0 @@
1
- import os
2
- import threading
3
- import requests
4
- import logging
5
- import queue
6
- import json
7
- import asyncio
8
- from typing import List, Optional, Literal
9
- from collections import defaultdict, deque
10
- from concurrent.futures import ThreadPoolExecutor
11
-
12
- from telegram import Update, Message, Bot
13
- from telegram.ext import (
14
- ApplicationBuilder,
15
- ContextTypes,
16
- CommandHandler,
17
- MessageHandler,
18
- filters,
19
- )
20
- from pydantic import BaseModel, Field, ValidationError
21
-
22
- from FLUX import generate_image
23
- from VoiceReply import generate_voice_reply
24
- from polLLM import generate_llm, LLMBadRequestError
25
-
26
- # --- Logging Setup ---------------------------------------------------------
27
-
28
- LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
29
- logger = logging.getLogger("eve_bot")
30
- logger.setLevel(LOG_LEVEL)
31
-
32
- handler = logging.StreamHandler()
33
- formatter = logging.Formatter(
34
- "%(asctime)s [%(levelname)s] [%(chat_id)s/%(user_id)s] %(message)s"
35
- )
36
- handler.setFormatter(formatter)
37
-
38
- class ContextFilter(logging.Filter):
39
- def filter(self, record):
40
- record.chat_id = getattr(record, "chat_id", "-")
41
- record.user_id = getattr(record, "user_id", "-")
42
- return True
43
-
44
- handler.addFilter(ContextFilter())
45
- logger.handlers = [handler]
46
-
47
- # Thread‐local to carry context through helpers
48
- _thread_ctx = threading.local()
49
- def set_thread_context(chat_id, user_id, message_id):
50
- _thread_ctx.chat_id = chat_id
51
- _thread_ctx.user_id = user_id
52
- _thread_ctx.message_id = message_id
53
-
54
- def get_thread_context():
55
- return (
56
- getattr(_thread_ctx, "chat_id", None),
57
- getattr(_thread_ctx, "user_id", None),
58
- getattr(_thread_ctx, "message_id", None),
59
- )
60
-
61
- # --- Conversation History -------------------------------------------------
62
-
63
- history = defaultdict(lambda: deque(maxlen=20))
64
-
65
- def record_user_message(chat_id, user_id, message):
66
- history[(chat_id, user_id)].append(f"User: {message}")
67
-
68
- def record_bot_message(chat_id, user_id, message):
69
- history[(chat_id, user_id)].append(f"Assistant: {message}")
70
-
71
- def get_history_text(chat_id, user_id):
72
- return "\n".join(history[(chat_id, user_id)])
73
-
74
- def clear_history(chat_id, user_id):
75
- history[(chat_id, user_id)].clear()
76
-
77
- # --- Config ---------------------------------------------------------------
78
-
79
- class BotConfig:
80
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
81
- IMAGE_DIR = "/tmp/images"
82
- AUDIO_DIR = "/tmp/audio"
83
- DEFAULT_IMAGE_COUNT = 4
84
-
85
- @classmethod
86
- def validate(cls):
87
- if not cls.TELEGRAM_TOKEN:
88
- raise ValueError("Missing TELEGRAM_TOKEN")
89
-
90
- BotConfig.validate()
91
-
92
- # --- Threading & Queues ---------------------------------------------------
93
-
94
- task_queue = queue.Queue()
95
- polls = {}
96
-
97
- def worker():
98
- while True:
99
- task = task_queue.get()
100
- try:
101
- if task["type"] == "image":
102
- _fn_generate_images(**task)
103
- except Exception as e:
104
- logger.error(f"Worker error {task}: {e}")
105
- finally:
106
- task_queue.task_done()
107
-
108
- for _ in range(4):
109
- threading.Thread(target=worker, daemon=True).start()
110
-
111
- # --- Core Handlers --------------------------------------------------------
112
-
113
- async def _fn_send_text(mid: int, cid: int, text: str, context: ContextTypes.DEFAULT_TYPE):
114
- chat_id, user_id, _ = get_thread_context()
115
- msg: Message = await context.bot.send_message(
116
- chat_id=cid,
117
- text=text,
118
- reply_to_message_id=mid
119
- )
120
- record_bot_message(chat_id, user_id, text)
121
- # enqueue audio reply in async loop
122
- context.application.create_task(_fn_voice_reply_async(
123
- msg.message_id, cid, text, context
124
- ))
125
-
126
- async def _fn_send_text_wrapper(mid, cid, message, context):
127
- await _fn_send_text(mid, cid, message, context)
128
-
129
- async def _fn_voice_reply_async(mid: int, cid: int, prompt: str, context: ContextTypes.DEFAULT_TYPE):
130
- proc = (
131
- f"Just say this exactly as written in a friendly, playful, "
132
- f"happy and helpful but a little bit clumsy-cute way: {prompt}"
133
- )
134
- res = generate_voice_reply(proc, model="openai-audio", voice="coral", audio_dir=BotConfig.AUDIO_DIR)
135
- if res and res[0]:
136
- path, _ = res
137
- with open(path, "rb") as f:
138
- await context.bot.send_audio(chat_id=cid, audio=f, reply_to_message_id=mid)
139
- os.remove(path)
140
- else:
141
- await _fn_send_text(mid, cid, prompt, context)
142
-
143
- async def _fn_summarize(mid, cid, text, context):
144
- summary = generate_llm(f"Summarize:\n\n{text}")
145
- await _fn_send_text(mid, cid, summary, context)
146
-
147
- async def _fn_translate(mid, cid, lang, text, context):
148
- resp = generate_llm(f"Translate to {lang}:\n\n{text}")
149
- await _fn_send_text(mid, cid, resp, context)
150
-
151
- async def _fn_joke(mid, cid, context):
152
- try:
153
- j = requests.get("https://official-joke-api.appspot.com/random_joke", timeout=5).json()
154
- joke = f"{j['setup']}\n\n{j['punchline']}"
155
- except:
156
- joke = generate_llm("Tell me a short joke.")
157
- await _fn_send_text(mid, cid, joke, context)
158
-
159
- async def _fn_weather(mid, cid, loc, context):
160
- raw = requests.get(f"http://sl.wttr.in/{loc}?format=4", timeout=5).text
161
- report = generate_llm(f"Give a weather report in °C:\n\n{raw}")
162
- await _fn_send_text(mid, cid, report, context)
163
-
164
- async def _fn_inspire(mid, cid, context):
165
- quote = generate_llm("Give me a unique, random short inspirational quote.")
166
- await _fn_send_text(mid, cid, f"✨ {quote}", context)
167
-
168
- async def _fn_meme(mid, cid, txt, context):
169
- await context.bot.send_message(chat_id=cid, text="🎨 Generating meme…", reply_to_message_id=mid)
170
- task_queue.put({"type":"image","message_id":mid,"chat_id":cid,"prompt":f"meme: {txt}"})
171
-
172
- async def _fn_poll_create(mid, cid, question, options, context):
173
- votes = {i+1:0 for i in range(len(options))}
174
- polls[cid] = {"question": question, "options": options, "votes": votes, "voters": {}}
175
- text = f"📊 *Poll:* {question}\n" + "\n".join(f"{i+1}. {o}" for i,o in enumerate(options))
176
- await context.bot.send_message(chat_id=cid, text=text, reply_to_message_id=mid, parse_mode="Markdown")
177
- record_bot_message(cid, get_thread_context()[1], text)
178
-
179
- async def _fn_poll_vote(mid, cid, voter, choice, context):
180
- poll = polls.get(cid)
181
- if not poll or choice<1 or choice>len(poll["options"]): return
182
- prev = poll["voters"].get(voter)
183
- if prev: poll["votes"][prev] -= 1
184
- poll["votes"][choice] += 1
185
- poll["voters"][voter] = choice
186
- await _fn_send_text(mid, cid, f"✅ Voted for {poll['options'][choice-1]}", context)
187
-
188
- async def _fn_poll_results(mid, cid, context):
189
- poll = polls.get(cid)
190
- if not poll:
191
- await _fn_send_text(mid, cid, "No active poll.", context)
192
- return
193
- txt = f"📊 *Results:* {poll['question']}\n" + "\n".join(
194
- f"{i}. {o}: {poll['votes'][i]}" for i,o in enumerate(poll["options"],1)
195
- )
196
- await _fn_send_text(mid, cid, txt, context)
197
-
198
- async def _fn_poll_end(mid, cid, context):
199
- poll = polls.pop(cid, None)
200
- if not poll:
201
- await _fn_send_text(mid, cid, "No active poll.", context)
202
- return
203
- txt = f"📊 *Final Results:* {poll['question']}\n" + "\n".join(
204
- f"{i}. {o}: {poll['votes'][i]}" for i,o in enumerate(poll["options"],1)
205
- )
206
- await _fn_send_text(mid, cid, txt, context)
207
-
208
- def _fn_generate_images(message_id, chat_id, prompt, count=1, width=None, height=None, **_):
209
- """
210
- Runs in a background thread. Spins up its own asyncio loop
211
- so we can `await` the bot’s send_message / send_photo coroutines.
212
- """
213
- b = Bot(BotConfig.TELEGRAM_TOKEN)
214
- loop = asyncio.new_event_loop()
215
- asyncio.set_event_loop(loop)
216
- try:
217
- loop.run_until_complete(
218
- b.send_message(
219
- chat_id=chat_id,
220
- text=f"✨ Generating {count} image(s)…",
221
- reply_to_message_id=message_id
222
- )
223
- )
224
- for i in range(1, count+1):
225
- try:
226
- img, path, ret_p, url = generate_image(
227
- prompt, str(message_id), str(message_id),
228
- BotConfig.IMAGE_DIR, width=width, height=height
229
- )
230
- caption = f"✨ Image {i}/{count}: {url}\n\n{ret_p}"
231
- with open(path, "rb") as f:
232
- loop.run_until_complete(
233
- b.send_photo(
234
- chat_id=chat_id,
235
- photo=f,
236
- caption=caption,
237
- reply_to_message_id=message_id
238
- )
239
- )
240
- os.remove(path)
241
- except Exception as e:
242
- logger.warning(f"Img {i}/{count} failed: {e}")
243
- loop.run_until_complete(
244
- b.send_message(
245
- chat_id=chat_id,
246
- text=f"😢 Failed to generate image {i}/{count}.",
247
- reply_to_message_id=message_id
248
- )
249
- )
250
- finally:
251
- loop.close()
252
-
253
- # --- Pydantic Models & Intent Routing ------------------------------------
254
-
255
- class BaseIntent(BaseModel):
256
- action: str
257
-
258
- class SummarizeIntent(BaseIntent):
259
- action: Literal["summarize"]
260
- text: str
261
-
262
- class TranslateIntent(BaseIntent):
263
- action: Literal["translate"]
264
- lang: str
265
- text: str
266
-
267
- class JokeIntent(BaseIntent):
268
- action: Literal["joke"]
269
-
270
- class WeatherIntent(BaseIntent):
271
- action: Literal["weather"]
272
- location: str
273
-
274
- class InspireIntent(BaseIntent):
275
- action: Literal["inspire"]
276
-
277
- class MemeIntent(BaseIntent):
278
- action: Literal["meme"]
279
- text: str
280
-
281
- class PollCreateIntent(BaseIntent):
282
- action: Literal["poll_create"]
283
- question: str
284
- options: List[str]
285
-
286
- class PollVoteIntent(BaseIntent):
287
- action: Literal["poll_vote"]
288
- voter: str
289
- choice: int
290
-
291
- class PollResultsIntent(BaseIntent):
292
- action: Literal["poll_results"]
293
-
294
- class PollEndIntent(BaseIntent):
295
- action: Literal["poll_end"]
296
-
297
- class GenerateImageIntent(BaseModel):
298
- action: Literal["generate_image"]
299
- prompt: str
300
- count: int = Field(default=1, ge=1)
301
- width: Optional[int]
302
- height: Optional[int]
303
-
304
- class SendTextIntent(BaseIntent):
305
- action: Literal["send_text"]
306
- message: str
307
-
308
- INTENT_MODELS = [
309
- SummarizeIntent, TranslateIntent, JokeIntent, WeatherIntent,
310
- InspireIntent, MemeIntent, PollCreateIntent, PollVoteIntent,
311
- PollResultsIntent, PollEndIntent, GenerateImageIntent, SendTextIntent
312
- ]
313
-
314
- async def _fn_enqueue_image(mid, cid, prompt, count, width, height, context):
315
- task_queue.put({
316
- "type":"image",
317
- "message_id": mid,
318
- "chat_id": cid,
319
- "prompt": prompt,
320
- "count": count,
321
- "width": width,
322
- "height": height
323
- })
324
-
325
- ACTION_HANDLERS = {
326
- "summarize": _fn_summarize,
327
- "translate": _fn_translate,
328
- "joke": _fn_joke,
329
- "weather": _fn_weather,
330
- "inspire": _fn_inspire,
331
- "meme": _fn_meme,
332
- "poll_create": _fn_poll_create,
333
- "poll_vote": _fn_poll_vote,
334
- "poll_results": _fn_poll_results,
335
- "poll_end": _fn_poll_end,
336
- "generate_image": _fn_enqueue_image,
337
- "send_text": _fn_send_text_wrapper,
338
- }
339
-
340
- def route_intent(user_input: str, chat_id: str, sender: str):
341
- history_text = get_history_text(chat_id, sender)
342
- sys_prompt = (
343
- "You never perform work yourself—you only invoke one of the available functions. "
344
- "When the user asks for something that matches a function signature, you must return exactly one JSON object matching that function’s parameters—and nothing else. "
345
- "Do not wrap it in markdown, do not add extra text, and do not show the JSON to the user. "
346
- "If the user’s request does not match any function, reply in plain text, and never mention JSON or internal logic.\n\n"
347
- "- summarize(text)\n"
348
- "- translate(lang, text)\n"
349
- "- joke()\n"
350
- "- weather(location)\n"
351
- "- inspire()\n"
352
- "- meme(text)\n"
353
- "- poll_create(question, options)\n"
354
- "- poll_vote(voter, choice)\n"
355
- "- poll_results()\n"
356
- "- poll_end()\n"
357
- "- generate_image(prompt, count, width, height)\n"
358
- "- send_text(message)\n\n"
359
- "Return only raw JSON matching one of these shapes. For example:\n"
360
- " {\"action\":\"generate_image\",\"prompt\":\"a red fox\",\"count\":3,\"width\":512,\"height\":512}\n"
361
- "Otherwise, use send_text to reply with plain chat and you should only return one json for the current message not for previous conversations.\n"
362
- f"Conversation so far:\n{history_text}\n\n current message: User: {user_input}"
363
- )
364
- try:
365
- raw = generate_llm(sys_prompt)
366
- except LLMBadRequestError:
367
- clear_history(chat_id, sender)
368
- return SendTextIntent(action="send_text", message="Oops, let’s start fresh!")
369
- try:
370
- parsed = json.loads(raw)
371
- except json.JSONDecodeError:
372
- return SendTextIntent(action="send_text", message=raw)
373
-
374
- for M in INTENT_MODELS:
375
- try:
376
- return M.model_validate(parsed)
377
- except ValidationError:
378
- continue
379
-
380
- action = parsed.get("action")
381
- if action in ACTION_HANDLERS:
382
- data = parsed
383
- kwargs = {}
384
- if action == "generate_image":
385
- kwargs = {
386
- "prompt": data.get("prompt",""),
387
- "count": int(data.get("count", BotConfig.DEFAULT_IMAGE_COUNT)),
388
- "width": data.get("width"),
389
- "height": data.get("height"),
390
- }
391
- elif action == "send_text":
392
- kwargs = {"message": data.get("message","")}
393
- elif action == "translate":
394
- kwargs = {"lang": data.get("lang",""), "text": data.get("text","")}
395
- elif action == "summarize":
396
- kwargs = {"text": data.get("text","")}
397
- elif action == "weather":
398
- kwargs = {"location": data.get("location","")}
399
- elif action == "meme":
400
- kwargs = {"text": data.get("text","")}
401
- elif action == "poll_create":
402
- kwargs = {"question": data.get("question",""), "options": data.get("options",[])}
403
- elif action == "poll_vote":
404
- kwargs = {"voter": sender, "choice": int(data.get("choice",0))}
405
- try:
406
- model = next(m for m in INTENT_MODELS if getattr(m, "__fields__", {}).get("action").default == action)
407
- return model.model_validate({"action":action, **kwargs})
408
- except Exception:
409
- return SendTextIntent(action="send_text", message=raw)
410
-
411
- return SendTextIntent(action="send_text", message=raw)
412
-
413
- # --- Telegram Handlers ----------------------------------------------------
414
-
415
- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
416
- await update.message.reply_text("🌟 Eve is online! Type /help to see commands.")
417
-
418
- async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
419
- await update.message.reply_markdown(
420
- "🤖 *Eve* commands:\n"
421
- "• /help\n"
422
- "• /summarize <text>\n"
423
- "• /translate <lang>|<text>\n"
424
- "• /joke\n"
425
- "• /weather <loc>\n"
426
- "• /inspire\n"
427
- "• /meme <text>\n"
428
- "• /poll <Q>|opt1|opt2 …\n"
429
- "• /results\n"
430
- "• /endpoll\n"
431
- "• /gen <prompt>|<count>|<width>|<height>\n"
432
- "Otherwise just chat with me."
433
- )
434
-
435
- async def message_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
436
- msg: Message = update.message
437
- chat_id = msg.chat.id
438
- user_id = msg.from_user.id
439
- mid = msg.message_id
440
- text = msg.text or ""
441
-
442
- set_thread_context(chat_id, user_id, mid)
443
- record_user_message(chat_id, user_id, text)
444
-
445
- low = text.lower().strip()
446
- if low == "/help":
447
- return await help_cmd(update, context)
448
- if low.startswith("/summarize "):
449
- return await _fn_summarize(mid, chat_id, text[11:].strip(), context)
450
- if low.startswith("/translate "):
451
- lang, txt = text[11:].split("|",1)
452
- return await _fn_translate(mid, chat_id, lang.strip(), txt.strip(), context)
453
- if low == "/joke":
454
- return await _fn_joke(mid, chat_id, context)
455
- if low.startswith("/weather "):
456
- return await _fn_weather(mid, chat_id, text[9:].strip().replace(" ","+"), context)
457
- if low == "/inspire":
458
- return await _fn_inspire(mid, chat_id, context)
459
- if low.startswith("/meme "):
460
- return await _fn_meme(mid, chat_id, text[6:].strip(), context)
461
- if low.startswith("/poll "):
462
- parts = [p.strip() for p in text[6:].split("|")]
463
- return await _fn_poll_create(mid, chat_id, parts[0], parts[1:], context)
464
- if chat_id in polls and low.isdigit():
465
- return await _fn_poll_vote(mid, chat_id, str(user_id), int(low), context)
466
- if low == "/results":
467
- return await _fn_poll_results(mid, chat_id, context)
468
- if low == "/endpoll":
469
- return await _fn_poll_end(mid, chat_id, context)
470
- if low.startswith("/gen"):
471
- parts = text[4:].split("|")
472
- pr = parts[0].strip()
473
- ct = int(parts[1]) if len(parts)>1 and parts[1].isdigit() else BotConfig.DEFAULT_IMAGE_COUNT
474
- width = int(parts[2]) if len(parts)>2 and parts[2].isdigit() else None
475
- height = int(parts[3]) if len(parts)>3 and parts[3].isdigit() else None
476
- task_queue.put({
477
- "type":"image","message_id":mid,"chat_id":chat_id,
478
- "prompt":pr,"count":ct,"width":width,"height":height
479
- })
480
- return
481
-
482
- intent = route_intent(text, str(chat_id), str(user_id))
483
- handler = ACTION_HANDLERS.get(intent.action)
484
- kwargs = intent.model_dump(exclude={"action"})
485
- await handler(mid, chat_id, **kwargs, context=context)
486
-
487
- def main():
488
- app = ApplicationBuilder().token(BotConfig.TELEGRAM_TOKEN).build()
489
- app.add_handler(CommandHandler("start", start))
490
- app.add_handler(CommandHandler("help", help_cmd))
491
- app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_router))
492
- logger.info("Starting Telegram bot…")
493
- app.run_polling()
494
-
495
- if __name__ == "__main__":
496
- main()