camel-ai 0.2.75a6__py3-none-any.whl → 0.2.76__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +1001 -205
- camel/agents/mcp_agent.py +30 -27
- camel/configs/__init__.py +6 -0
- camel/configs/amd_config.py +70 -0
- camel/configs/cometapi_config.py +104 -0
- camel/data_collectors/alpaca_collector.py +15 -6
- camel/environments/tic_tac_toe.py +1 -1
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/docker/Dockerfile +3 -12
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/loaders/__init__.py +11 -2
- camel/loaders/chunkr_reader.py +9 -0
- camel/memories/__init__.py +2 -1
- camel/memories/agent_memories.py +3 -1
- camel/memories/blocks/chat_history_block.py +21 -3
- camel/memories/records.py +88 -8
- camel/messages/base.py +127 -34
- camel/models/__init__.py +4 -0
- camel/models/amd_model.py +101 -0
- camel/models/azure_openai_model.py +0 -6
- camel/models/base_model.py +30 -0
- camel/models/cometapi_model.py +83 -0
- camel/models/model_factory.py +4 -0
- camel/models/openai_compatible_model.py +0 -6
- camel/models/openai_model.py +0 -6
- camel/models/zhipuai_model.py +61 -2
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/retrievers/auto_retriever.py +1 -0
- camel/runtimes/daytona_runtime.py +11 -12
- camel/societies/workforce/prompts.py +131 -50
- camel/societies/workforce/single_agent_worker.py +434 -49
- camel/societies/workforce/structured_output_handler.py +30 -18
- camel/societies/workforce/task_channel.py +43 -0
- camel/societies/workforce/utils.py +105 -12
- camel/societies/workforce/workforce.py +1322 -311
- camel/societies/workforce/workforce_logger.py +24 -5
- camel/storages/key_value_storages/json.py +15 -2
- camel/storages/object_storages/google_cloud.py +1 -1
- camel/storages/vectordb_storages/oceanbase.py +10 -11
- camel/storages/vectordb_storages/tidb.py +8 -6
- camel/tasks/task.py +4 -3
- camel/toolkits/__init__.py +18 -5
- camel/toolkits/aci_toolkit.py +45 -0
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +11 -31
- camel/toolkits/{file_write_toolkit.py → file_toolkit.py} +194 -34
- camel/toolkits/function_tool.py +6 -1
- camel/toolkits/google_drive_mcp_toolkit.py +12 -31
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +12 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +79 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +95 -59
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +5 -612
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +0 -1
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +619 -95
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +7 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +115 -219
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +1 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +405 -131
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +9 -5
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
- camel/toolkits/markitdown_toolkit.py +27 -1
- camel/toolkits/mcp_toolkit.py +348 -348
- camel/toolkits/message_integration.py +3 -0
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/note_taking_toolkit.py +18 -8
- camel/toolkits/notion_mcp_toolkit.py +16 -26
- camel/toolkits/origene_mcp_toolkit.py +8 -49
- camel/toolkits/playwright_mcp_toolkit.py +12 -31
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/slack_toolkit.py +50 -1
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +924 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +17 -11
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/types/enums.py +124 -1
- camel/types/unified_model_type.py +5 -0
- camel/utils/commons.py +17 -0
- camel/utils/context_utils.py +804 -0
- camel/utils/mcp.py +136 -2
- camel/utils/token_counting.py +25 -17
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/METADATA +158 -59
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/RECORD +95 -76
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/terminal_toolkit.py +0 -1788
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,8 +19,8 @@ from pydantic import BaseModel, ValidationError
|
|
|
19
19
|
|
|
20
20
|
from camel.logger import get_logger
|
|
21
21
|
from camel.societies.workforce.utils import (
|
|
22
|
-
RecoveryDecision,
|
|
23
22
|
RecoveryStrategy,
|
|
23
|
+
TaskAnalysisResult,
|
|
24
24
|
TaskAssignResult,
|
|
25
25
|
WorkerConf,
|
|
26
26
|
)
|
|
@@ -65,9 +65,9 @@ class StructuredOutputHandler:
|
|
|
65
65
|
r'description.*?:\s*"([^"]+)"'
|
|
66
66
|
),
|
|
67
67
|
],
|
|
68
|
-
'
|
|
69
|
-
r'"
|
|
70
|
-
r'
|
|
68
|
+
'TaskAnalysisResult': [
|
|
69
|
+
r'"recovery_strategy"\s*:\s*"([^"]+)".*?"reasoning"\s*:\s*"([^"]+)"',
|
|
70
|
+
r'recovery_strategy.*?:\s*"([^"]+)".*?reasoning.*?:\s*"([^"]+)"',
|
|
71
71
|
],
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -239,12 +239,12 @@ Ensure the JSON is valid and properly formatted.
|
|
|
239
239
|
except (IndexError, AttributeError):
|
|
240
240
|
continue
|
|
241
241
|
|
|
242
|
-
elif schema_name == '
|
|
242
|
+
elif schema_name == 'TaskAnalysisResult':
|
|
243
243
|
for pattern in patterns:
|
|
244
244
|
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
|
245
245
|
if match:
|
|
246
246
|
try:
|
|
247
|
-
|
|
247
|
+
recovery_strategy = match.group(1)
|
|
248
248
|
reasoning = match.group(2)
|
|
249
249
|
# Look for modified_task_content
|
|
250
250
|
content_match = re.search(
|
|
@@ -252,14 +252,25 @@ Ensure the JSON is valid and properly formatted.
|
|
|
252
252
|
text,
|
|
253
253
|
re.IGNORECASE,
|
|
254
254
|
)
|
|
255
|
+
# Look for quality_score (for quality evaluation)
|
|
256
|
+
score_match = re.search(
|
|
257
|
+
r'"quality_score"\s*:\s*(\d+)',
|
|
258
|
+
text,
|
|
259
|
+
re.IGNORECASE,
|
|
260
|
+
)
|
|
255
261
|
return {
|
|
256
|
-
'
|
|
262
|
+
'recovery_strategy': recovery_strategy,
|
|
257
263
|
'reasoning': reasoning,
|
|
258
264
|
'modified_task_content': (
|
|
259
265
|
content_match.group(1)
|
|
260
266
|
if content_match
|
|
261
267
|
else None
|
|
262
268
|
),
|
|
269
|
+
'quality_score': (
|
|
270
|
+
int(score_match.group(1))
|
|
271
|
+
if score_match
|
|
272
|
+
else None
|
|
273
|
+
),
|
|
263
274
|
}
|
|
264
275
|
except (IndexError, AttributeError):
|
|
265
276
|
continue
|
|
@@ -370,21 +381,22 @@ Ensure the JSON is valid and properly formatted.
|
|
|
370
381
|
else:
|
|
371
382
|
assignment['dependencies'] = []
|
|
372
383
|
|
|
373
|
-
elif schema_name == '
|
|
374
|
-
# Ensure
|
|
375
|
-
if '
|
|
376
|
-
strategy = fixed_data['
|
|
384
|
+
elif schema_name == 'TaskAnalysisResult':
|
|
385
|
+
# Ensure recovery_strategy is valid
|
|
386
|
+
if 'recovery_strategy' in fixed_data:
|
|
387
|
+
strategy = fixed_data['recovery_strategy'].lower()
|
|
377
388
|
valid_strategies = [
|
|
378
389
|
'retry',
|
|
379
390
|
'replan',
|
|
380
391
|
'decompose',
|
|
381
392
|
'create_worker',
|
|
393
|
+
'reassign',
|
|
382
394
|
]
|
|
383
395
|
if strategy not in valid_strategies:
|
|
384
396
|
# Try to match partial
|
|
385
397
|
for valid in valid_strategies:
|
|
386
398
|
if valid.startswith(strategy) or strategy in valid:
|
|
387
|
-
fixed_data['
|
|
399
|
+
fixed_data['recovery_strategy'] = valid
|
|
388
400
|
break
|
|
389
401
|
|
|
390
402
|
return fixed_data
|
|
@@ -410,10 +422,10 @@ Ensure the JSON is valid and properly formatted.
|
|
|
410
422
|
sys_msg="You are a helpful assistant.",
|
|
411
423
|
description="A general-purpose worker",
|
|
412
424
|
)
|
|
413
|
-
elif schema_name == '
|
|
414
|
-
return
|
|
415
|
-
strategy=RecoveryStrategy.RETRY,
|
|
425
|
+
elif schema_name == 'TaskAnalysisResult':
|
|
426
|
+
return TaskAnalysisResult(
|
|
416
427
|
reasoning="Unable to parse response, defaulting to retry",
|
|
428
|
+
recovery_strategy=RecoveryStrategy.RETRY,
|
|
417
429
|
modified_task_content=None,
|
|
418
430
|
)
|
|
419
431
|
else:
|
|
@@ -482,11 +494,11 @@ Ensure the JSON is valid and properly formatted.
|
|
|
482
494
|
description=f"Fallback worker for task: {task_content}...",
|
|
483
495
|
)
|
|
484
496
|
|
|
485
|
-
elif schema_name == '
|
|
497
|
+
elif schema_name == 'TaskAnalysisResult':
|
|
486
498
|
# Default to retry strategy
|
|
487
|
-
return
|
|
488
|
-
strategy=RecoveryStrategy.RETRY,
|
|
499
|
+
return TaskAnalysisResult(
|
|
489
500
|
reasoning=f"Fallback decision due to: {error_message}",
|
|
501
|
+
recovery_strategy=RecoveryStrategy.RETRY,
|
|
490
502
|
modified_task_content=None,
|
|
491
503
|
)
|
|
492
504
|
|
|
@@ -16,8 +16,11 @@ from collections import defaultdict, deque
|
|
|
16
16
|
from enum import Enum
|
|
17
17
|
from typing import Dict, List, Optional, Set
|
|
18
18
|
|
|
19
|
+
from camel.logger import get_logger
|
|
19
20
|
from camel.tasks import Task
|
|
20
21
|
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class PacketStatus(Enum):
|
|
23
26
|
r"""The status of a packet. The packet can be in one of the following
|
|
@@ -269,6 +272,46 @@ class TaskChannel:
|
|
|
269
272
|
async with self._condition:
|
|
270
273
|
return list(self._task_by_status[PacketStatus.ARCHIVED])
|
|
271
274
|
|
|
275
|
+
async def get_in_flight_tasks(self, publisher_id: str) -> List[Task]:
|
|
276
|
+
r"""Get all tasks that are currently in-flight (SENT, RETURNED
|
|
277
|
+
or PROCESSING) published by the given publisher.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
publisher_id (str): The ID of the publisher whose
|
|
281
|
+
in-flight tasks to retrieve.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List[Task]: List of tasks that are currently in-flight.
|
|
285
|
+
"""
|
|
286
|
+
async with self._condition:
|
|
287
|
+
in_flight_tasks = []
|
|
288
|
+
seen_task_ids = set() # Track seen IDs for duplicate detection
|
|
289
|
+
|
|
290
|
+
# Get tasks with SENT, RETURNED or PROCESSING
|
|
291
|
+
# status published by this publisher
|
|
292
|
+
for status in [
|
|
293
|
+
PacketStatus.SENT,
|
|
294
|
+
PacketStatus.PROCESSING,
|
|
295
|
+
PacketStatus.RETURNED,
|
|
296
|
+
]:
|
|
297
|
+
for task_id in self._task_by_status[status]:
|
|
298
|
+
if task_id in self._task_dict:
|
|
299
|
+
packet = self._task_dict[task_id]
|
|
300
|
+
if packet.publisher_id == publisher_id:
|
|
301
|
+
# Defensive check: detect if task appears in
|
|
302
|
+
# multiple status sets (should never happen)
|
|
303
|
+
if task_id in seen_task_ids:
|
|
304
|
+
logger.warning(
|
|
305
|
+
f"Task {task_id} found in multiple "
|
|
306
|
+
f"status sets. This indicates a bug in "
|
|
307
|
+
f"status management."
|
|
308
|
+
)
|
|
309
|
+
continue
|
|
310
|
+
in_flight_tasks.append(packet.task)
|
|
311
|
+
seen_task_ids.add(task_id)
|
|
312
|
+
|
|
313
|
+
return in_flight_tasks
|
|
314
|
+
|
|
272
315
|
async def get_task_by_id(self, task_id: str) -> Task:
|
|
273
316
|
r"""Get a task from the channel by its ID."""
|
|
274
317
|
async with self._condition:
|
|
@@ -42,6 +42,36 @@ class TaskResult(BaseModel):
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
class QualityEvaluation(BaseModel):
|
|
46
|
+
r"""Quality evaluation result for a completed task.
|
|
47
|
+
|
|
48
|
+
.. deprecated::
|
|
49
|
+
Use :class:`TaskAnalysisResult` instead. This class is kept for
|
|
50
|
+
backward compatibility.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
quality_sufficient: bool = Field(
|
|
54
|
+
description="Whether the task result meets quality standards."
|
|
55
|
+
)
|
|
56
|
+
quality_score: int = Field(
|
|
57
|
+
description="Quality score from 0 to 100.", ge=0, le=100
|
|
58
|
+
)
|
|
59
|
+
issues: List[str] = Field(
|
|
60
|
+
default_factory=list,
|
|
61
|
+
description="List of quality issues found in the result.",
|
|
62
|
+
)
|
|
63
|
+
recovery_strategy: Optional[str] = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
description="Recommended recovery strategy if quality is "
|
|
66
|
+
"insufficient: "
|
|
67
|
+
"'retry', 'reassign', 'replan', or 'decompose'.",
|
|
68
|
+
)
|
|
69
|
+
modified_task_content: Optional[str] = Field(
|
|
70
|
+
default=None,
|
|
71
|
+
description="Modified task content for replan strategy.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
45
75
|
class TaskAssignment(BaseModel):
|
|
46
76
|
r"""An individual task assignment within a batch."""
|
|
47
77
|
|
|
@@ -52,7 +82,8 @@ class TaskAssignment(BaseModel):
|
|
|
52
82
|
dependencies: List[str] = Field(
|
|
53
83
|
default_factory=list,
|
|
54
84
|
description="List of task IDs that must complete before this task. "
|
|
55
|
-
"This is critical for the task decomposition and
|
|
85
|
+
"This is critical for the task decomposition and "
|
|
86
|
+
"execution.",
|
|
56
87
|
)
|
|
57
88
|
|
|
58
89
|
# Allow LLMs to output dependencies as a comma-separated string or empty
|
|
@@ -60,7 +91,8 @@ class TaskAssignment(BaseModel):
|
|
|
60
91
|
# downstream logic does not break with validation errors.
|
|
61
92
|
@staticmethod
|
|
62
93
|
def _split_and_strip(dep_str: str) -> List[str]:
|
|
63
|
-
r"""Utility to split a comma separated string and strip
|
|
94
|
+
r"""Utility to split a comma separated string and strip
|
|
95
|
+
whitespace."""
|
|
64
96
|
return [d.strip() for d in dep_str.split(',') if d.strip()]
|
|
65
97
|
|
|
66
98
|
@field_validator("dependencies", mode="before")
|
|
@@ -74,7 +106,8 @@ class TaskAssignment(BaseModel):
|
|
|
74
106
|
|
|
75
107
|
|
|
76
108
|
class TaskAssignResult(BaseModel):
|
|
77
|
-
r"""The result of task assignment for both single and batch
|
|
109
|
+
r"""The result of task assignment for both single and batch
|
|
110
|
+
assignments."""
|
|
78
111
|
|
|
79
112
|
assignments: List[TaskAssignment] = Field(
|
|
80
113
|
description="List of task assignments."
|
|
@@ -88,6 +121,7 @@ class RecoveryStrategy(str, Enum):
|
|
|
88
121
|
REPLAN = "replan"
|
|
89
122
|
DECOMPOSE = "decompose"
|
|
90
123
|
CREATE_WORKER = "create_worker"
|
|
124
|
+
REASSIGN = "reassign"
|
|
91
125
|
|
|
92
126
|
def __str__(self):
|
|
93
127
|
return self.value
|
|
@@ -116,17 +150,75 @@ class FailureContext(BaseModel):
|
|
|
116
150
|
)
|
|
117
151
|
|
|
118
152
|
|
|
119
|
-
class
|
|
120
|
-
r"""
|
|
153
|
+
class TaskAnalysisResult(BaseModel):
|
|
154
|
+
r"""Unified result for task failure analysis and quality evaluation.
|
|
155
|
+
|
|
156
|
+
This model combines both failure recovery decisions and quality evaluation
|
|
157
|
+
results into a single structure. For failure analysis, only the recovery
|
|
158
|
+
strategy and reasoning fields are populated. For quality evaluation, all
|
|
159
|
+
fields including quality_score and issues are populated.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# Common fields - always populated
|
|
163
|
+
reasoning: str = Field(
|
|
164
|
+
description="Explanation for the analysis result or recovery "
|
|
165
|
+
"decision"
|
|
166
|
+
)
|
|
121
167
|
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
recovery_strategy: Optional[RecoveryStrategy] = Field(
|
|
169
|
+
default=None,
|
|
170
|
+
description="Recommended recovery strategy: 'retry', 'replan', "
|
|
171
|
+
"'decompose', 'create_worker', or 'reassign'. None indicates no "
|
|
172
|
+
"recovery needed (quality sufficient).",
|
|
124
173
|
)
|
|
125
|
-
|
|
174
|
+
|
|
126
175
|
modified_task_content: Optional[str] = Field(
|
|
127
|
-
default=None,
|
|
176
|
+
default=None,
|
|
177
|
+
description="Modified task content if strategy requires replan",
|
|
128
178
|
)
|
|
129
179
|
|
|
180
|
+
# Quality-specific fields - populated only for quality evaluation
|
|
181
|
+
quality_score: Optional[int] = Field(
|
|
182
|
+
default=None,
|
|
183
|
+
description="Quality score from 0 to 100 (only for quality "
|
|
184
|
+
"evaluation). "
|
|
185
|
+
"None indicates this is a failure analysis, "
|
|
186
|
+
"not quality evaluation.",
|
|
187
|
+
ge=0,
|
|
188
|
+
le=100,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
issues: List[str] = Field(
|
|
192
|
+
default_factory=list,
|
|
193
|
+
description="List of issues found. For failures: error details. "
|
|
194
|
+
"For quality evaluation: quality issues.",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def is_quality_evaluation(self) -> bool:
|
|
199
|
+
r"""Check if this is a quality evaluation result.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
bool: True if this is a quality evaluation (has quality_score),
|
|
203
|
+
False if this is a failure analysis.
|
|
204
|
+
"""
|
|
205
|
+
return self.quality_score is not None
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def quality_sufficient(self) -> bool:
|
|
209
|
+
r"""For quality evaluations, check if quality meets standards.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
bool: True if quality is sufficient (score >= 70 and no recovery
|
|
213
|
+
strategy recommended), False otherwise. Always False for
|
|
214
|
+
failure analysis results.
|
|
215
|
+
"""
|
|
216
|
+
return (
|
|
217
|
+
self.quality_score is not None
|
|
218
|
+
and self.quality_score >= 70
|
|
219
|
+
and self.recovery_strategy is None
|
|
220
|
+
)
|
|
221
|
+
|
|
130
222
|
|
|
131
223
|
def check_if_running(
|
|
132
224
|
running: bool,
|
|
@@ -178,7 +270,7 @@ def check_if_running(
|
|
|
178
270
|
if retries < max_retries:
|
|
179
271
|
logger.warning(
|
|
180
272
|
f"{error_msg} Retrying in {retry_delay}s... "
|
|
181
|
-
f"(Attempt {retries+1}/{max_retries})"
|
|
273
|
+
f"(Attempt {retries + 1}/{max_retries})"
|
|
182
274
|
)
|
|
183
275
|
time.sleep(retry_delay)
|
|
184
276
|
retries += 1
|
|
@@ -200,7 +292,7 @@ def check_if_running(
|
|
|
200
292
|
logger.warning(
|
|
201
293
|
f"Exception in {func.__name__}: {e}. "
|
|
202
294
|
f"Retrying in {retry_delay}s... "
|
|
203
|
-
f"(Attempt {retries+1}/{max_retries})"
|
|
295
|
+
f"(Attempt {retries + 1}/{max_retries})"
|
|
204
296
|
)
|
|
205
297
|
time.sleep(retry_delay)
|
|
206
298
|
retries += 1
|
|
@@ -218,7 +310,8 @@ def check_if_running(
|
|
|
218
310
|
# This should not be reached, but just in case
|
|
219
311
|
if handle_exceptions:
|
|
220
312
|
logger.error(
|
|
221
|
-
f"Unexpected failure in {func.__name__}:
|
|
313
|
+
f"Unexpected failure in {func.__name__}: "
|
|
314
|
+
f"{last_exception}"
|
|
222
315
|
)
|
|
223
316
|
return None
|
|
224
317
|
else:
|