Route create_content by content_type instead of URL presence

Content Creation tasks with a URL were incorrectly routed to the
optimization path. Now the scheduler sets content_type from Work
Category, and the tool routes on that. Chat callers fall back to
URL-based detection when content_type is empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix/customer-field-migration
PeninsulaInd 2026-03-08 20:58:07 -05:00
parent 3ed7f2bbdd
commit 85d9fc7a1f
3 changed files with 69 additions and 16 deletions

View File

@ -435,7 +435,7 @@ class Scheduler:
field_mapping = mapping.get("field_mapping", {}) field_mapping = mapping.get("field_mapping", {})
missing_clickup = [field_mapping.get(f, f) for f in missing] missing_clickup = [field_mapping.get(f, f) for f in missing]
msg = f"Skipped: missing required field(s): {', '.join(missing_clickup)}" msg = f"Skipped: missing required field(s): {', '.join(missing_clickup)}"
log.info("Skipping ClickUp task %s (%s) — %s", task_id, task.name, msg) log.debug("Skipping ClickUp task %s (%s) — %s", task_id, task.name, msg)
self._notify( self._notify(
f"Skipped ClickUp task: **{task.name}**\n{msg}", f"Skipped ClickUp task: **{task.name}**\n{msg}",
category="clickup", category="clickup",
@ -453,6 +453,13 @@ class Scheduler:
args["clickup_task_id"] = task_id args["clickup_task_id"] = task_id
args["clickup_task_status"] = task.status args["clickup_task_status"] = task.status
# Map Work Category to content_type so create_content routes correctly
if tool_name == "create_content":
if task.task_type == "On Page Optimization":
args["content_type"] = "on page optimization"
elif task.task_type == "Content Creation":
args["content_type"] = "new content"
# Execute the skill via the tool registry # Execute the skill via the tool registry
if hasattr(self.agent, "_tools") and self.agent._tools: if hasattr(self.agent, "_tools") and self.agent._tools:
result = self.agent._tools.execute(tool_name, args) result = self.agent._tools.execute(tool_name, args)

View File

@ -741,10 +741,10 @@ def _sync_clickup_optimization_complete(
@tool( @tool(
"create_content", "create_content",
"Two-phase SEO content creation: Phase 1 researches + outlines, Phase 2 writes " "SEO content creation. Set content_type='new content' for new pages "
"full content from the approved outline. Auto-detects phase from ClickUp task " "(Phase 1 outline → Phase 2 full write), or content_type='on page optimization' "
"status ('outline approved' → Phase 2). " "to optimize an existing page with Cora data. Auto-detects phase from ClickUp "
"Auto-detects content type from URL presence if not specified.", "task status ('outline approved' → Phase 2). Ask the user which type if unclear.",
category="content", category="content",
) )
def create_content( def create_content(
@ -758,8 +758,9 @@ def create_content(
Args: Args:
keyword: Primary target keyword (e.g. "plumbing services"). keyword: Primary target keyword (e.g. "plumbing services").
url: Target page URL. If provided on-page optimization; if empty new content. url: Target page URL (optional for new content, required for optimization).
content_type: Type of content. Auto-detected from URL if empty. content_type: 'new content' or 'on page optimization'. Controls routing.
If empty, inferred from URL presence as fallback.
cli_flags: Optional flags (e.g. "service" for service page hint). cli_flags: Optional flags (e.g. "service" for service page hint).
""" """
if not keyword: if not keyword:
@ -805,9 +806,10 @@ def create_content(
capabilities_default = config.content.company_capabilities_default if config else "" capabilities_default = config.content.company_capabilities_default if config else ""
# Optimization path: URL present → run Phase 3 test block pipeline # Optimization path: content_type determines route (URL fallback for chat callers)
# (skips the outline gate entirely) if content_type.lower() == "on page optimization":
if url: if not url:
return "Error: On Page Optimization requires a URL (IMSURL field)."
return _run_optimization( return _run_optimization(
agent=agent, agent=agent,
config=config, config=config,

View File

@ -884,8 +884,8 @@ class TestSyncClickupOptimizationComplete:
class TestCreateContentRouting: class TestCreateContentRouting:
@patch("cheddahbot.tools.content_creation._run_optimization") @patch("cheddahbot.tools.content_creation._run_optimization")
def test_url_present_routes_to_optimization(self, mock_opt, tmp_db, tmp_path): def test_explicit_optimization_routes_correctly(self, mock_opt, tmp_db, tmp_path):
"""When URL is present, create_content should call _run_optimization.""" """When content_type='on page optimization', routes to _run_optimization."""
mock_opt.return_value = "## Optimization Complete" mock_opt.return_value = "## Optimization Complete"
cfg = Config() cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines")) cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
@ -898,14 +898,15 @@ class TestCreateContentRouting:
result = create_content( result = create_content(
keyword="plumbing services", keyword="plumbing services",
url="https://example.com/plumbing", url="https://example.com/plumbing",
content_type="on page optimization",
ctx=ctx, ctx=ctx,
) )
mock_opt.assert_called_once() mock_opt.assert_called_once()
assert result == "## Optimization Complete" assert result == "## Optimization Complete"
@patch("cheddahbot.tools.content_creation._run_optimization") @patch("cheddahbot.tools.content_creation._run_optimization")
def test_no_url_does_not_route_to_optimization(self, mock_opt, tmp_db, tmp_path): def test_explicit_new_content_with_url_routes_to_phase1(self, mock_opt, tmp_db, tmp_path):
"""When URL is empty, create_content should NOT call _run_optimization.""" """Content Creation with URL should go to Phase 1, NOT optimization."""
cfg = Config() cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines")) cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
agent = MagicMock() agent = MagicMock()
@ -918,15 +919,58 @@ class TestCreateContentRouting:
} }
result = create_content( result = create_content(
keyword="new keyword", keyword="new keyword",
url="", url="https://example.com/future-page",
content_type="new content",
ctx=ctx, ctx=ctx,
) )
mock_opt.assert_not_called() mock_opt.assert_not_called()
assert "Phase 1 Complete" in result assert "Phase 1 Complete" in result
@patch("cheddahbot.tools.content_creation._run_optimization")
def test_optimization_without_url_returns_error(self, mock_opt, tmp_db, tmp_path):
"""On Page Optimization without URL should return an error."""
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
ctx = {
"agent": MagicMock(),
"config": cfg,
"db": tmp_db,
"clickup_task_id": "",
}
result = create_content(
keyword="plumbing services",
url="",
content_type="on page optimization",
ctx=ctx,
)
mock_opt.assert_not_called()
assert "Error" in result
assert "URL" in result
@patch("cheddahbot.tools.content_creation._run_optimization")
def test_fallback_url_routes_to_optimization(self, mock_opt, tmp_db, tmp_path):
"""When content_type is empty and URL present, falls back to optimization."""
mock_opt.return_value = "## Optimization Complete"
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
ctx = {
"agent": MagicMock(),
"config": cfg,
"db": tmp_db,
"clickup_task_id": "routing_test",
}
result = create_content(
keyword="plumbing services",
url="https://example.com/plumbing",
content_type="",
ctx=ctx,
)
mock_opt.assert_called_once()
assert result == "## Optimization Complete"
@patch("cheddahbot.tools.content_creation._run_optimization") @patch("cheddahbot.tools.content_creation._run_optimization")
def test_new_content_still_calls_phase1(self, mock_opt, tmp_db, tmp_path): def test_new_content_still_calls_phase1(self, mock_opt, tmp_db, tmp_path):
"""Regression: new content (no URL) still goes through _run_phase1.""" """Regression: new content (no URL, no content_type) still goes through _run_phase1."""
cfg = Config() cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines")) cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
agent = MagicMock() agent = MagicMock()