fix(agents): make librarian failures structured and user-safe

- run_librarian / run_librarian_stream raise AgentError instead of
  returning/yielding error text as normal output; detail stays in logs
- delegate_to_* wrappers now put a curated butler-toned sentence in
  DelegationResult.output on failure and never expose str(e), so
  streaming's error branch is reachable and honest
- _execute_single_delegation propagates success; direct delegation only
  records delegate_to_* as called when the expert actually succeeded
- librarian tools return user-safe messages instead of
  'Error searching: {e}' strings that leaked internal URLs into
  synthesis; coordination stream errors are curated as well
- ruff cleanups (TYPE_CHECKING forward refs, B904, unused locals) in
  the touched files to keep them lint-clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:12:30 +02:00
co-authored by Claude Fable 5
parent 59f5b54ac9
commit f853db8ccc
10 changed files with 277 additions and 140 deletions
+15 -11
View File
@@ -10,7 +10,8 @@ based on Steward recommendations. Handles:
"""
import asyncio
import time
from typing import Any, AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Any
from src.agents.librarian import run_librarian, run_librarian_stream
from src.agents.protocol import (
@@ -22,7 +23,6 @@ from src.agents.protocol import (
CoordinationResult,
DelegationIntent,
DelegationReason,
ToolCallRecord,
)
from src.core.household_registry import get_household_registry
from src.core.logging_config import get_logger
@@ -87,7 +87,7 @@ class CoordinationEngine:
self,
intent: DelegationIntent,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> AgentResponse:
"""
Execute a single delegation to an expert agent.
@@ -166,7 +166,7 @@ class CoordinationEngine:
duration_ms=duration_ms,
)
except asyncio.TimeoutError:
except TimeoutError as e:
duration_ms = int((time.time() - start_time) * 1000)
logger.error(
"delegation_timeout",
@@ -176,7 +176,7 @@ class CoordinationEngine:
raise AgentTimeoutError(
f"Agent '{agent_name}' timed out after {duration_ms}ms",
agent_name=agent_name,
)
) from e
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
@@ -198,7 +198,7 @@ class CoordinationEngine:
self,
intent: DelegationIntent,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> AsyncGenerator[str, None]:
"""
Execute a delegation with streaming output.
@@ -242,19 +242,23 @@ class CoordinationEngine:
logger.info("delegation_stream_completed", agent=agent_name)
except Exception as e:
# Exception detail stays in the logs; yield a curated
# user-safe sentence instead of leaking internals.
from src.agents.delegation import get_think_message
logger.error(
"delegation_stream_error",
agent=agent_name,
error=str(e),
exc_info=True,
)
yield f"\n\n[Error from {agent_name}: {str(e)}]"
yield "\n\n" + get_think_message(agent_name, intent.task, "error")
async def coordinate(
self,
intents: list[DelegationIntent],
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> CoordinationResult:
"""
Coordinate execution of multiple delegations.
@@ -329,7 +333,7 @@ class CoordinationEngine:
# Global coordination engine instance
_coordination_engine: Optional[CoordinationEngine] = None
_coordination_engine: CoordinationEngine | None = None
def get_coordination_engine() -> CoordinationEngine:
@@ -344,7 +348,7 @@ async def delegate_to_librarian(
task: str,
context: str = "",
reason: DelegationReason = DelegationReason.DOMAIN_EXPERTISE,
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> AgentResponse:
"""
Convenience function to delegate a task to The Librarian.
@@ -377,7 +381,7 @@ async def delegate_to_librarian(
async def delegate_to_librarian_stream(
task: str,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> AsyncGenerator[str, None]:
"""
Convenience function to delegate to Librarian with streaming.
+19 -12
View File
@@ -8,12 +8,12 @@ returns a structured result for synthesis.
This implements the agent-as-tool pattern recommended by PydanticAI:
agents call other agents via tool wrappers, keeping each agent focused.
"""
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
from enum import Enum
from typing import AsyncGenerator, Callable, Optional, Any
from src.core.logging_config import get_logger
from src.core.tracing import trace_span, SpanType
from src.core.tracing import SpanType, trace_span
logger = get_logger(__name__)
@@ -167,7 +167,7 @@ class DelegationTask:
action: str = ""
priority: int = 0
depends_on: list[str] = field(default_factory=list)
result: Optional[str] = None
result: str | None = None
task_id: str = ""
def __post_init__(self):
@@ -186,14 +186,16 @@ class DelegationResult:
expert_name: Which expert handled the task
task: Original task description
success: Whether the delegation succeeded
output: Expert's response/findings
error: Error message if failed
output: Expert's response/findings. On failure this holds a
curated, user-safe butler sentence (never exception detail)
error: Short user-safe error label if failed. Exception detail
stays in the logs only
"""
expert_name: str
task: str
success: bool
output: str
error: Optional[str] = None
error: str | None = None
async def delegate_to_librarian(
@@ -285,12 +287,15 @@ async def delegate_to_librarian(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs; the user-facing output
# is a curated butler sentence so internals never leak into
# synthesis.
return DelegationResult(
expert_name="librarian",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("librarian", task, "error"),
error="The Librarian was unable to complete the task.",
)
@@ -383,12 +388,13 @@ async def delegate_to_biographer(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs only.
return DelegationResult(
expert_name="biographer",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("biographer", task, "error"),
error="The Biographer was unable to complete the task.",
)
@@ -480,12 +486,13 @@ async def delegate_to_housekeeper(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs only.
return DelegationResult(
expert_name="housekeeper",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("housekeeper", task, "error"),
error="The Housekeeper was unable to complete the task.",
)
+23 -7
View File
@@ -7,7 +7,7 @@ the library-desk API, offering:
- Wiki and document management
- Semantic search and knowledge graph exploration
"""
from typing import Any, Optional
from typing import Any
from pydantic_ai import Agent
@@ -27,7 +27,7 @@ from src.agents.librarian.tools import (
smart_create_wiki_page,
update_wiki_page,
)
from src.core.config import config
from src.agents.protocol import AgentError
from src.core.logging_config import get_logger
logger = get_logger(__name__)
@@ -138,7 +138,7 @@ If a tool fails or you cannot access a data source:
"""
# Lazy initialization to avoid connection issues during imports
_librarian_agent: Optional[Agent[None, str]] = None
_librarian_agent: Agent[None, str] | None = None
def _create_librarian_agent() -> Agent[None, str]:
@@ -204,7 +204,7 @@ def get_librarian_agent() -> Agent[None, str]:
async def run_librarian(
task: str,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> str:
"""
Execute a research task with The Librarian.
@@ -220,6 +220,10 @@ async def run_librarian(
Returns:
Research results and findings
Raises:
AgentError: If the research task fails. Exception detail is
logged here; callers map the failure to a user-safe message.
Example:
result = await run_librarian(
task="Find information about Docker networking",
@@ -255,19 +259,23 @@ async def run_librarian(
return result.output
except Exception as e:
# Full detail stays in the logs; callers receive a structured
# failure instead of error text masquerading as research output.
logger.error(
"librarian_task_error",
task=task[:50],
error=str(e),
exc_info=True,
)
return f"The Librarian encountered an error: {str(e)}"
raise AgentError(
"Research task failed", agent_name="librarian"
) from e
async def run_librarian_stream(
task: str,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
):
"""
Execute a research task with streaming output.
@@ -282,6 +290,10 @@ async def run_librarian_stream(
Yields:
str: Text deltas from the response
Raises:
AgentError: If the research task fails. Exception detail is
logged here; callers map the failure to a user-safe message.
Example:
async for delta in run_librarian_stream("Find Docker docs"):
print(delta, end="", flush=True)
@@ -309,10 +321,14 @@ async def run_librarian_stream(
logger.info("librarian_stream_completed", task=task[:50])
except Exception as e:
# Full detail stays in the logs; raise instead of yielding error
# text into the stream as if it were research output.
logger.error(
"librarian_stream_error",
task=task[:50],
error=str(e),
exc_info=True,
)
yield f"\n\nThe Librarian encountered an error: {str(e)}"
raise AgentError(
"Research task failed", agent_name="librarian"
) from e
+14 -14
View File
@@ -111,7 +111,7 @@ async def hybrid_search(
except Exception as e:
logger.error("librarian_hybrid_search_error", error=str(e), query=query)
return f"Error searching: {str(e)}"
return "I was unable to search the knowledge archives; the search service did not respond properly."
# ============================================================================
@@ -159,7 +159,7 @@ async def search_wiki(
except Exception as e:
logger.error("librarian_wiki_search_error", error=str(e))
return f"Error searching wiki: {str(e)}"
return "I was unable to search the wiki at this time."
async def get_wiki_page(
@@ -202,7 +202,7 @@ async def get_wiki_page(
except Exception as e:
logger.error("librarian_get_page_error", error=str(e), page_id=page_id)
return f"Error getting page {page_id}: {str(e)}"
return f"I was unable to retrieve wiki page {page_id}."
async def list_dossiers() -> str:
@@ -236,7 +236,7 @@ async def list_dossiers() -> str:
except Exception as e:
logger.error("librarian_list_dossiers_error", error=str(e))
return f"Error listing dossiers: {str(e)}"
return "I was unable to retrieve the list of dossiers."
async def get_dossier_pages(
@@ -277,7 +277,7 @@ async def get_dossier_pages(
except Exception as e:
logger.error("librarian_get_dossier_error", error=str(e))
return f"Error getting dossier: {str(e)}"
return f"I was unable to retrieve the dossier '{dossier_name}'."
# ============================================================================
@@ -326,7 +326,7 @@ async def semantic_search(
except Exception as e:
logger.error("librarian_semantic_search_error", error=str(e))
return f"Error in semantic search: {str(e)}"
return "I was unable to complete the semantic search."
# ============================================================================
@@ -379,7 +379,7 @@ async def explore_knowledge_graph(
except Exception as e:
logger.error("librarian_explore_graph_error", error=str(e))
return f"Error exploring knowledge graph: {str(e)}"
return "I was unable to explore the knowledge graph."
async def find_related_entities(
@@ -450,7 +450,7 @@ async def find_related_entities(
except Exception as e:
logger.error("librarian_find_related_error", error=str(e))
return f"Error finding related entities: {str(e)}"
return f"I was unable to look up entities related to '{entity_name}'."
# ============================================================================
@@ -533,7 +533,7 @@ async def search_web(
except Exception as e:
logger.error("librarian_web_search_error", error=str(e), query=query)
return f"Error searching web: {str(e)}"
return "I was unable to search the web at this time."
async def read_url(
@@ -609,7 +609,7 @@ async def read_url(
except Exception as e:
logger.error("librarian_read_url_error", error=str(e), url=url)
return f"Error reading URL: {str(e)}"
return f"I was unable to read the page at {url}."
async def read_urls_batch(
@@ -683,7 +683,7 @@ async def read_urls_batch(
except Exception as e:
logger.error("librarian_read_urls_batch_error", error=str(e))
return f"Error reading URLs: {str(e)}"
return "I was unable to read the requested pages."
# ============================================================================
@@ -766,7 +766,7 @@ async def update_wiki_page(
except Exception as e:
logger.error("librarian_update_page_error", error=str(e), page_id=page_id)
return f"Error updating page {page_id}: {str(e)}"
return f"I was unable to update wiki page {page_id}."
async def create_wiki_page(
@@ -841,7 +841,7 @@ async def create_wiki_page(
except Exception as e:
logger.error("librarian_create_page_error", error=str(e), title=title)
return f"Error creating page: {str(e)}"
return f"I was unable to create the page '{title}'."
async def smart_create_wiki_page(
@@ -930,7 +930,7 @@ async def smart_create_wiki_page(
except Exception as e:
logger.error("librarian_smart_create_error", error=str(e), topic=topic)
return f"Error creating page about '{topic}': {str(e)}"
return f"I was unable to create a page about '{topic}'."
# ============================================================================
+62 -45
View File
@@ -6,32 +6,32 @@ Tracks conversation history for analytics and future vector memory.
Integrates with Steward preprocessing for Phase 2 two-tier architecture.
"""
import time
import asyncio
import re
import secrets
from typing import AsyncGenerator
import time
from collections.abc import AsyncGenerator
from src.agents.delegation import get_think_message
from src.agents.registry import ModelRegistry
from src.agents.steward.schemas import StewardRecommendation
from src.core.context import current_conversation, current_user, get_default_user
from src.core.logging_config import get_logger
from src.core.preprocessing import preprocess_request
from src.core.tool_tracking import ToolCallTracker
from src.core.tracing import SpanType, end_trace, start_span, start_trace
from src.responses.context import ContextWindow
from src.responses.history import ConversationHistory
from src.responses.schemas import (
FunctionCallOutputItem,
MessageOutputItem,
OutputTextContent,
ReasoningOutputItem,
Response,
ResponseRequest,
ResponseUsage,
MessageOutputItem,
ReasoningOutputItem,
FunctionCallOutputItem,
OutputTextContent,
)
from src.responses.streaming import StreamingCoordinator
from src.responses.history import ConversationHistory
from src.responses.context import ContextWindow
from src.core.preprocessing import preprocess_request
from src.core.tool_tracking import ToolCallTracker
from src.core.logging_config import get_logger
from src.core.tracing import start_trace, end_trace, start_span, SpanType
from src.core.context import current_user, current_conversation, get_default_user
from src.agents.steward.schemas import StewardRecommendation
import re
import asyncio
logger = get_logger(__name__)
@@ -63,7 +63,7 @@ async def _execute_single_delegation(
agent_name: str,
task: str,
tracker: "ToolCallTracker",
) -> tuple[str, str]:
) -> tuple[str, str, bool]:
"""
Execute a single delegation to an agent.
@@ -73,7 +73,8 @@ async def _execute_single_delegation(
tracker: Tool call tracker
Returns:
tuple: (agent_name, result_summary)
tuple: (agent_name, result_summary, success). On failure the
result summary is a curated user-safe sentence.
"""
import time
start_time = time.time()
@@ -83,24 +84,24 @@ async def _execute_single_delegation(
result = await delegate_to_biographer(task=task)
duration = time.time() - start_time
await tracker.track_call("delegate_to_biographer", duration)
return (agent_name, result.output)
return (agent_name, result.output, result.success)
elif agent_name == "librarian":
from src.agents.delegation import delegate_to_librarian
result = await delegate_to_librarian(task=task)
duration = time.time() - start_time
await tracker.track_call("delegate_to_librarian", duration)
return (agent_name, result.output)
return (agent_name, result.output, result.success)
elif agent_name == "housekeeper":
from src.agents.delegation import delegate_to_housekeeper
result = await delegate_to_housekeeper(task=task)
duration = time.time() - start_time
await tracker.track_call("delegate_to_housekeeper", duration)
return (agent_name, result.output)
return (agent_name, result.output, result.success)
else:
return (agent_name, f"Unknown agent: {agent_name}")
return (agent_name, f"Unknown agent: {agent_name}", False)
async def _handle_text_delegation(
@@ -175,13 +176,24 @@ async def _handle_text_delegation(
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Combine results
# Combine results (failures carry curated user-safe sentences)
summaries = []
for agent_name, result in results:
if isinstance(result, Exception):
summaries.append(f"**{agent_name}**: Error - {result}")
for (agent, task), item in zip(matches, results, strict=False):
agent_name = agent.lower()
if isinstance(item, BaseException):
logger.error(
"delegation_failed",
agent=agent_name,
error=str(item),
conversation_id=conversation_id,
)
summaries.append(
f"**{agent_name}**: "
f"{get_think_message(agent_name, task, 'error')}"
)
else:
summaries.append(f"**{agent_name}**: {result}")
_, output, _ = item
summaries.append(f"**{agent_name}**: {output}")
return "\n\n".join(summaries)
@@ -197,20 +209,19 @@ async def _handle_text_delegation(
conversation_id=conversation_id,
)
try:
_, result = await _execute_single_delegation(
_, output, _ = await _execute_single_delegation(
agent_name, task, tracker
)
summaries.append(result)
summaries.append(output)
except Exception as e:
logger.error(
"delegation_failed",
agent=agent_name,
error=str(e),
conversation_id=conversation_id,
exc_info=True,
)
summaries.append(
f"I apologize, sir. Delegation to {agent_name} failed: {e}"
)
summaries.append(get_think_message(agent_name, task, "error"))
return "\n\n".join(summaries)
@@ -219,8 +230,9 @@ async def _handle_text_delegation(
"text_delegation_failed",
error=str(e),
conversation_id=conversation_id,
exc_info=True,
)
return f"I apologize, sir. I encountered an error processing delegations: {e}"
return "I apologize, sir. I was unable to complete the requested delegations."
async def _direct_delegation(
@@ -254,13 +266,14 @@ async def _direct_delegation(
results = []
for agent in recommendation.recommended_capabilities:
try:
agent_name, result = await _execute_single_delegation(
agent_name, result, success = await _execute_single_delegation(
agent, user_message, tracker
)
results.append(result)
logger.info(
"direct_delegation_complete",
agent=agent_name,
success=success,
result_preview=result[:100] if result else "empty",
conversation_id=conversation_id,
)
@@ -270,8 +283,9 @@ async def _direct_delegation(
agent=agent,
error=str(e),
conversation_id=conversation_id,
exc_info=True,
)
results.append(f"I apologize, sir. Delegation to {agent} failed: {e}")
results.append(get_think_message(agent, user_message, "error"))
return "\n\n".join(results) if results else "I apologize, sir. No delegation results available."
@@ -309,15 +323,17 @@ async def _direct_delegation_with_results(
for agent in recommendation.recommended_capabilities:
try:
agent_name, result = await _execute_single_delegation(
agent_name, result, success = await _execute_single_delegation(
agent, user_message, tracker
)
expert_results[agent_name] = result
tools_called.append(f"delegate_to_{agent_name}")
if success:
tools_called.append(f"delegate_to_{agent_name}")
logger.info(
"direct_delegation_result",
agent=agent_name,
success=success,
result_preview=result[:100] if result else "empty",
conversation_id=conversation_id,
)
@@ -327,8 +343,9 @@ async def _direct_delegation_with_results(
agent=agent,
error=str(e),
conversation_id=conversation_id,
exc_info=True,
)
expert_results[agent] = f"Error: {e}"
expert_results[agent] = get_think_message(agent, user_message, "error")
return {
"tools_called": tools_called,
@@ -439,7 +456,7 @@ async def create_response(request: ResponseRequest) -> Response:
user_input = _extract_user_input(request.input)
# Start trace
trace = start_trace(
start_trace(
conversation_id=conversation_id,
user=effective_user,
request={
@@ -451,7 +468,7 @@ async def create_response(request: ResponseRequest) -> Response:
)
# Start service span
service_span = start_span(
start_span(
"create_response",
SpanType.ROUTER,
metadata={"model": request.model, "user": effective_user},
@@ -509,7 +526,7 @@ async def create_response(request: ResponseRequest) -> Response:
return response
except Exception as e:
except Exception:
end_trace(status="error")
raise
@@ -550,7 +567,7 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
user_input = _extract_user_input(request.input)
# Start trace
trace = start_trace(
start_trace(
conversation_id=conversation_id,
user=effective_user,
request={
@@ -562,7 +579,7 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
)
# Start service span
service_span = start_span(
start_span(
"create_response_with_steward",
SpanType.ROUTER,
metadata={"model": request.model, "user": effective_user},
@@ -706,7 +723,7 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
return response
except Exception as e:
except Exception:
end_trace(status="error")
raise
+46 -26
View File
@@ -8,14 +8,22 @@ Handles Server-Sent Events (SSE) streaming with proper event types:
- response.done
"""
from enum import Enum
from typing import Literal, AsyncGenerator
import json
import time
from collections.abc import AsyncGenerator
from enum import Enum
from typing import TYPE_CHECKING, Literal
from src.agents.registry import ModelRegistry
from src.core.logging_config import get_logger
from src.core.models import CustomBaseModel
from src.responses.schemas import Response
from src.agents.registry import ModelRegistry
if TYPE_CHECKING:
from src.agents.steward.schemas import StewardRecommendation
from src.core.tool_tracking import ToolCallTracker
from src.responses.schemas import ResponseRequest
logger = get_logger(__name__)
# ============================================================================
@@ -131,18 +139,17 @@ class StreamingCoordinator:
Yields:
StreamEvent: Stream of SSE events
"""
from src.responses.service import (
_calculate_usage,
generate_id,
_conversation_history,
_direct_delegation_with_results,
)
import asyncio
from src.agents.tatlock import TatlockAgent
from src.core.preprocessing import preprocess_request
from src.core.tool_tracking import ToolCallTracker
from src.responses.schemas import MessageOutputItem, ReasoningOutputItem, OutputTextContent
from src.agents.tatlock import TatlockAgent
from src.agents.delegation import get_think_message, STREAMING_DELEGATION_WRAPPERS
import asyncio
from src.responses.schemas import MessageOutputItem, OutputTextContent
from src.responses.service import (
_calculate_usage,
_conversation_history,
generate_id,
)
output_items = []
@@ -280,13 +287,14 @@ class StreamingCoordinator:
Returns:
dict: Orchestration results with think_messages list
"""
import time as time_module
from src.agents.delegation import (
get_think_message,
delegate_to_librarian,
delegate_to_biographer,
delegate_to_housekeeper,
delegate_to_librarian,
get_think_message,
)
import time as time_module
expert_results = {}
tools_called = []
@@ -319,15 +327,26 @@ class StreamingCoordinator:
success_msg = get_think_message(agent, user_message, "success")
think_messages.append(success_msg + "\n")
else:
error_msg = result.error if result else "Unknown error"
expert_results[agent] = f"Error: {error_msg}"
# Emit error think message
# Failed delegations carry a curated user-safe sentence
# in output; exception detail is already in the logs.
error_think = get_think_message(agent, user_message, "error")
if result and result.output:
expert_results[agent] = result.output
else:
expert_results[agent] = error_think
# Emit error think message
think_messages.append(error_think + "\n")
except Exception as e:
expert_results[agent] = f"Error: {e}"
logger.error(
"direct_delegation_stream_error",
agent=agent,
error=str(e),
conversation_id=conversation_id,
exc_info=True,
)
error_think = get_think_message(agent, user_message, "error")
expert_results[agent] = error_think
think_messages.append(error_think + "\n")
return {
@@ -361,9 +380,10 @@ class StreamingCoordinator:
event: response.done
data: {"response": {...}}
"""
from src.responses.service import _calculate_usage, generate_id
import asyncio
from src.responses.service import _calculate_usage, generate_id
output_items = []
last_message_text = "" # Track last streamed message text to compute deltas
@@ -488,10 +508,10 @@ class StreamingCoordinator:
def _convert_output_items(self, items: list) -> list:
"""Convert agent OutputItem objects to schema OutputItem objects."""
from src.responses.schemas import (
MessageOutputItem,
ReasoningOutputItem,
FunctionCallOutputItem,
MessageOutputItem,
OutputTextContent,
ReasoningOutputItem,
)
converted = []
@@ -521,9 +541,9 @@ class StreamingCoordinator:
def _create_error_event(self, error: Exception) -> ErrorEvent:
"""Create error event from exception."""
from src.core.exceptions import (
RateLimitError,
ContextLengthError,
AppException,
ContextLengthError,
RateLimitError,
)
if isinstance(error, RateLimitError):