Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 128 additions & 10 deletions src/copaw/app/channels/qq/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,89 @@ def _should_plaintext_fallback_from_markdown(exc: Exception) -> bool:
)


async def _fix_content_with_llm(
original_text: str,
error_info: dict,
process_handler: ProcessHandler,
) -> Optional[str]:
"""Use LLM to fix message that failed to send.

Args:
original_text: The text that failed to send
error_info: Error details from QQ API
process_handler: The agent process handler to query LLM

Returns:
Fixed text or None if fixing failed
"""
from agentscope_runtime.engine.schemas.agent_schemas import (
AgentRequest,
Message,
Role,
ContentType,
TextContent,
)

# Simple prompt: just give the error and original message
prompt = f"""Failed to send this message to QQ:

{original_text}

Error: {json.dumps(error_info, ensure_ascii=False)}

Please fix the message to make it sendable. Return only the fixed message."""

try:
msg = Message(
type=ContentType.TEXT,
role=Role.USER,
content=[TextContent(type=ContentType.TEXT, text=prompt)],
)
request = AgentRequest(
session_id="qq_fix",
user_id="system",
input=[msg],
channel="internal",
)

fixed_text_parts = []
async for event in process_handler(request):
obj = getattr(event, "object", None)
status = getattr(event, "status", None)
if obj == "message" and status == "completed":
parts = getattr(event, "content", None) or []
for part in parts:
if getattr(part, "type", None) == ContentType.TEXT:
fixed_text_parts.append(getattr(part, "text", "") or "")

fixed_text = "".join(fixed_text_parts).strip()

# Validate the fix is not empty
if fixed_text:
logger.info(
"LLM fixed QQ message: original_len=%d, fixed_len=%d",
len(original_text),
len(fixed_text),
)
return fixed_text

logger.warning("LLM fix returned empty text")
return None

except Exception:
logger.exception("Failed to fix message with LLM")
return None


def _is_token_expired_error(exc: Exception) -> bool:
"""Check if error indicates token expiration (401) or auth failure."""
if isinstance(exc, QQApiError):
return exc.status == 401
# Also check for auth-related messages
err_str = str(exc).lower()
return any(kw in err_str for kw in ["unauthorized", "token", "auth", "401"])


def _get_api_base() -> str:
"""API root address (e.g. sandbox: https://sandbox.api.sgroup.qq.com)"""
return os.getenv("QQ_API_BASE", DEFAULT_API_BASE).rstrip("/")
Expand Down Expand Up @@ -687,15 +770,20 @@ async def _dispatch(send_text: str, markdown: bool) -> None:
await _dispatch(clean_text, use_markdown)
text_sent = True
except Exception as exc:
if not use_markdown:
logger.exception("send text failed")
elif not _should_plaintext_fallback_from_markdown(exc):
logger.exception(
"send text failed with markdown; "
"skip fallback to avoid duplicates",
)
else:
logger.exception(
# Check if token expired - try refresh and retry once
if _is_token_expired_error(exc):
logger.warning("QQ API token expired, refreshing and retrying")
self._clear_token_cache()
try:
token = await self._get_access_token_async()
await _dispatch(clean_text, use_markdown)
text_sent = True
logger.info("QQ send succeeded after token refresh")
except Exception as retry_exc:
logger.exception("send failed even after token refresh")
# Check if markdown payload issue - fallback to plain text
elif use_markdown and _should_plaintext_fallback_from_markdown(exc):
logger.warning(
"send text failed with markdown payload validation; "
"fallback to plain text",
)
Expand All @@ -708,8 +796,38 @@ async def _dispatch(send_text: str, markdown: bool) -> None:
try:
await _dispatch(fallback_text, False)
text_sent = True
except Exception:
except Exception as fallback_exc:
logger.exception("send text fallback failed")
# For all other errors, try LLM to fix
else:
error_info = (
exc.data if isinstance(exc, QQApiError) else {"error": str(exc)}
)
if isinstance(error_info, dict):
err_code = error_info.get("err_code") or error_info.get("code", "unknown")
else:
err_code = str(exc)[:50]
logger.warning(
"QQ send failed (%s), trying LLM to fix",
err_code,
)
Comment on lines +810 to +813
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conditional expression to determine the error code for logging is quite complex and embedded within the logger.warning call, which makes it difficult to read and understand at a glance. For better readability and maintainability, consider extracting this logic into a separate variable before the logging call.

Suggested change
logger.warning(
"QQ send failed (%s), trying LLM to fix",
error_info.get("err_code") or error_info.get("code", "unknown")
if isinstance(error_info, dict)
else str(exc)[:50],
)
if isinstance(error_info, dict):
err_code = error_info.get("err_code") or error_info.get("code", "unknown")
else:
err_code = str(exc)[:50]
logger.warning(
"QQ send failed (%s), trying LLM to fix",
err_code,
)


# Try LLM-based fixing
fixed_text = await _fix_content_with_llm(
clean_text,
error_info,
self._process,
)

if fixed_text:
try:
await _dispatch(fixed_text, False)
text_sent = True
logger.info("QQ send succeeded after LLM fix")
except Exception:
logger.exception("send failed even after LLM fix")
else:
logger.warning("LLM fix failed, giving up")

# Send images if any
if image_urls and message_type in ("c2c", "group"):
Expand Down