Devon: track show transitions and avoid repeating suggestions

Adds new_show() that inserts a NEW SHOW marker into Devon's history
so he won't reference previous episodes' callers or topics, trims
older history while keeping some long-term memory, and clears
research_cache + pending suggestions. Buffered suggestions are now
tracked in history via _track_suggestion() so Devon won't suggest
the same thing twice. Devon history scoped to current show on ask.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 23:39:52 -06:00
co-authored by Claude Opus 4.6
parent 54ad04ae09
commit 44ac569fec
+40 -15
View File
@@ -391,9 +391,14 @@ class InternService:
"content": "A caller is on the line right now. Focus on delivering useful facts, context, and information. Skip personal stories and anecdotes — save those for when it's just you and Luke talking between calls."
})
# Include Devon's own recent conversation history
# Include Devon's own recent conversation history (current show only)
if self._devon_history:
messages.extend(self._devon_history[-10:])
last_marker = -1
for i, msg in enumerate(self._devon_history):
if msg.get("role") == "system" and "NEW SHOW" in msg.get("content", ""):
last_marker = i
relevant = self._devon_history[last_marker + 1:] if last_marker >= 0 else self._devon_history
messages.extend(relevant[-10:])
messages.append({"role": "user", "content": question})
@@ -437,6 +442,11 @@ class InternService:
"tool_calls": tool_calls,
}
def _track_suggestion(self, text: str):
"""Record a suggestion in Devon's history so he won't repeat it."""
self._devon_history.append({"role": "assistant", "content": text})
self._save()
async def interject(self, conversation: list[dict], caller_active: bool = False) -> dict | None:
"""Intern looks at conversation and decides if there's something worth adding.
Returns {text, sources, tool_calls} or None if nothing to add."""
@@ -549,6 +559,7 @@ class InternService:
if result:
self.pending_interjection = result["text"]
self.pending_sources = result.get("tool_calls", [])
self._track_suggestion(result["text"])
await on_suggestion(result["text"], result["sources"])
print(f"[Intern] Buffered suggestion: {result['text'][:60]}...")
except Exception as e:
@@ -563,6 +574,24 @@ class InternService:
)
print("[Intern] Monitoring started")
def new_show(self):
"""Mark the start of a new show in Devon's memory so he knows the context has changed."""
if self._devon_history and self._devon_history[-1].get("role") == "system" and "NEW SHOW" in self._devon_history[-1].get("content", ""):
print("[Intern] Skipping duplicate new_show marker")
return
self._devon_history.append({
"role": "system",
"content": "--- NEW SHOW STARTING --- The previous show is over. A brand new episode is about to begin. Everything before this point was a previous show. Don't reference the previous show's callers or topics unless Luke brings them up. Fresh energy, clean slate."
})
self.research_cache.clear()
self.pending_interjection = None
self.pending_sources = []
# Trim older history but keep some for long-term memory
if len(self._devon_history) > 30:
self._devon_history = self._devon_history[-30:]
self._save()
print("[Intern] New show marker added to conversation history")
def stop_monitoring(self):
self.monitoring = False
if self._monitor_task and not self._monitor_task.done():
@@ -588,29 +617,25 @@ class InternService:
def _clean_for_tts(text: str) -> str:
if not text:
return ""
# Strip stage directions BEFORE markdown processing
# Parenthetical: (laughs), (sighs nervously), (clears throat), etc.
text = re.sub(r'\s*\([^)]{1,40}\)\s*', ' ', text)
# Multi-word asterisk stage directions: *sighs deeply*, *nervous laughter*
text = re.sub(r'\s*\*\w+\s[^*]{1,30}\*\s*', ' ', text)
# Single-word asterisk stage directions (known action words only)
_actions = r'(?:laughs?|sighs?|pauses?|smiles?|chuckles?|grins?|nods?|shrugs?|frowns?|coughs?|gasps?|whispers?|mumbles?|gulps?|blinks?|winces?|crying|sobbing)'
text = re.sub(r'\s*\*' + _actions + r'\*\s*', ' ', text, flags=re.IGNORECASE)
# Remove markdown formatting (after stage directions are stripped)
# Devon-specific: strip markdown and tool artifacts first
# Remove markdown formatting
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'\*(.+?)\*', r'\1', text)
text = re.sub(r'`(.+?)`', r'\1', text)
text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text)
# Remove bullet points / list markers
text = re.sub(r'^\s*[-*•]\s+', '', text, flags=re.MULTILINE)
# Collapse whitespace
text = re.sub(r'\s+', ' ', text).strip()
# Remove quotes that TTS reads awkwardly
text = text.replace('"', '').replace('"', '').replace('"', '')
# Remove smart quotes that TTS reads awkwardly
text = text.replace('\u201c', '').replace('\u201d', '').replace('\u2018', '').replace('\u2019', "'")
# Strip tool error artifacts that shouldn't be spoken on air
text = re.sub(r'(?:Error|ERROR|error):?\s*\S.*?(?:\.|$)', '', text)
text = re.sub(r'Tool unavailable[^.]*\.?', '', text)
text = re.sub(r'\s+', ' ', text).strip()
# Run shared TTS preprocessing (stage directions, numbers, abbreviations,
# symbols, pronunciation fixes, breathing pauses)
from backend.main import clean_for_tts
text = clean_for_tts(text, formal=True)
return text