fix(engine): add resilient tool display, auto-recovery on restart, and transcript fallback
This commit is contained in:
@@ -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"• <code>{t}</code>" 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,
|
||||
|
||||
+124
-43
@@ -6318,28 +6318,40 @@ async def process_agent_turn(
|
||||
thinking_msg = f"💭 <i>در حال تفکر روی پروژه <b>{escape_html(curr_proj.name)}</b>...</i> ▌" if is_fa else f"💭 <i>Thinking on project <b>{escape_html(curr_proj.name)}</b>...</i> ▌"
|
||||
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<i>(ادامه در پیام بعدی...)</i>" if is_fa else "...\n<i>(continues...)</i>")
|
||||
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"🟢 <b>ربات راهاندازی مجدد شد و آنلاین است (پروژه فعال: <code>{escape_html(proj_name)}</code>).</b>\n\nآماده دریافت دستورات جدید شما هستم!"
|
||||
if is_fa else
|
||||
f"🟢 <b>AGY Bot restarted successfully and is online (Active Project: <code>{escape_html(proj_name)}</code>)!</b>\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"• <code>{escape_html(t)}</code>" for t in rec["tools"][-8:])
|
||||
recovered_text = (
|
||||
f"✅ <b>دستورات و ابزارهای زیر پیش از راهاندازی مجدد با موفقیت اعمال شدند:</b>\n\n{tools_list}"
|
||||
if is_fa else
|
||||
f"✅ <b>The following operations were executed before restart:</b>\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"🟢 <b>[پاسخ بازیابیشده پس از ریاستارت ربات - پروژه: <code>{escape_html(proj_name)}</code>]</b>\n\n"
|
||||
if is_fa else
|
||||
f"🟢 <b>[Response Recovered After Restart - Project: <code>{escape_html(proj_name)}</code>]</b>\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"🟢 <b>ربات راهاندازی مجدد شد و آنلاین است (پروژه فعال: <code>{escape_html(proj_name)}</code>).</b>\n\nآماده دریافت دستورات جدید شما هستم!"
|
||||
if is_fa else
|
||||
f"🟢 <b>AGY Bot restarted successfully and is online (Active Project: <code>{escape_html(proj_name)}</code>)!</b>\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
|
||||
|
||||
@@ -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 = "💭 <b>تفکر و استدلال:</b>" if is_fa else "💭 <b>Reasoning / Thinking:</b>"
|
||||
return f"<blockquote expandable>{header}\n{escaped}</blockquote>\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"🔧 <b>{label}:</b> <code>{escape_html(tool_name)}</code>\n<pre>{escaped_args}</pre>\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"<code>{escape_html(cmd[:120])}</code>" 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"📄 <code>{escape_html(Path(file_p).name if '/' in file_p else file_p)}</code>"
|
||||
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"🔍 <code>{escape_html(Path(file_p).name if '/' in file_p else file_p)}</code>"
|
||||
elif tool_name == "grep_search":
|
||||
q = params.get("Query", "")
|
||||
if q:
|
||||
target = f"🔍 <code>{escape_html(q[:80])}</code>"
|
||||
elif tool_name == "find_by_name":
|
||||
pat = params.get("Pattern", "")
|
||||
if pat:
|
||||
target = f"📁 <code>{escape_html(pat[:80])}</code>"
|
||||
elif tool_name in ("read_url_content", "search_web"):
|
||||
u = params.get("Url") or params.get("query", "")
|
||||
if u:
|
||||
target = f"🌐 <code>{escape_html(u[:80])}</code>"
|
||||
elif tool_name == "manage_task":
|
||||
act = params.get("Action", "")
|
||||
tid = params.get("TaskId", "")
|
||||
target = f"⚙️ <code>{escape_html(act)} {escape_html(tid)}</code>"
|
||||
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"🤖 <code>{escape_html(', '.join(roles)[:80])}</code>"
|
||||
|
||||
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"<code>{escape_html(short_args)}</code>"
|
||||
|
||||
action_text = f" ({escape_html(action)})" if action else ""
|
||||
out = f"🔧 <b>{label}:</b> <code>{escape_html(tool_name)}</code>{action_text}"
|
||||
if target:
|
||||
out += f"\n ↳ {target}"
|
||||
return out + "\n"
|
||||
|
||||
def markdown_to_telegram_html(text: str) -> str:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user