Skip to content

Commit d0eb9ee

Browse files
authored
Merge pull request #442 from father-bot/vision
Vision
2 parents 299fb57 + 2aa7649 commit d0eb9ee

3 files changed

Lines changed: 357 additions & 20 deletions

File tree

bot/bot.py

Lines changed: 189 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import database
3232
import openai_utils
3333

34+
import base64
3435

3536
# setup
3637
db = database.Database()
@@ -177,6 +178,168 @@ async def retry_handle(update: Update, context: CallbackContext):
177178

178179
await message_handle(update, context, message=last_dialog_message["user"], use_new_dialog_timeout=False)
179180

181+
async def _vision_message_handle_fn(
182+
update: Update, context: CallbackContext, use_new_dialog_timeout: bool = True
183+
):
184+
logger.info('_vision_message_handle_fn')
185+
user_id = update.message.from_user.id
186+
current_model = db.get_user_attribute(user_id, "current_model")
187+
188+
if current_model != "gpt-4-vision-preview":
189+
await update.message.reply_text(
190+
"🥲 Images processing is only available for <b>gpt-4-vision-preview</b> model. Please change your settings in /settings",
191+
parse_mode=ParseMode.HTML,
192+
)
193+
return
194+
195+
chat_mode = db.get_user_attribute(user_id, "current_chat_mode")
196+
197+
# new dialog timeout
198+
if use_new_dialog_timeout:
199+
if (datetime.now() - db.get_user_attribute(user_id, "last_interaction")).seconds > config.new_dialog_timeout and len(db.get_dialog_messages(user_id)) > 0:
200+
db.start_new_dialog(user_id)
201+
await update.message.reply_text(f"Starting new dialog due to timeout (<b>{config.chat_modes[chat_mode]['name']}</b> mode) ✅", parse_mode=ParseMode.HTML)
202+
db.set_user_attribute(user_id, "last_interaction", datetime.now())
203+
204+
buf = None
205+
if update.message.effective_attachment:
206+
photo = update.message.effective_attachment[-1]
207+
photo_file = await context.bot.get_file(photo.file_id)
208+
209+
# store file in memory, not on disk
210+
buf = io.BytesIO()
211+
await photo_file.download_to_memory(buf)
212+
buf.name = "image.jpg" # file extension is required
213+
buf.seek(0) # move cursor to the beginning of the buffer
214+
215+
# in case of CancelledError
216+
n_input_tokens, n_output_tokens = 0, 0
217+
218+
try:
219+
# send placeholder message to user
220+
placeholder_message = await update.message.reply_text("...")
221+
message = update.message.caption or update.message.text
222+
223+
# send typing action
224+
await update.message.chat.send_action(action="typing")
225+
226+
if message is None or len(message) == 0:
227+
await update.message.reply_text(
228+
"🥲 You sent <b>empty message</b>. Please, try again!",
229+
parse_mode=ParseMode.HTML,
230+
)
231+
return
232+
233+
dialog_messages = db.get_dialog_messages(user_id, dialog_id=None)
234+
parse_mode = {"html": ParseMode.HTML, "markdown": ParseMode.MARKDOWN}[
235+
config.chat_modes[chat_mode]["parse_mode"]
236+
]
237+
238+
chatgpt_instance = openai_utils.ChatGPT(model=current_model)
239+
if config.enable_message_streaming:
240+
gen = chatgpt_instance.send_vision_message_stream(
241+
message,
242+
dialog_messages=dialog_messages,
243+
image_buffer=buf,
244+
chat_mode=chat_mode,
245+
)
246+
else:
247+
(
248+
answer,
249+
(n_input_tokens, n_output_tokens),
250+
n_first_dialog_messages_removed,
251+
) = await chatgpt_instance.send_vision_message(
252+
message,
253+
dialog_messages=dialog_messages,
254+
image_buffer=buf,
255+
chat_mode=chat_mode,
256+
)
257+
258+
async def fake_gen():
259+
yield "finished", answer, (
260+
n_input_tokens,
261+
n_output_tokens,
262+
), n_first_dialog_messages_removed
263+
264+
gen = fake_gen()
265+
266+
prev_answer = ""
267+
async for gen_item in gen:
268+
(
269+
status,
270+
answer,
271+
(n_input_tokens, n_output_tokens),
272+
n_first_dialog_messages_removed,
273+
) = gen_item
274+
answer = current_model + " " + answer
275+
answer = answer[:4096] # telegram message limit
276+
277+
# update only when 100 new symbols are ready
278+
if abs(len(answer) - len(prev_answer)) < 100 and status != "finished":
279+
continue
280+
281+
try:
282+
await context.bot.edit_message_text(
283+
answer,
284+
chat_id=placeholder_message.chat_id,
285+
message_id=placeholder_message.message_id,
286+
parse_mode=parse_mode,
287+
)
288+
except telegram.error.BadRequest as e:
289+
if str(e).startswith("Message is not modified"):
290+
continue
291+
else:
292+
await context.bot.edit_message_text(
293+
answer,
294+
chat_id=placeholder_message.chat_id,
295+
message_id=placeholder_message.message_id,
296+
)
297+
298+
await asyncio.sleep(0.01) # wait a bit to avoid flooding
299+
300+
prev_answer = answer
301+
302+
# update user data
303+
if buf is not None:
304+
base_image = base64.b64encode(buf.getvalue()).decode("utf-8")
305+
new_dialog_message = {"user": [
306+
{
307+
"type": "text",
308+
"text": message,
309+
},
310+
{
311+
"type": "image",
312+
"image": base_image,
313+
}
314+
]
315+
, "bot": answer, "date": datetime.now()}
316+
else:
317+
new_dialog_message = {"user": [{"type": "text", "text": message}], "bot": answer, "date": datetime.now()}
318+
319+
db.set_dialog_messages(
320+
user_id,
321+
db.get_dialog_messages(user_id, dialog_id=None) + [new_dialog_message],
322+
dialog_id=None
323+
)
324+
325+
db.update_n_used_tokens(user_id, current_model, n_input_tokens, n_output_tokens)
326+
327+
except asyncio.CancelledError:
328+
# note: intermediate token updates only work when enable_message_streaming=True (config.yml)
329+
db.update_n_used_tokens(user_id, current_model, n_input_tokens, n_output_tokens)
330+
raise
331+
332+
except Exception as e:
333+
error_text = f"Something went wrong during completion. Reason: {e}"
334+
logger.error(error_text)
335+
await update.message.reply_text(error_text)
336+
return
337+
338+
async def unsupport_message_handle(update: Update, context: CallbackContext, message=None):
339+
error_text = f"I don't know how to read files or videos. Send the picture in normal mode (Quick Mode)."
340+
logger.error(error_text)
341+
await update.message.reply_text(error_text)
342+
return
180343

181344
async def message_handle(update: Update, context: CallbackContext, message=None, use_new_dialog_timeout=True):
182345
# check if bot was mentioned (for group chats)
@@ -204,6 +367,8 @@ async def message_handle(update: Update, context: CallbackContext, message=None,
204367
await generate_image_handle(update, context, message=message)
205368
return
206369

370+
current_model = db.get_user_attribute(user_id, "current_model")
371+
207372
async def message_handle_fn():
208373
# new dialog timeout
209374
if use_new_dialog_timeout:
@@ -214,7 +379,6 @@ async def message_handle_fn():
214379

215380
# in case of CancelledError
216381
n_input_tokens, n_output_tokens = 0, 0
217-
current_model = db.get_user_attribute(user_id, "current_model")
218382

219383
try:
220384
# send placeholder message to user
@@ -249,11 +413,12 @@ async def fake_gen():
249413
gen = fake_gen()
250414

251415
prev_answer = ""
416+
252417
async for gen_item in gen:
253418
status, answer, (n_input_tokens, n_output_tokens), n_first_dialog_messages_removed = gen_item
254-
419+
answer = current_model + " " + answer
255420
answer = answer[:4096] # telegram message limit
256-
421+
257422
# update only when 100 new symbols are ready
258423
if abs(len(answer) - len(prev_answer)) < 100 and status != "finished":
259424
continue
@@ -267,11 +432,12 @@ async def fake_gen():
267432
await context.bot.edit_message_text(answer, chat_id=placeholder_message.chat_id, message_id=placeholder_message.message_id)
268433

269434
await asyncio.sleep(0.01) # wait a bit to avoid flooding
270-
435+
271436
prev_answer = answer
272-
437+
273438
# update user data
274439
new_dialog_message = {"user": _message, "bot": answer, "date": datetime.now()}
440+
275441
db.set_dialog_messages(
276442
user_id,
277443
db.get_dialog_messages(user_id, dialog_id=None) + [new_dialog_message],
@@ -300,7 +466,19 @@ async def fake_gen():
300466
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
301467

302468
async with user_semaphores[user_id]:
303-
task = asyncio.create_task(message_handle_fn())
469+
if current_model == "gpt-4-vision-preview" or update.message.photo is not None and len(update.message.photo) > 0:
470+
logger.error('gpt-4-vision-preview')
471+
if current_model != "gpt-4-vision-preview":
472+
current_model = "gpt-4-vision-preview"
473+
db.set_user_attribute(user_id, "current_model", "gpt-4-vision-preview")
474+
task = asyncio.create_task(
475+
_vision_message_handle_fn(update, context, use_new_dialog_timeout=use_new_dialog_timeout)
476+
)
477+
else:
478+
task = asyncio.create_task(
479+
message_handle_fn()
480+
)
481+
304482
user_tasks[user_id] = task
305483

306484
try:
@@ -392,6 +570,7 @@ async def new_dialog_handle(update: Update, context: CallbackContext):
392570

393571
user_id = update.message.from_user.id
394572
db.set_user_attribute(user_id, "last_interaction", datetime.now())
573+
db.set_user_attribute(user_id, "current_model", "gpt-3.5-turbo")
395574

396575
db.start_new_dialog(user_id)
397576
await update.message.reply_text("Starting new dialog ✅")
@@ -672,6 +851,9 @@ def run_bot() -> None:
672851
application.add_handler(CommandHandler("help_group_chat", help_group_chat_handle, filters=user_filter))
673852

674853
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND & user_filter, message_handle))
854+
application.add_handler(MessageHandler(filters.PHOTO & ~filters.COMMAND & user_filter, message_handle))
855+
application.add_handler(MessageHandler(filters.VIDEO & ~filters.COMMAND & user_filter, unsupport_message_handle))
856+
application.add_handler(MessageHandler(filters.Document.ALL & ~filters.COMMAND & user_filter, unsupport_message_handle))
675857
application.add_handler(CommandHandler("retry", retry_handle, filters=user_filter))
676858
application.add_handler(CommandHandler("new", new_dialog_handle, filters=user_filter))
677859
application.add_handler(CommandHandler("cancel", cancel_handle, filters=user_filter))
@@ -694,4 +876,4 @@ def run_bot() -> None:
694876

695877

696878
if __name__ == "__main__":
697-
run_bot()
879+
run_bot()

0 commit comments

Comments
 (0)