feat: think out loud
This commit is contained in:
@@ -98,6 +98,61 @@ from framework.tracker.llm_debug_logger import log_llm_turn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Tags whose content is internal reasoning and must be stripped from
|
||||
# the user-visible stream. Covers <think> and the 5-pillar character
|
||||
# assessment tags.
|
||||
_INTERNAL_TAGS = frozenset({
|
||||
"think",
|
||||
"social_distance",
|
||||
"context",
|
||||
"mood_filter",
|
||||
"physical_presence",
|
||||
"language_engine",
|
||||
})
|
||||
_INTERNAL_OPEN_RE = re.compile(r"<(" + "|".join(_INTERNAL_TAGS) + r")>")
|
||||
_INTERNAL_CLOSE_RE = re.compile(r"</(" + "|".join(_INTERNAL_TAGS) + r")>\s*")
|
||||
|
||||
|
||||
def _strip_internal_tags(content: str, snapshot: str) -> str:
|
||||
"""Strip internal reasoning tags from a streaming text chunk.
|
||||
|
||||
Uses the *snapshot* (full accumulated text) to detect whether we
|
||||
were inside an internal block BEFORE this chunk arrived, and
|
||||
filters *content* accordingly.
|
||||
"""
|
||||
# Fast path: no angle brackets anywhere
|
||||
if "<" not in snapshot:
|
||||
return content
|
||||
|
||||
# Check state using the snapshot BEFORE this content was appended.
|
||||
prior = snapshot[: len(snapshot) - len(content)] if len(snapshot) > len(content) else ""
|
||||
_inside = False
|
||||
for m in _INTERNAL_OPEN_RE.finditer(prior):
|
||||
tag = m.group(1)
|
||||
if prior.find(f"</{tag}>", m.end()) == -1:
|
||||
_inside = True
|
||||
|
||||
if _inside:
|
||||
# We were inside an internal block. Check if this chunk closes it.
|
||||
close_m = _INTERNAL_CLOSE_RE.search(content)
|
||||
if close_m:
|
||||
# Emit only text after the closing tag
|
||||
return content[close_m.end():]
|
||||
return "" # still inside, suppress
|
||||
|
||||
# We're outside. Strip any complete <tag>...</tag> pairs in this chunk,
|
||||
# and suppress from an opening tag to end-of-chunk if unclosed.
|
||||
result = content
|
||||
for tag in _INTERNAL_TAGS:
|
||||
result = re.sub(
|
||||
rf"<{tag}>.*?</{tag}>\s*", "", result, flags=re.DOTALL
|
||||
)
|
||||
# If an internal tag opens but doesn't close in this chunk, truncate
|
||||
open_m = _INTERNAL_OPEN_RE.search(result)
|
||||
if open_m:
|
||||
result = result[:open_m.start()]
|
||||
return result
|
||||
|
||||
|
||||
async def _describe_images_as_text(image_content: list[dict[str, Any]]) -> str | None:
|
||||
"""Describe images using the best available vision model."""
|
||||
@@ -2161,17 +2216,13 @@ class AgentLoop(NodeProtocol):
|
||||
):
|
||||
if isinstance(event, TextDeltaEvent):
|
||||
accumulated_text = event.snapshot
|
||||
# Filter <think>...</think> blocks from client output.
|
||||
# Content inside think tags is internal reasoning -- only
|
||||
# the text after </think> is shown to the user.
|
||||
_content = event.content
|
||||
if "<think>" in event.snapshot and "</think>" not in event.snapshot:
|
||||
_content = "" # still inside think block
|
||||
elif "</think>" in _content:
|
||||
# End of think block -- emit only text after the tag
|
||||
_content = _content.split("</think>", 1)[-1]
|
||||
elif "<think>" in _content:
|
||||
_content = "" # opening tag in this chunk
|
||||
# Filter internal reasoning tags from client output.
|
||||
# Uses the snapshot (full text so far) to detect
|
||||
# whether we're inside an internal block, and only
|
||||
# emits text that falls outside all such blocks.
|
||||
_content = _strip_internal_tags(
|
||||
event.content, event.snapshot
|
||||
)
|
||||
if _content:
|
||||
await self._publish_text_delta(
|
||||
stream_id,
|
||||
|
||||
@@ -532,16 +532,17 @@ _package_builder_knowledge = _shared_building_knowledge + _planning_knowledge +
|
||||
# -- Character core (immutable across all phases) --
|
||||
|
||||
_queen_character_core = """\
|
||||
You are the user's dedicated advisor. Your identity, \
|
||||
personality, and communication style are defined in the <core_identity> \
|
||||
and <hidden_background> sections above. Stay in character at all times.
|
||||
You are the advisor defined in <core_identity> above. Stay in character.
|
||||
|
||||
You remember people. When you've worked with someone before, you build on \
|
||||
what you know -- their preferences, their depth, what worked. You don't \
|
||||
treat returning users like strangers.
|
||||
Before every response, write the 5-dimension assessment tags as shown \
|
||||
in <roleplay_examples>. These tags are stripped from user view but kept \
|
||||
in conversation history -- you will see them on subsequent turns:
|
||||
<social_distance> <context> <mood_filter> <physical_presence> <language_engine>
|
||||
Then write your visible response. Direct, in character, no preamble.
|
||||
|
||||
The instructions that follow tell you what to DO in each phase. \
|
||||
Your identity tells you WHO you are. Don't confuse the two.\
|
||||
You remember people. When you've worked with someone before, build on \
|
||||
what you know. The instructions that follow tell you what to DO in each \
|
||||
phase. Your identity tells you WHO you are.\
|
||||
"""
|
||||
|
||||
# -- Phase-specific work roles (what you DO, not who you ARE) --
|
||||
@@ -721,7 +722,7 @@ Report the last run's results to the user and ask what they want to do next.
|
||||
# -- Behavior shared across all phases --
|
||||
|
||||
_queen_behavior_always = """
|
||||
# System Ruless
|
||||
# System Rules
|
||||
|
||||
## ask_user (CRITICAL)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user