diff --git a/cheddahbot/scheduler.py b/cheddahbot/scheduler.py index f88c40d..4e4ce7b 100644 --- a/cheddahbot/scheduler.py +++ b/cheddahbot/scheduler.py @@ -318,14 +318,16 @@ class Scheduler: state["started_at"] = now self.db.kv_set(kv_key, json.dumps(state)) - # Set ClickUp status to "in progress" client = self._get_clickup_client() - client.update_task_status(task_id, self.config.clickup.in_progress_status) try: # Build tool arguments from field mapping args = self._build_tool_args(state) + # Pass clickup_task_id so the tool can handle its own ClickUp sync + # (status updates, comments, attachments) if it supports it. + args["clickup_task_id"] = task_id + # Execute the skill via the tool registry if hasattr(self.agent, "_tools") and self.agent._tools: result = self.agent._tools.execute(skill_name, args) @@ -335,30 +337,46 @@ class Scheduler: f"Task description: {state.get('custom_fields', {})}" ) - # Extract and upload any docx deliverables - docx_paths = _extract_docx_paths(result) - state["deliverable_paths"] = docx_paths - uploaded_count = 0 - for path in docx_paths: - if client.upload_attachment(task_id, path): - uploaded_count += 1 - else: - log.warning("Failed to upload %s for task %s", path, task_id) + # Check if the tool already handled ClickUp sync internally + tool_handled_sync = "## ClickUp Sync" in result - # Success - state["state"] = "completed" - state["completed_at"] = datetime.now(UTC).isoformat() - self.db.kv_set(kv_key, json.dumps(state)) + if tool_handled_sync: + # Tool did its own status updates, comments, and attachments. + # Just update the kv_store state. + state["state"] = "completed" + state["completed_at"] = datetime.now(UTC).isoformat() + self.db.kv_set(kv_key, json.dumps(state)) + else: + # Tool doesn't handle sync — scheduler does it (fallback path). + # Set status to "in progress" (tool didn't do it) + client.update_task_status(task_id, self.config.clickup.in_progress_status) - # Update ClickUp - client.update_task_status(task_id, self.config.clickup.review_status) - attach_note = f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" - comment = ( - f"āœ… CheddahBot completed this task.\n\n" - f"Skill: {skill_name}\n" - f"Result:\n{result[:3000]}{attach_note}" - ) - client.add_comment(task_id, comment) + # Extract and upload any docx deliverables + docx_paths = _extract_docx_paths(result) + state["deliverable_paths"] = docx_paths + uploaded_count = 0 + for path in docx_paths: + if client.upload_attachment(task_id, path): + uploaded_count += 1 + else: + log.warning("Failed to upload %s for task %s", path, task_id) + + # Success + state["state"] = "completed" + state["completed_at"] = datetime.now(UTC).isoformat() + self.db.kv_set(kv_key, json.dumps(state)) + + # Update ClickUp + client.update_task_status(task_id, self.config.clickup.review_status) + attach_note = ( + f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" + ) + comment = ( + f"āœ… CheddahBot completed this task.\n\n" + f"Skill: {skill_name}\n" + f"Result:\n{result[:3000]}{attach_note}" + ) + client.add_comment(task_id, comment) self._notify( f"ClickUp task completed: **{task_name}**\n" diff --git a/cheddahbot/tools/__init__.py b/cheddahbot/tools/__init__.py index 3a140ee..c1dd3f6 100644 --- a/cheddahbot/tools/__init__.py +++ b/cheddahbot/tools/__init__.py @@ -159,6 +159,15 @@ class ToolRegistry: "memory": self.agent._memory, "agent_registry": self.agent_registry, } + + # Filter args to only params the function accepts (plus **kwargs) + has_var_keyword = any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + if not has_var_keyword: + accepted = set(sig.parameters.keys()) + args = {k: v for k, v in args.items() if k in accepted} + result = tool_def.func(**args) return str(result) if result is not None else "Done." except Exception as e: diff --git a/cheddahbot/tools/press_release.py b/cheddahbot/tools/press_release.py index e69b33e..48ccc39 100644 --- a/cheddahbot/tools/press_release.py +++ b/cheddahbot/tools/press_release.py @@ -43,6 +43,24 @@ def _set_status(ctx: dict | None, message: str) -> None: ctx["db"].kv_set("pipeline:status", message) +def _get_clickup_client(ctx: dict | None): + """Create a ClickUpClient from tool context, or None if unavailable.""" + if not ctx or not ctx.get("config") or not ctx["config"].clickup.enabled: + return None + try: + from ..clickup import ClickUpClient + + config = ctx["config"] + return ClickUpClient( + api_token=config.clickup.api_token, + workspace_id=config.clickup.workspace_id, + task_type_field_name=config.clickup.task_type_field_name, + ) + except Exception as e: + log.warning("Could not create ClickUp client: %s", e) + return None + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -394,6 +412,25 @@ def write_press_releases( agent = ctx["agent"] + # ── ClickUp: set "in progress" and post starting comment ──────────── + cu_client = None + if clickup_task_id: + cu_client = _get_clickup_client(ctx) + if cu_client: + try: + config = ctx["config"] + cu_client.update_task_status( + clickup_task_id, config.clickup.in_progress_status + ) + cu_client.add_comment( + clickup_task_id, + f"šŸ”„ CheddahBot starting press release creation.\n\n" + f"Topic: {topic}\nCompany: {company_name}", + ) + log.info("ClickUp task %s set to in-progress", clickup_task_id) + except Exception as e: + log.warning("ClickUp start-sync failed for %s: %s", clickup_task_id, e) + # Load skill prompts try: pr_skill = _load_skill("press_release_prompt.md") @@ -550,6 +587,25 @@ def write_press_releases( text_to_docx(clean_result, docx_path) docx_files.append(str(docx_path)) + # ── ClickUp: upload docx attachments + comment ───────────────────── + uploaded_count = 0 + if clickup_task_id and cu_client: + try: + for path in docx_files: + if cu_client.upload_attachment(clickup_task_id, path): + uploaded_count += 1 + else: + log.warning("ClickUp: failed to upload %s for task %s", path, clickup_task_id) + cu_client.add_comment( + clickup_task_id, + f"šŸ“Ž Saved {len(docx_files)} press release(s). " + f"{uploaded_count} file(s) attached.\n" + f"Generating JSON-LD schemas next...", + ) + log.info("ClickUp: uploaded %d attachments for task %s", uploaded_count, clickup_task_id) + except Exception as e: + log.warning("ClickUp attachment upload failed for %s: %s", clickup_task_id, e) + # ── Step 4: Generate 2 JSON-LD schemas (Sonnet + WebSearch) ─────────── log.info("[PR Pipeline] Step 4/4: Generating 2 JSON-LD schemas...") schema_texts: list[str] = [] @@ -629,54 +685,35 @@ def write_press_releases( output_parts.append(f"| {c['step']} | {c['model']} | {c['elapsed_s']} |") output_parts.append(f"| **Total** | | **{round(total_elapsed, 1)}** |") - # ── ClickUp sync (when triggered from chat with a task ID) ─────────── - if clickup_task_id and ctx and ctx.get("config") and ctx["config"].clickup.enabled: + # ── ClickUp: completion — status to review + final comment ────────── + if clickup_task_id and cu_client: try: - from ..clickup import ClickUpClient - config = ctx["config"] - client = ClickUpClient( - api_token=config.clickup.api_token, - workspace_id=config.clickup.workspace_id, - task_type_field_name=config.clickup.task_type_field_name, - ) - # Upload each .docx as an attachment - uploaded_count = 0 - for path in docx_files: - if client.upload_attachment(clickup_task_id, path): - uploaded_count += 1 - else: - log.warning("ClickUp: failed to upload %s for task %s", path, clickup_task_id) - - # Post a result comment + # Post completion comment attach_note = f"\nšŸ“Ž {uploaded_count} file(s) attached." if uploaded_count else "" result_text = "\n".join(output_parts)[:3000] comment = ( - f"āœ… CheddahBot completed this task (via chat).\n\n" + f"āœ… CheddahBot completed this task.\n\n" f"Skill: write_press_releases\n" f"Result:\n{result_text}{attach_note}" ) - client.add_comment(clickup_task_id, comment) + cu_client.add_comment(clickup_task_id, comment) - # Update task status to review - client.update_task_status(clickup_task_id, config.clickup.review_status) + # Set status to internal review + cu_client.update_task_status(clickup_task_id, config.clickup.review_status) # Update kv_store state if one exists db = ctx.get("db") if db: - import json as _json - kv_key = f"clickup:task:{clickup_task_id}:state" existing = db.kv_get(kv_key) if existing: - state = _json.loads(existing) + state = json.loads(existing) state["state"] = "completed" state["completed_at"] = datetime.now(UTC).isoformat() state["deliverable_paths"] = docx_files - db.kv_set(kv_key, _json.dumps(state)) - - client.close() + db.kv_set(kv_key, json.dumps(state)) output_parts.append("\n## ClickUp Sync\n") output_parts.append(f"- Task `{clickup_task_id}` updated") @@ -689,6 +726,8 @@ def write_press_releases( output_parts.append("\n## ClickUp Sync\n") output_parts.append(f"- **Sync failed:** {e}") output_parts.append("- Press release results are still valid above") + finally: + cu_client.close() return "\n".join(output_parts) diff --git a/identity/USER.md b/identity/USER.md index 527eeda..e9096f0 100644 --- a/identity/USER.md +++ b/identity/USER.md @@ -17,3 +17,6 @@ - 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. + +## Strict Protocols +- STRICT METADATA ADHERENCE: You must never alias, rename, truncate, or 'clean up' task titles, client names, or IDs retrieved from the ClickUp API. All references to tasks must use the verbatim strings returned by the API. Proactive 'organization' of task data that alters the original text is strictly forbidden.