fix(ralph): "Planifică" deschide modal/ForceReply când descrierea lipsește
Înainte: click pe 🧠 Planifică (Discord/Telegram) sau /plan <slug> fără descriere pe un proiect din workspace fără entry în approved-tasks.json → mesaj eroare "Adaugă mai întâi cu /p <slug> <descriere>" și user-ul trebuia să facă două operații. Acum: - Discord button "Planifică" cu descriere goală → deschide RalphPlanModal cu TextInput pentru descriere; on_submit pornește direct start_planning_session - Discord /plan <slug> fără description param și fără entry → același modal (response.send_modal ÎNAINTE de defer — Discord constraint) - Telegram callback "Planifică" cu descriere goală → set state STEP_INPUT_DESCRIPTION_THEN_PLAN + ForceReply; handle_message detectează step și pornește planning cu textul user-ului - ralph_flow.py: nou STEP_INPUT_DESCRIPTION_THEN_PLAN (alături de cel existent pentru propose-only) start_planning_session deja auto-creează entry în approved-tasks.json dacă proiectul lipsește, deci flow-ul e end-to-end: workspace → click → descriere → planning agent activ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1007,8 +1007,8 @@ def create_bot(config: Config) -> discord.Client:
|
|||||||
slug: str,
|
slug: str,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await interaction.response.defer(ephemeral=True)
|
|
||||||
# Resolve description: explicit param wins, else look up in approved-tasks.
|
# Resolve description: explicit param wins, else look up in approved-tasks.
|
||||||
|
# Done BEFORE defer so we can fall back to a modal if missing.
|
||||||
desc = (description or "").strip()
|
desc = (description or "").strip()
|
||||||
if not desc:
|
if not desc:
|
||||||
try:
|
try:
|
||||||
@@ -1020,13 +1020,12 @@ def create_bot(config: Config) -> discord.Client:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("approved-tasks lookup failed")
|
logger.exception("approved-tasks lookup failed")
|
||||||
if not desc:
|
if not desc:
|
||||||
await interaction.followup.send(
|
# No description anywhere — ask via modal and start planning on submit.
|
||||||
f"Nu am descriere pentru `{slug}`. Adaugă cu `/p {slug} <descriere>` "
|
from src.adapters.discord_views import RalphPlanModal
|
||||||
"sau pasează `description` la `/plan`.",
|
await interaction.response.send_modal(RalphPlanModal(slug))
|
||||||
ephemeral=True,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
channel_id = str(interaction.channel_id)
|
channel_id = str(interaction.channel_id)
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
f"🧠 Pornesc planning pentru `{slug}`… (durează ~60s)", ephemeral=True
|
f"🧠 Pornesc planning pentru `{slug}`… (durează ~60s)", ephemeral=True
|
||||||
|
|||||||
@@ -116,6 +116,46 @@ class RalphProposeModal(discord.ui.Modal, title="Propune feature Ralph"):
|
|||||||
await interaction.response.send_message(result, ephemeral=True)
|
await interaction.response.send_message(result, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RalphPlanModal(discord.ui.Modal, title="Planifică Ralph"):
|
||||||
|
"""Modal asking for a description and starting a planning session immediately."""
|
||||||
|
|
||||||
|
description: discord.ui.TextInput = discord.ui.TextInput(
|
||||||
|
label="Descriere",
|
||||||
|
placeholder="Ce vrei să planificăm? (1-3 propoziții)",
|
||||||
|
style=discord.TextStyle.paragraph,
|
||||||
|
required=True,
|
||||||
|
max_length=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, slug: str) -> None:
|
||||||
|
super().__init__(timeout=VIEW_TIMEOUT)
|
||||||
|
self.slug = slug
|
||||||
|
self.title = f"Planifică: {slug}"[:45]
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||||
|
from src.router import start_planning_session
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
description = str(self.description.value).strip()
|
||||||
|
await interaction.followup.send(
|
||||||
|
f"🧠 Pornesc planning pentru `{self.slug}`… (durează ~60s)",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
channel_id = str(interaction.channel_id)
|
||||||
|
try:
|
||||||
|
first = start_planning_session(self.slug, description, channel_id, "discord")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("start_planning_session via modal failed for %s", self.slug)
|
||||||
|
await interaction.followup.send(f"Planning blocat: {e}", ephemeral=True)
|
||||||
|
return
|
||||||
|
for chunk in [first[i:i + 1900] for i in range(0, len(first), 1900)] or [first]:
|
||||||
|
await interaction.followup.send(chunk, ephemeral=True)
|
||||||
|
await interaction.followup.send(
|
||||||
|
"Răspunde aici. Apasă **Continuă faza** când ești gata să trec la următoarea.",
|
||||||
|
view=PlanningActiveView(),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# RalphProjectView — per-project action buttons
|
# RalphProjectView — per-project action buttons
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -175,7 +215,6 @@ class RalphProjectView(discord.ui.View):
|
|||||||
|
|
||||||
@discord.ui.button(label="🧠 Planifică", style=discord.ButtonStyle.primary, row=2)
|
@discord.ui.button(label="🧠 Planifică", style=discord.ButtonStyle.primary, row=2)
|
||||||
async def plan(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
async def plan(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
||||||
await interaction.response.defer(ephemeral=True)
|
|
||||||
# Look up description from approved-tasks.json
|
# Look up description from approved-tasks.json
|
||||||
description = ""
|
description = ""
|
||||||
try:
|
try:
|
||||||
@@ -187,12 +226,11 @@ class RalphProjectView(discord.ui.View):
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.exception("approved-tasks lookup failed")
|
log.exception("approved-tasks lookup failed")
|
||||||
if not description:
|
if not description:
|
||||||
await interaction.followup.send(
|
# No description yet — ask for one and start planning on submit.
|
||||||
f"Nu am descriere pentru `{self.slug}`. "
|
# Modal must be sent BEFORE response.defer (Discord constraint).
|
||||||
f"Adaugă mai întâi cu `/p {self.slug} <descriere>`.",
|
await interaction.response.send_modal(RalphPlanModal(self.slug))
|
||||||
ephemeral=True,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
channel_id = str(interaction.channel_id)
|
channel_id = str(interaction.channel_id)
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
f"🧠 Pornesc planning pentru `{self.slug}`… (durează ~60s)",
|
f"🧠 Pornesc planning pentru `{self.slug}`… (durează ~60s)",
|
||||||
|
|||||||
@@ -684,12 +684,17 @@ async def callback_ralph(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
description = p.get("description") or ""
|
description = p.get("description") or ""
|
||||||
break
|
break
|
||||||
if not description:
|
if not description:
|
||||||
|
# No description yet — set state and prompt with ForceReply.
|
||||||
|
# Next message in this chat will start the planning session.
|
||||||
|
ralph_flow.set_state(
|
||||||
|
ADAPTER_NAME, chat_id, user_id,
|
||||||
|
step=ralph_flow.STEP_INPUT_DESCRIPTION_THEN_PLAN,
|
||||||
|
project=slug,
|
||||||
|
)
|
||||||
await context.bot.send_message(
|
await context.bot.send_message(
|
||||||
chat_id=int(chat_id),
|
chat_id=int(chat_id),
|
||||||
text=(
|
text=f"📝 Descriere pentru *{slug}* — pornesc planning după ce trimiți:",
|
||||||
f"Nu am descriere pentru `{slug}`. "
|
reply_markup=ForceReply(selective=True),
|
||||||
f"Adaugă mai întâi cu `/p {slug} <descriere>`."
|
|
||||||
),
|
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -915,16 +920,39 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
chat_id, text[:100],
|
chat_id, text[:100],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ralph multi-step state: if user is replying to a "Descriere pentru X" prompt,
|
# Ralph multi-step state: route the next message based on the recorded step.
|
||||||
# route this text to _ralph_propose instead of Claude.
|
|
||||||
state = ralph_flow.get_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
state = ralph_flow.get_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||||
if state and state.get("step") == ralph_flow.STEP_INPUT_DESCRIPTION:
|
if state:
|
||||||
|
step = state.get("step")
|
||||||
slug = state.get("project")
|
slug = state.get("project")
|
||||||
if slug:
|
if slug and step == ralph_flow.STEP_INPUT_DESCRIPTION:
|
||||||
ralph_flow.clear_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
ralph_flow.clear_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||||
result = await asyncio.to_thread(_ralph_propose, slug, text)
|
result = await asyncio.to_thread(_ralph_propose, slug, text)
|
||||||
await message.reply_text(result)
|
await message.reply_text(result)
|
||||||
return
|
return
|
||||||
|
if slug and step == ralph_flow.STEP_INPUT_DESCRIPTION_THEN_PLAN:
|
||||||
|
ralph_flow.clear_state(ADAPTER_NAME, str(chat_id), str(user_id))
|
||||||
|
await message.reply_text(
|
||||||
|
f"🧠 Pornesc planning pentru *{slug}*… (durează ~60s)",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
first = await asyncio.to_thread(
|
||||||
|
start_planning_session, slug, text, str(chat_id), ADAPTER_NAME,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("start_planning_session failed for %s", slug)
|
||||||
|
await message.reply_text(f"Planning blocat: {e}")
|
||||||
|
return
|
||||||
|
for chunk in split_planning_chunks(first):
|
||||||
|
await context.bot.send_message(chat_id=chat_id, text=chunk)
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text="Răspunde aici. Apasă _Continuă faza_ când ești gata să trec la următoarea.",
|
||||||
|
reply_markup=_build_planning_active_keyboard(),
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Show typing indicator
|
# Show typing indicator
|
||||||
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ DEFAULT_TTL_SECONDS = 600 # 10 minutes
|
|||||||
|
|
||||||
# Step values used across adapters
|
# Step values used across adapters
|
||||||
STEP_INPUT_DESCRIPTION = "input_description"
|
STEP_INPUT_DESCRIPTION = "input_description"
|
||||||
STEP_IN_PLANNING = "in_planning" # reserved for W2 (planning agent)
|
STEP_INPUT_DESCRIPTION_THEN_PLAN = "input_description_then_plan"
|
||||||
|
STEP_IN_PLANNING = "in_planning" # planning agent active in this channel
|
||||||
|
|
||||||
|
|
||||||
def _key(adapter: str, chat_id: str, user_id: str) -> str:
|
def _key(adapter: str, chat_id: str, user_id: str) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user