diff --git a/telegram-agy-bot/agy_engine.py b/telegram-agy-bot/agy_engine.py index 6ef1a9b..043809e 100644 --- a/telegram-agy-bot/agy_engine.py +++ b/telegram-agy-bot/agy_engine.py @@ -1543,6 +1543,21 @@ class AGYEngine: logger.debug(f"Post-prompt git commit skipped: {ce}") text_out = final_response if final_response else "".join(collected_tokens) + if not text_out.strip() and session.conversation_id: + try: + rec = get_conversation_last_output(session.conversation_id) + if rec.get("text"): + text_out = rec["text"] + elif rec.get("tools"): + is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") + tool_bullets = "\n".join(f"• {t}" for t in rec["tools"][-10:]) + if is_fa: + text_out = f"✅ **دستورات و ابزارهای زیر با موفقیت اجرا شدند:**\n\n{tool_bullets}" + else: + text_out = f"✅ **The following tools and operations executed successfully:**\n\n{tool_bullets}" + except Exception as ex: + logger.debug(f"Fallback transcript lookup failed: {ex}") + return AgentResult( text=text_out, usage=last_usage, diff --git a/telegram-agy-bot/bot.py b/telegram-agy-bot/bot.py index b9d2be9..55604b3 100644 --- a/telegram-agy-bot/bot.py +++ b/telegram-agy-bot/bot.py @@ -6318,28 +6318,40 @@ async def process_agent_turn( thinking_msg = f"💭 در حال تفکر روی پروژه {escape_html(curr_proj.name)}... ▌" if is_fa else f"💭 Thinking on project {escape_html(curr_proj.name)}... ▌" full_html = proj_header + "\n\n" + thinking_msg - if len(full_html) <= settings.max_message_length: - try: - await status_msg.edit_text( - full_html, - parse_mode=constants.ParseMode.HTML, - disable_web_page_preview=True, - reply_markup=get_stop_button(session.language) if not final else None, - ) - except BadRequest as e: - if "Message is not modified" not in str(e): - try: - await status_msg.edit_text( - escape_html(raw_text[:3800]), - parse_mode=constants.ParseMode.HTML, - reply_markup=get_stop_button(session.language) if not final else None, - ) - except Exception: - pass - except RetryAfter as e: - await asyncio.sleep(e.retry_after) - except Exception as e: - logger.debug(f"Streaming edit skipped: {e}") + if len(full_html) > settings.max_message_length: + suffix_str = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else "" + available_for_body = max(200, settings.max_message_length - len(suffix_str) - 60) + trimmed_raw = raw_text[:available_for_body] + formatted_body = markdown_to_telegram_html(trimmed_raw) + more_indicator = ("...\n(ادامه در پیام بعدی...)" if is_fa else "...\n(continues...)") + full_html = formatted_body + more_indicator + suffix_str + if len(full_html) > settings.max_message_length: + full_html = full_html[:settings.max_message_length - 10] + "..." + + try: + await status_msg.edit_text( + full_html, + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=get_stop_button(session.language) if not final else None, + ) + except BadRequest as e: + if "Message is not modified" not in str(e): + try: + fallback_body = escape_html(raw_text[:3500]) + fallback_suffix = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else "" + fallback_msg = (fallback_body + fallback_suffix)[:settings.max_message_length] + await status_msg.edit_text( + fallback_msg, + parse_mode=constants.ParseMode.HTML, + reply_markup=get_stop_button(session.language) if not final else None, + ) + except Exception: + pass + except RetryAfter as e: + await asyncio.sleep(e.retry_after) + except Exception as e: + logger.debug(f"Streaming edit skipped: {e}") def on_delta(token: str): accumulated_tokens.append(token) @@ -6568,32 +6580,101 @@ async def on_startup(app: Application): is_admin = chat_id in settings.admin_user_ids try: - # Clean up pending status message if interrupted - if is_admin and session.last_status_msg_id: + # Check if there was an interrupted or un-delivered turn + if session.turn_in_progress or not session.last_delivered or session.last_status_msg_id: is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") - ready_msg = ( - f"🟢 ربات راه‌اندازی مجدد شد و آنلاین است (پروژه فعال: {escape_html(proj_name)}).\n\nآماده دریافت دستورات جدید شما هستم!" - if is_fa else - f"🟢 AGY Bot restarted successfully and is online (Active Project: {escape_html(proj_name)})!\n\nReady for your commands!" - ) - try: - await app.bot.edit_message_text( - chat_id=chat_id, - message_id=session.last_status_msg_id, - text=ready_msg, - parse_mode=constants.ParseMode.HTML, - disable_web_page_preview=True, + recovered_text = None + + # Attempt to recover last output from AGY transcript + if curr_proj and curr_proj.conversation_id: + try: + from agy_engine import get_conversation_last_output + rec = get_conversation_last_output(curr_proj.conversation_id) + if rec.get("text"): + recovered_text = rec["text"] + elif rec.get("tools"): + tools_list = "\n".join(f"• {escape_html(t)}" for t in rec["tools"][-8:]) + recovered_text = ( + f"✅ دستورات و ابزارهای زیر پیش از راه‌اندازی مجدد با موفقیت اعمال شدند:\n\n{tools_list}" + if is_fa else + f"✅ The following operations were executed before restart:\n\n{tools_list}" + ) + except Exception as rec_err: + logger.debug(f"Startup recovery failed for {chat_id}: {rec_err}") + + if recovered_text: + formatted_html = markdown_to_telegram_html(recovered_text) + rec_header = ( + f"🟢 [پاسخ بازیابی‌شده پس از ری‌استارت ربات - پروژه: {escape_html(proj_name)}]\n\n" + if is_fa else + f"🟢 [Response Recovered After Restart - Project: {escape_html(proj_name)}]\n\n" ) - except Exception as edit_err: - logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}") + full_delivered = rec_header + formatted_html + chunks = split_message(full_delivered, max_length=settings.max_message_length) + + if session.last_status_msg_id: + try: + await app.bot.edit_message_text( + chat_id=chat_id, + message_id=session.last_status_msg_id, + text=chunks[0], + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + ) + for extra in chunks[1:]: + await app.bot.send_message( + chat_id=chat_id, + text=extra, + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + ) + except Exception: + for c in chunks: + try: + await app.bot.send_message( + chat_id=chat_id, + text=c, + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + ) + except Exception: + pass + else: + for c in chunks: + try: + await app.bot.send_message( + chat_id=chat_id, + text=c, + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + ) + except Exception: + pass + + session.last_response = recovered_text + if curr_proj: + curr_proj.last_response = recovered_text + + elif is_admin and session.last_status_msg_id: + ready_msg = ( + f"🟢 ربات راه‌اندازی مجدد شد و آنلاین است (پروژه فعال: {escape_html(proj_name)}).\n\nآماده دریافت دستورات جدید شما هستم!" + if is_fa else + f"🟢 AGY Bot restarted successfully and is online (Active Project: {escape_html(proj_name)})!\n\nReady for your commands!" + ) + try: + await app.bot.edit_message_text( + chat_id=chat_id, + message_id=session.last_status_msg_id, + text=ready_msg, + parse_mode=constants.ParseMode.HTML, + disable_web_page_preview=True, + ) + except Exception as edit_err: + logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}") except Exception as e: logger.error(f"Failed to deliver recovery message to chat {chat_id}: {e}", exc_info=True) finally: - # Delete/Clear last message and turn flags from memory - session.last_response = None - session.last_prompt = None - if curr_proj: - curr_proj.last_response = None + # Reset turn flags cleanly session.turn_in_progress = False session.restart_pending = False session.last_delivered = True diff --git a/telegram-agy-bot/formatters.py b/telegram-agy-bot/formatters.py index 0c55123..841a5b3 100644 --- a/telegram-agy-bot/formatters.py +++ b/telegram-agy-bot/formatters.py @@ -1,5 +1,7 @@ import re import html +import json +from pathlib import Path from typing import List, Optional, Dict, Any def escape_html(text: str) -> str: @@ -127,12 +129,76 @@ def format_thought( header = "💭 تفکر و استدلال:" if is_fa else "💭 Reasoning / Thinking:" return f"
{header}\n{escaped}
\n" -def format_tool_call(tool_name: str, tool_args: str, lang: str = "fa") -> str: - """Formats an active tool execution notification.""" - escaped_args = escape_html(tool_args) +def format_tool_call(tool_name: str, tool_args: Any, lang: str = "fa") -> str: + """Formats an active tool execution notification into a clean, concise badge.""" is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") label = "ابزار" if is_fa else "Tool" - return f"🔧 {label}: {escape_html(tool_name)}\n
{escaped_args}
\n" + + # Parse tool arguments if passed as JSON string or dict + params = {} + if isinstance(tool_args, dict): + params = tool_args + elif isinstance(tool_args, str): + try: + params = json.loads(tool_args) + except Exception: + params = {} + + # Extract friendly description/action + action = "" + target = "" + if isinstance(params, dict) and params: + action = params.get("toolAction") or params.get("toolSummary") or params.get("Description") or "" + + # Tool specific highlights + if tool_name == "run_command": + cmd = params.get("CommandLine", "") + target = f"{escape_html(cmd[:120])}" if cmd else "" + elif tool_name in ("write_to_file", "replace_file_content"): + file_p = params.get("TargetFile", "") + instr = params.get("Instruction", "") + if file_p: + target = f"📄 {escape_html(Path(file_p).name if '/' in file_p else file_p)}" + if instr and not action: + action = instr[:100] + elif tool_name in ("view_file", "view_image"): + file_p = params.get("AbsolutePath", "") + if file_p: + target = f"🔍 {escape_html(Path(file_p).name if '/' in file_p else file_p)}" + elif tool_name == "grep_search": + q = params.get("Query", "") + if q: + target = f"🔍 {escape_html(q[:80])}" + elif tool_name == "find_by_name": + pat = params.get("Pattern", "") + if pat: + target = f"📁 {escape_html(pat[:80])}" + elif tool_name in ("read_url_content", "search_web"): + u = params.get("Url") or params.get("query", "") + if u: + target = f"🌐 {escape_html(u[:80])}" + elif tool_name == "manage_task": + act = params.get("Action", "") + tid = params.get("TaskId", "") + target = f"⚙️ {escape_html(act)} {escape_html(tid)}" + elif tool_name == "invoke_subagent": + subagents = params.get("Subagents", []) + roles = [s.get("Role", s.get("TypeName", "Agent")) for s in subagents if isinstance(s, dict)] + if roles: + target = f"🤖 {escape_html(', '.join(roles)[:80])}" + + if not action and not target and isinstance(tool_args, str) and tool_args.strip(): + # Fallback short string preview + short_args = tool_args.strip().replace("\n", " ") + if len(short_args) > 100: + short_args = short_args[:100] + "..." + target = f"{escape_html(short_args)}" + + action_text = f" ({escape_html(action)})" if action else "" + out = f"🔧 {label}: {escape_html(tool_name)}{action_text}" + if target: + out += f"\n ↳ {target}" + return out + "\n" def markdown_to_telegram_html(text: str) -> str: """