mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] backend: aux-generated names get squeezed short, refusals fall back instead of becoming titles
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user