Skip to content

Commit a57b2a0

Browse files
authored
Merge pull request #121 from MLAI-AUS-Inc/codex/domain-scan-routing-fix
adding publish as PR skill
2 parents 2b52bc2 + 8c4f935 commit a57b2a0

File tree

7 files changed

+410
-9
lines changed

7 files changed

+410
-9
lines changed

roo-standalone/roo/agent.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,21 @@ def remember_thread_context(
146146
*,
147147
domain: Optional[str] = None,
148148
workflow: Optional[str] = None,
149+
active_job_id: Optional[str] = None,
149150
) -> None:
150151
"""Persist recent thread routing context so follow-ups stay on the right skill."""
151152
thread_key = self._thread_key(channel_id, thread_ts)
152153
if not thread_key or not skill_name:
153154
return
154155

156+
existing = self._thread_skill_context.get(thread_key, {})
155157
self._thread_skill_context[thread_key] = {
156-
"skill_name": skill_name,
157-
"domain": domain,
158-
"workflow": workflow,
158+
"skill_name": skill_name or existing.get("skill_name"),
159+
"domain": domain if domain is not None else existing.get("domain"),
160+
"workflow": workflow if workflow is not None else existing.get("workflow"),
161+
"active_job_id": (
162+
active_job_id if active_job_id is not None else existing.get("active_job_id")
163+
),
159164
"updated_at": datetime.now(timezone.utc),
160165
}
161166

@@ -227,6 +232,7 @@ def _get_routing_intent(
227232
text,
228233
thread_skill_name=(thread_context or {}).get("skill_name"),
229234
thread_domain=(thread_context or {}).get("domain"),
235+
thread_job_id=(thread_context or {}).get("active_job_id"),
230236
)
231237
if not route:
232238
return None

roo-standalone/roo/clients/mlai_backend.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,23 @@ async def publish_article(self, job_id: str, slack_user_id: str) -> dict:
945945
response.raise_for_status()
946946
return response.json()
947947

948+
async def publish_article_as_pr(self, job_id: str, slack_user_id: str) -> dict:
949+
"""Promote a completed content-only article into a draft-PR publish run."""
950+
if not self.base_url:
951+
raise ValueError("MLAI_BACKEND_URL not configured")
952+
953+
clean_id = self._clean_slack_id(slack_user_id)
954+
955+
async with httpx.AsyncClient() as client:
956+
response = await client.post(
957+
f"{self.base_url}/api/v1/content/jobs/{job_id}/publish-pr",
958+
json={"slack_user_id": clean_id},
959+
headers=self.headers,
960+
timeout=60.0,
961+
)
962+
response.raise_for_status()
963+
return response.json()
964+
948965
# =========================================================================
949966
# Missing Admin / Points Methods
950967
# =========================================================================

roo-standalone/roo/content_intent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
r"\bwhat\s+should\s+i\s+write\b",
3333
r"\b(?:recommend|suggest)\s+(?:a\s+)?(?:topic|article|keyword)\b",
3434
)
35+
PUBLISH_PR_PATTERNS = (
36+
r"\bpublish\b.*\b(?:article|bundle|draft|post)\b.*\bas\s+a\s+p\.?r\.?\b",
37+
r"\bpublish\b.*\bas\s+a\s+pull\s+request\b",
38+
r"\bturn\b.*\b(?:article|bundle|draft|post)\b.*\binto\s+a\s+p\.?r\.?\b",
39+
r"\bopen\b.*\b(?:a\s+)?(?:draft\s+)?p\.?r\.?\b",
40+
)
3541
WRITE_PATTERNS = (
3642
r"\bwrite\b.*\b(article|blog(?:\s+post)?|content)\b",
3743
r"\bgenerate\b.*\b(article|blog(?:\s+post)?|content)\b",
@@ -89,6 +95,8 @@ def detect_content_action(text: str) -> Optional[str]:
8995
return "scaffold"
9096
if any(re.search(pattern, text_lower) for pattern in RESEARCH_PATTERNS):
9197
return "research"
98+
if any(re.search(pattern, text_lower) for pattern in PUBLISH_PR_PATTERNS):
99+
return "publish_pr"
92100
if any(re.search(pattern, text_lower) for pattern in WRITE_PATTERNS):
93101
return "write"
94102
return None
@@ -108,6 +116,7 @@ def parse_routing_intent(
108116
*,
109117
thread_skill_name: Optional[str] = None,
110118
thread_domain: Optional[str] = None,
119+
thread_job_id: Optional[str] = None,
111120
) -> Optional[Dict[str, Any]]:
112121
"""Return a deterministic routing decision for common content flows."""
113122
normalized = normalize_slack_text(text)
@@ -150,6 +159,18 @@ def parse_routing_intent(
150159
params["domain"] = domain
151160
return {"skill_name": "content-factory", "params": params}
152161

162+
if (
163+
action == "publish_pr"
164+
and thread_skill_name == "content-factory"
165+
and (thread_job_id or thread_domain)
166+
):
167+
params = {"action": "publish_pr"}
168+
if thread_job_id:
169+
params["job_id"] = thread_job_id
170+
if domain:
171+
params["domain"] = domain
172+
return {"skill_name": "content-factory", "params": params}
173+
153174
if action == "write" and (
154175
domain or re.search(r"\b(?:article|blog(?:\s+post)?|content)\b", text_lower)
155176
):

roo-standalone/roo/main.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ async def _maybe_attach_content_factory_progress(
228228
if not message_ts:
229229
return
230230

231+
workflow = str(
232+
result_data.get("content_factory_workflow")
233+
or result_data.get("content_factory_watchdog_mode")
234+
or ""
235+
).strip() or None
236+
domain = str(result_data.get("content_factory_domain") or "").strip() or None
237+
231238
from .clients.mlai_backend import MLAIBackendClient
232239

233240
settings = get_settings()
@@ -248,6 +255,14 @@ async def _maybe_attach_content_factory_progress(
248255
print(f"⚠️ Failed to attach progress message {message_ts} to job {job_id}: {exc}")
249256
return
250257

258+
_remember_content_thread_context(
259+
channel_id,
260+
thread_ts,
261+
domain,
262+
workflow or "write",
263+
active_job_id=job_id,
264+
)
265+
251266
if result_data.get("content_factory_watchdog"):
252267
asyncio.create_task(_watch_content_factory_quiet_run(job_id))
253268

@@ -323,6 +338,13 @@ async def _trigger_article_generation_from_pending(
323338
)
324339
print(f"✅ Auto-generation triggered for {domain}")
325340
if result.get("job_id") or result.get("run_id"):
341+
_remember_content_thread_context(
342+
intent_channel,
343+
intent_thread,
344+
domain,
345+
"research" if include_decision_stage else "write",
346+
active_job_id=result.get("job_id") or result.get("run_id"),
347+
)
326348
asyncio.create_task(_watch_content_factory_quiet_run(result.get("job_id") or result.get("run_id")))
327349
return True
328350
except Exception as e:
@@ -341,6 +363,8 @@ def _remember_content_thread_context(
341363
thread_ts: str | None,
342364
domain: str | None,
343365
workflow: str,
366+
*,
367+
active_job_id: str | None = None,
344368
) -> None:
345369
"""Keep content-factory as the active skill for follow-ups in this thread."""
346370
if not channel_id or not thread_ts:
@@ -353,6 +377,7 @@ def _remember_content_thread_context(
353377
thread_ts,
354378
domain=domain,
355379
workflow=workflow,
380+
active_job_id=active_job_id,
356381
)
357382
except Exception as e:
358383
print(f"⚠️ Failed to persist content thread context: {e}")
@@ -2508,6 +2533,13 @@ async def slack_actions(request: Request):
25082533
)
25092534
print(f"✅ Article generation triggered for {domain}: {result}")
25102535
if result.get("job_id") or result.get("run_id"):
2536+
_remember_content_thread_context(
2537+
reply_channel,
2538+
reply_thread_ts,
2539+
domain,
2540+
"write",
2541+
active_job_id=result.get("job_id") or result.get("run_id"),
2542+
)
25112543
asyncio.create_task(_watch_content_factory_quiet_run(result.get("job_id") or result.get("run_id")))
25122544
except Exception as e:
25132545
print(f"❌ Failed to trigger article generation: {e}")
@@ -2636,6 +2668,13 @@ async def slack_actions(request: Request):
26362668
)
26372669
print(f"✅ Article discovery triggered for {domain}: {result}")
26382670
if str(result.get("status") or "").strip().lower() == "awaiting_delivery_mode":
2671+
_remember_content_thread_context(
2672+
reply_channel,
2673+
reply_thread_ts,
2674+
domain,
2675+
"awaiting_delivery_mode",
2676+
active_job_id=result.get("job_id") or result.get("run_id"),
2677+
)
26392678
try:
26402679
from .slack_client import get_slack_client
26412680
get_slack_client().chat_update(
@@ -2651,6 +2690,13 @@ async def slack_actions(request: Request):
26512690
except Exception as update_error:
26522691
print(f"⚠️ Failed to update delivery-mode prompt: {update_error}")
26532692
elif result.get("job_id") or result.get("run_id"):
2693+
_remember_content_thread_context(
2694+
reply_channel,
2695+
reply_thread_ts,
2696+
domain,
2697+
"research",
2698+
active_job_id=result.get("job_id") or result.get("run_id"),
2699+
)
26542700
asyncio.create_task(_watch_content_factory_quiet_run(result.get("job_id") or result.get("run_id")))
26552701
except Exception as e:
26562702
print(f"❌ Failed to trigger article discovery: {e}")
@@ -2761,6 +2807,13 @@ async def slack_actions(request: Request):
27612807
request_source=CONTENT_FACTORY_REQUEST_SOURCE,
27622808
)
27632809
if result.get("job_id") or result.get("run_id"):
2810+
_remember_content_thread_context(
2811+
payload.get("channel", {}).get("id"),
2812+
payload.get("message", {}).get("thread_ts") or payload.get("message", {}).get("ts"),
2813+
domain,
2814+
"awaiting_delivery_mode",
2815+
active_job_id=result.get("job_id") or result.get("run_id"),
2816+
)
27642817
asyncio.create_task(_watch_content_factory_quiet_run(result.get("job_id") or result.get("run_id")))
27652818

27662819
selected_label = "content-only" if delivery_mode == "content_only" else "publish-via-code"
@@ -3322,6 +3375,13 @@ async def slack_actions(request: Request):
33223375
)
33233376
print(f"✅ Topic confirmed for job {job_id}")
33243377
follow_up = _build_confirm_topic_follow_up(result)
3378+
_remember_content_thread_context(
3379+
payload.get("channel", {}).get("id"),
3380+
payload.get("message", {}).get("thread_ts") or payload.get("message", {}).get("ts"),
3381+
follow_up.get("domain"),
3382+
"awaiting_delivery_mode" if follow_up.get("requires_delivery_mode") else "write",
3383+
active_job_id=follow_up.get("active_job_id"),
3384+
)
33253385

33263386
try:
33273387
from .slack_client import get_slack_client

roo-standalone/roo/skills/executor.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ def _build_article_delivery_mode_prompt(
386386
"content_factory_progress_job_id": job_id,
387387
"content_factory_watchdog": False,
388388
"content_factory_watchdog_mode": "awaiting_delivery_mode",
389+
"content_factory_domain": domain,
390+
"content_factory_workflow": "awaiting_delivery_mode",
389391
},
390392
}
391393

@@ -1335,6 +1337,38 @@ def _build_content_factory_start_response(
13351337
"content_factory_progress_job_id": job_id,
13361338
"content_factory_watchdog": True,
13371339
"content_factory_watchdog_mode": workflow,
1340+
"content_factory_domain": domain,
1341+
"content_factory_workflow": workflow,
1342+
},
1343+
}
1344+
1345+
def _build_publish_pr_start_response(
1346+
self,
1347+
*,
1348+
domain: Optional[str],
1349+
job_id: str,
1350+
) -> dict:
1351+
display_domain = normalize_content_factory_domain(domain) or domain or "this domain"
1352+
return {
1353+
"message": (
1354+
f"Publishing the completed article bundle for {display_domain} as a draft PR. "
1355+
"I'll keep this message updated as the run moves forward."
1356+
),
1357+
"blocks": build_live_status_blocks(
1358+
display_domain,
1359+
summary_text=(
1360+
"Promoting the completed article bundle into the repo and preview flow. "
1361+
"I'll keep this message updated."
1362+
),
1363+
include_decision_stage=False,
1364+
current_stage="preparing",
1365+
),
1366+
"data": {
1367+
"content_factory_progress_job_id": job_id,
1368+
"content_factory_watchdog": True,
1369+
"content_factory_watchdog_mode": "publish_pr",
1370+
"content_factory_domain": display_domain,
1371+
"content_factory_workflow": "publish_pr",
13381372
},
13391373
}
13401374

@@ -1352,6 +1386,56 @@ def _get_connected_domain_info(
13521386
None,
13531387
)
13541388

1389+
async def _publish_content_bundle_as_pr(
1390+
self,
1391+
api_client,
1392+
*,
1393+
job_id: str,
1394+
domain: Optional[str],
1395+
slack_user_id: str,
1396+
) -> Any:
1397+
if not job_id:
1398+
return (
1399+
"I couldn't tell which article bundle to publish as a PR in this thread. "
1400+
"Please ask from the original article thread after the content-ready message."
1401+
)
1402+
1403+
try:
1404+
response = await api_client.publish_article_as_pr(job_id, slack_user_id)
1405+
except httpx.HTTPStatusError as exc:
1406+
error_message = str(exc)
1407+
try:
1408+
error_data = exc.response.json()
1409+
except Exception:
1410+
error_data = {}
1411+
if isinstance(error_data, dict):
1412+
error_message = (
1413+
error_data.get("error")
1414+
or error_data.get("message")
1415+
or error_message
1416+
)
1417+
return f"❌ I couldn't publish that article bundle as a PR: {error_message}"
1418+
except Exception as exc:
1419+
return f"❌ I couldn't publish that article bundle as a PR: {exc}"
1420+
1421+
child_job_id = str(response.get("job_id") or response.get("run_id") or "").strip()
1422+
if not child_job_id:
1423+
return (
1424+
"I asked Content Factory to publish that article bundle as a PR, "
1425+
"but it didn't return a run ID."
1426+
)
1427+
1428+
resolved_domain = (
1429+
normalize_content_factory_domain(domain)
1430+
or normalize_content_factory_domain(response.get("domain"))
1431+
or domain
1432+
or response.get("domain")
1433+
)
1434+
return self._build_publish_pr_start_response(
1435+
domain=resolved_domain,
1436+
job_id=child_job_id,
1437+
)
1438+
13551439
def _resolve_content_factory_repo_name(
13561440
self,
13571441
integration: dict,
@@ -1395,6 +1479,13 @@ async def _execute_content_factory(
13951479
domain = params.get("domain")
13961480
org_config_cached = None
13971481
action = params.get("action")
1482+
if action == "publish_pr":
1483+
return await self._publish_content_bundle_as_pr(
1484+
api_client,
1485+
job_id=str(params.get("job_id") or "").strip(),
1486+
domain=domain,
1487+
slack_user_id=user_id,
1488+
)
13981489
is_scan_request = self._is_explicit_scan_request(text, params)
13991490
is_article_flow = action != "scaffold" and not is_scan_request
14001491
requested_delivery_mode, requested_delivery_mode_confirmed = self._resolve_requested_article_delivery_mode(

0 commit comments

Comments
 (0)