mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
* [aidan] ui: sidebar reorder * [aidan] ux: sidebar naming bug fix * [aidan] ui: title changing improvements * [aidan] ui/ux: aux title gen streams for cx/ route + agent writes meta.json first - All aux LLM calls (chat title, turn label, group meta, dashboard name) now stream instead of using messages.create; 9router's cx/ subscription response translator drops content for GPT-5-family models on the non-streaming path but works per-event. - aux_max_tokens_for floors GPT-5 budget at 4K so reasoning still leaves headroom for a label; non-reasoning models get the base 100. - App Builder skill: write meta.json FIRST (step 1 of Quick start) so the app's name surfaces in the sidebar + ViewEditor header on the agent's first tool call instead of waiting until end-of-turn. - Session-end sync_output_from_meta_json takes a fallback_name (= session.name) so an agent that never writes meta.json still leaves the app with the aux-LLM chat title rather than "Untitled App". - Title display: truncateForTitle caps at 4 words / 30 chars; displayChatTitle picks the right Phase 1 placeholder by session.mode; normalizeSessionName strips legacy Agent-XXXX names at slice intake. - Typewriter component (char-by-char delete-then-type) drives the chat header, dashboard card title, sidebar Apps entry, and ViewEditor TextField + description field; honors useReducedMotion. * [aidan] tune: reduce gpt-5 aux token floor
47 lines
2.2 KiB
Python
47 lines
2.2 KiB
Python
# 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 aux_max_tokens_for(model: str | None, base: int = 100) -> int:
|
|
# GPT-5 reasoners burn reasoning tokens before output; floor at 2K so a label can land.
|
|
if isinstance(model, str) and "gpt-5" in model.lower():
|
|
return max(base, 2048)
|
|
return base
|
|
|
|
|
|
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.
|
|
safety stop, function-call-only turn) which makes `resp.content[0].text`
|
|
raise `'NoneType' object is not subscriptable` and bubbles up as a
|
|
fallback-required path. This walks the content list looking for the first
|
|
text block and returns "" if none exists, so callers can decide their own
|
|
fallback without a raw IndexError.
|
|
"""
|
|
try:
|
|
blocks = getattr(resp, "content", None) or []
|
|
for b in blocks:
|
|
t = getattr(b, "text", None)
|
|
if isinstance(t, str) and t:
|
|
return t
|
|
return ""
|
|
except Exception:
|
|
return ""
|