Add retry logic to ClickUp API writes and fix review status
- Add _retry() helper to ClickUpClient (3 attempts, exponential backoff, only retries 5xx/transport errors) - Apply retry to update_task_status, add_comment, upload_attachment - Fix review_status from "review" to "internal review" in config and defaults - Update SOUL.md personality and USER.md profile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
cf1faceab1
commit
46bc38106e
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -144,30 +145,58 @@ class ClickUpClient:
|
||||||
log.info("Found %d tasks across %d lists in space %s", len(all_tasks), len(list_ids), space_id)
|
log.info("Found %d tasks across %d lists in space %s", len(all_tasks), len(list_ids), space_id)
|
||||||
return all_tasks
|
return all_tasks
|
||||||
|
|
||||||
# ── Write ──
|
# ── Write (with retry) ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _retry(fn, max_attempts: int = 3, backoff: float = 2.0):
|
||||||
|
"""Call *fn* up to *max_attempts* times with exponential backoff.
|
||||||
|
|
||||||
|
Returns the result of *fn* on success, or re-raises the last exception.
|
||||||
|
Only retries on network/transport errors and 5xx status errors.
|
||||||
|
"""
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
try:
|
||||||
|
return fn()
|
||||||
|
except (httpx.TransportError, httpx.HTTPStatusError) as e:
|
||||||
|
# Don't retry 4xx client errors (bad request, auth, not found, etc.)
|
||||||
|
if isinstance(e, httpx.HTTPStatusError) and e.response.status_code < 500:
|
||||||
|
raise
|
||||||
|
last_exc = e
|
||||||
|
if attempt < max_attempts:
|
||||||
|
wait = backoff ** attempt
|
||||||
|
log.warning("Retry %d/%d after %.1fs: %s", attempt, max_attempts, wait, e)
|
||||||
|
time.sleep(wait)
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
def update_task_status(self, task_id: str, status: str) -> bool:
|
def update_task_status(self, task_id: str, status: str) -> bool:
|
||||||
"""Update a task's status."""
|
"""Update a task's status."""
|
||||||
try:
|
try:
|
||||||
|
def _call():
|
||||||
resp = self._client.put(f"/task/{task_id}", json={"status": status})
|
resp = self._client.put(f"/task/{task_id}", json={"status": status})
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
self._retry(_call)
|
||||||
log.info("Updated task %s status to '%s'", task_id, status)
|
log.info("Updated task %s status to '%s'", task_id, status)
|
||||||
return True
|
return True
|
||||||
except httpx.HTTPStatusError as e:
|
except (httpx.TransportError, httpx.HTTPStatusError) as e:
|
||||||
log.error("Failed to update task %s status: %s", task_id, e)
|
log.error("Failed to update task %s status: %s", task_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_comment(self, task_id: str, text: str) -> bool:
|
def add_comment(self, task_id: str, text: str) -> bool:
|
||||||
"""Add a comment to a task."""
|
"""Add a comment to a task."""
|
||||||
try:
|
try:
|
||||||
|
def _call():
|
||||||
resp = self._client.post(
|
resp = self._client.post(
|
||||||
f"/task/{task_id}/comment",
|
f"/task/{task_id}/comment",
|
||||||
json={"comment_text": text},
|
json={"comment_text": text},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
self._retry(_call)
|
||||||
log.info("Added comment to task %s", task_id)
|
log.info("Added comment to task %s", task_id)
|
||||||
return True
|
return True
|
||||||
except httpx.HTTPStatusError as e:
|
except (httpx.TransportError, httpx.HTTPStatusError) as e:
|
||||||
log.error("Failed to add comment to task %s: %s", task_id, e)
|
log.error("Failed to add comment to task %s: %s", task_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -183,6 +212,7 @@ class ClickUpClient:
|
||||||
log.warning("Attachment file not found: %s", fp)
|
log.warning("Attachment file not found: %s", fp)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
|
def _call():
|
||||||
with open(fp, "rb") as f:
|
with open(fp, "rb") as f:
|
||||||
resp = httpx.post(
|
resp = httpx.post(
|
||||||
f"{BASE_URL}/task/{task_id}/attachment",
|
f"{BASE_URL}/task/{task_id}/attachment",
|
||||||
|
|
@ -191,9 +221,11 @@ class ClickUpClient:
|
||||||
timeout=60.0,
|
timeout=60.0,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
self._retry(_call)
|
||||||
log.info("Uploaded attachment %s to task %s", fp.name, task_id)
|
log.info("Uploaded attachment %s to task %s", fp.name, task_id)
|
||||||
return True
|
return True
|
||||||
except httpx.HTTPStatusError as e:
|
except (httpx.TransportError, httpx.HTTPStatusError) as e:
|
||||||
log.warning("Failed to upload attachment to task %s: %s", task_id, e)
|
log.warning("Failed to upload attachment to task %s: %s", task_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class ClickUpConfig:
|
||||||
space_id: str = ""
|
space_id: str = ""
|
||||||
poll_interval_minutes: int = 20
|
poll_interval_minutes: int = 20
|
||||||
poll_statuses: list[str] = field(default_factory=lambda: ["to do"])
|
poll_statuses: list[str] = field(default_factory=lambda: ["to do"])
|
||||||
review_status: str = "review"
|
review_status: str = "internal review"
|
||||||
in_progress_status: str = "in progress"
|
in_progress_status: str = "in progress"
|
||||||
task_type_field_name: str = "Work Category"
|
task_type_field_name: str = "Work Category"
|
||||||
default_auto_execute: bool = False
|
default_auto_execute: bool = False
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ email:
|
||||||
clickup:
|
clickup:
|
||||||
poll_interval_minutes: 20 # 3x per hour
|
poll_interval_minutes: 20 # 3x per hour
|
||||||
poll_statuses: ["to do"]
|
poll_statuses: ["to do"]
|
||||||
review_status: "review"
|
review_status: "internal review"
|
||||||
in_progress_status: "in progress"
|
in_progress_status: "in progress"
|
||||||
task_type_field_name: "Work Category"
|
task_type_field_name: "Work Category"
|
||||||
default_auto_execute: false
|
default_auto_execute: false
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
# Soul
|
# Soul
|
||||||
|
|
||||||
You are Cheddah, a sharp and resourceful AI assistant.
|
You are Cheddah, a sharp and resourceful AI assistant. You are built on the avatar of Winston Churchill. Smart, confident, but friendly.
|
||||||
|
|
||||||
## Personality
|
## Personality
|
||||||
- Direct, no-nonsense, but warm
|
- Direct, no-nonsense, but warm
|
||||||
- You use humor when appropriate
|
- You use humor when appropriate
|
||||||
- You're proactive - suggest things before being asked
|
- You're proactive - suggest things before being asked. Especially as you begin to notice patterns in my behavior and schedule you can recommend items that fit.
|
||||||
|
- You are a good designer (css/html/js) and Bryan is not so you should help him when you can. If we don't have a designer skill installed you should go find a few and recommend one or two to include that you can use.
|
||||||
- You remember what the user tells you and reference it naturally
|
- You remember what the user tells you and reference it naturally
|
||||||
- You adapt your communication style to match the user's preferences
|
- You adapt your communication style to match the user's preferences
|
||||||
|
|
||||||
|
|
@ -16,5 +17,6 @@ You are Cheddah, a sharp and resourceful AI assistant.
|
||||||
- Ask for clarification rather than guessing on important decisions
|
- Ask for clarification rather than guessing on important decisions
|
||||||
|
|
||||||
## Quirks
|
## Quirks
|
||||||
- You occasionally use the word "cheddah" as slang for money/value
|
- You like to open converstions with random trivia facts - but only once at the begining per chat.
|
||||||
|
- You may use puns or quotes from Winston Churchill that fit the situation - but not too forced.
|
||||||
- You appreciate efficiency and elegant solutions
|
- You appreciate efficiency and elegant solutions
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
# User Profile
|
# User Profile
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
- Name: (your name here)
|
- Name: Bryan
|
||||||
- How to address: (first name, nickname, etc.)
|
- How to address: Bryan or Yan
|
||||||
- Origin: Cheddah is named after the user's Xbox Live gamertag, "CheddahYetti."
|
- Origin: Cheddah is named after the user's Xbox Live gamertag, "CheddahYetti."
|
||||||
- Fun Fact: The name is a nod to living in Wisconsin and the user being a "big guy."
|
- Fun Fact: The name is a nod to living in Wisconsin and the user being a "big guy."
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
- Technical level: (beginner/intermediate/advanced)
|
- Technical level: (advanced in Python)
|
||||||
- Primary language: Python
|
- Primary language: Python
|
||||||
- Working on: (current projects)
|
- Working on: (current projects)
|
||||||
|
|
||||||
|
|
||||||
## Preferences
|
## Preferences
|
||||||
- Communication style: (concise/detailed)
|
- Communication style: Likes to know what is going on before giving the go-ahead, but once he understands is willing to let the agents go to work without any input.
|
||||||
- (anything else you want the agent to know)
|
- If he says he is going to bed or going away for awhile, you can ask if we can run in unattended mode - he'll usually say yes.
|
||||||
|
- Simple code is better - he's the only guy in the shop so dont do code like it's for an enterprise. Simple and maintainable is important.
|
||||||
|
- He needs help with organization, and he really needs help keeping project documentation up to date. Do that for him.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue