[eric] backend: aux-generated names get squeezed short, refusals fall back instead of becoming titles

This commit is contained in:
ciregenz
2026-06-05 15:49:13 -07:00
parent aaa0d306db
commit 6ac76d3dbd
4 changed files with 87 additions and 28 deletions
+4 -11
View File
@@ -48,7 +48,7 @@ from backend.apps.agents.manager.prompt.tool_catalog import (
_get_denied_tool_names,
_is_fully_denied,
)
from backend.apps.agents.core.aux_llm import _safe_resp_text
from backend.apps.agents.core.aux_llm import _safe_resp_text, clean_short_label
from backend.apps.agents.manager.session.history_compaction import (
_build_history_prefix,
_get_branch_messages,
@@ -3599,7 +3599,7 @@ class AgentManager:
system=system_prompt,
messages=[{"role": "user", "content": user_turn}],
)
generated = _safe_resp_text(resp).strip().strip('"\'')
generated = clean_short_label(_safe_resp_text(resp))
if generated:
title = generated
except Exception as e:
@@ -3672,15 +3672,8 @@ class AgentManager:
),
}],
)
label = _safe_resp_text(resp).strip().strip('"\'').strip('.')
if not label:
return
# Defensive: cap length and strip leading 'I' / first-person if it
# slipped through despite the system prompt.
if label.lower().startswith(("i ", "i'm ", "i'll ")):
return # bail rather than show a hallucinated first-person label
if len(label) > 60:
label = label[:60].rsplit(" ", 1)[0]
# Bail on refusals/first-person rather than show a hallucinated label.
label = clean_short_label(_safe_resp_text(resp), max_words=6, max_chars=60)
if not label:
return
+21
View File
@@ -1,3 +1,24 @@
# Refusal/meta tells from aux label calls; any hit means "show the fallback, not this".
_REJECT_STARTS = ("i ", "i'm", "i'll", "i've", "as an", "sorry", "unfortunately", "please", "here")
_REJECT_ANYWHERE = ("cannot", "can't", "unable", "no information", "not enough", "need more", "provide more")
def clean_short_label(raw: str, max_words: int = 4, max_chars: int = 36) -> str:
"""Squeeze an aux-LLM reply into a safe short label: first line only, markdown
stripped, word/char capped; returns "" when it smells like an answer or refusal
so the caller falls back instead of showing 'I cannot...' as a title."""
line = next((l.strip() for l in (raw or "").splitlines() if l.strip()), "")
line = line.strip("\"'` ").lstrip("#*->• ").replace("**", "").replace("`", "")
line = line.rstrip(" .,:;!").strip()
low = line.lower()
if not line or low.startswith(_REJECT_STARTS) or any(t in low for t in _REJECT_ANYWHERE):
return ""
label = " ".join(line.split()[:max_words])
if len(label) > max_chars:
label = label[:max_chars].rsplit(" ", 1)[0].rstrip(" .,:;!") or label[:max_chars]
return label
def _safe_resp_text(resp) -> str:
"""Extract text from an Anthropic-shape response, tolerating Gemini/OpenAI
edge cases. Gemini through 9Router occasionally returns `content=[]` (e.g.
+16 -17
View File
@@ -317,7 +317,7 @@ async def generate_name(dashboard_id: str):
if not prompts:
return {"name": dashboard.name, "auto_named": dashboard.auto_named}
fallback = prompts[0][:40]
fallback = " ".join(prompts[0].split()[:4])[:36] or "Untitled Dashboard"
try:
from backend.apps.settings.settings import load_settings
from backend.apps.settings.credentials import get_anthropic_client_for_model
@@ -326,20 +326,19 @@ async def generate_name(dashboard_id: str):
aux_model, _aux_base = await resolve_aux_model(global_settings, preferred_tier="haiku")
client = get_anthropic_client_for_model(global_settings, aux_model)
if len(prompts) == 1:
system = (
"Generate a short 2-4 word workspace name summarizing this task. "
"Examples: 'Travel Planning', 'Code Review', 'Sales Dashboard'. "
"No quotes, no punctuation, no emojis, no explanation. Return ONLY the name."
)
user_content = prompts[0]
else:
system = (
"Generate a short 2-4 word workspace name capturing the theme of these tasks. "
"Examples: 'Research & Analysis', 'Content Creation', 'Project Setup'. "
"No quotes, no punctuation, no emojis, no explanation. Return ONLY the name."
)
user_content = "\n".join(f"- {p}" for p in prompts)
# Mirrors generate_title's hardening: the tasks are inert text to LABEL, never answer,
# or the aux model happily replies with a markdown essay that becomes the title.
system = (
"You label tasks with a 2-4 word workspace name. "
"Examples: 'Travel planning', 'Code review', 'Sales dashboard'. "
"You NEVER answer or perform the tasks. You NEVER describe yourself. "
"You NEVER begin with 'I', 'As an', 'Sorry', 'Unfortunately', or any first-person phrasing. "
"Return ONLY the 2-4 word name. No quotes, no punctuation, no emojis, no explanation."
)
user_content = (
"Name the workspace for the tasks inside <tasks> tags. Do not answer them.\n\n"
"<tasks>\n" + "\n".join(f"- {p}" for p in prompts) + "\n</tasks>"
)
resp = await client.messages.create(
model=aux_model,
@@ -347,8 +346,8 @@ async def generate_name(dashboard_id: str):
system=system,
messages=[{"role": "user", "content": user_content}],
)
from backend.apps.agents.core.aux_llm import _safe_resp_text
generated = _safe_resp_text(resp).strip().strip('"\'')
from backend.apps.agents.core.aux_llm import _safe_resp_text, clean_short_label
generated = clean_short_label(_safe_resp_text(resp))
if generated:
fallback = generated
except Exception as e:
+46
View File
@@ -0,0 +1,46 @@
from backend.apps.agents.core.aux_llm import clean_short_label
def test_clean_label_passthrough():
assert clean_short_label("Travel planning") == "Travel planning"
def test_markdown_essay_becomes_short():
raw = "# Git Rebase Explained\nGit rebase is a way to integrate changes from one branch"
assert clean_short_label(raw) == "Git Rebase Explained"
def test_first_line_word_cap():
assert clean_short_label("one two three four five six") == "one two three four"
def test_char_cap_lands_on_word_boundary():
out = clean_short_label("supercalifragilistic expialidocious antidisestablishmentarianism", max_words=4)
assert len(out) <= 36
assert not out.endswith(" ")
def test_refusals_rejected():
for bad in [
"I cannot generate a name without more information",
"Sorry, there is no task to summarize",
"I'm unable to help with that",
"As an AI, I need more context",
"Unfortunately no information was provided",
]:
assert clean_short_label(bad) == ""
def test_quotes_and_bullets_stripped():
assert clean_short_label('"**Code review**"') == "Code review"
assert clean_short_label("- Sales dashboard.") == "Sales dashboard"
def test_empty_and_whitespace():
assert clean_short_label("") == ""
assert clean_short_label("\n\n \n") == ""
def test_ing_words_not_false_rejected():
assert clean_short_label("Investigating the bug") == "Investigating the bug"
assert clean_short_label("iOS app design") == "iOS app design"