fix: remove <think> wrappers from think messages
Build and Push / build (release) Successful in 1m49s
Build and Push / build (release) Successful in 1m49s
Messages in reasoning_content should be plain text, not wrapped in <think> tags. Removed wrappers from: - delegation.py household think messages - orchestration.py status messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.8.4] - 2025-12-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Remove `<think>` wrappers from think messages** - Messages in `reasoning_content` should be plain text
|
||||
- Removed `<think>` wrappers from delegation.py household think messages
|
||||
- Removed `<think>` wrappers from orchestration.py status messages
|
||||
- Think messages now appear cleanly in Open WebUI's reasoning block
|
||||
|
||||
## [1.8.3] - 2025-12-16
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tatlock"
|
||||
version = "1.8.3"
|
||||
version = "1.8.4"
|
||||
description = "OpenAI-compatible API with Ollama backend"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
+23
-22
@@ -40,45 +40,46 @@ class ActionType(Enum):
|
||||
# =============================================================================
|
||||
|
||||
HOUSEHOLD_THINK_MESSAGES: dict[str, dict[ActionType, dict[str, str]]] = {
|
||||
# Note: No <think> wrappers needed - these go to reasoning_content field
|
||||
"librarian": {
|
||||
ActionType.RETRIEVE: {
|
||||
"start": "<think>Allow me to consult the archives, sir.</think>",
|
||||
"success": "<think>The Librarian has compiled the relevant findings.</think>",
|
||||
"error": "<think>I'm afraid the archives proved difficult to access.</think>",
|
||||
"start": "Allow me to consult the archives, sir.",
|
||||
"success": "The Librarian has compiled the relevant findings.",
|
||||
"error": "I'm afraid the archives proved difficult to access.",
|
||||
},
|
||||
ActionType.RESEARCH: {
|
||||
"start": "<think>I've dispatched the Librarian to conduct some fresh research.</think>",
|
||||
"success": "<think>The Librarian has returned with findings, sir.</think>",
|
||||
"error": "<think>The research proved inconclusive, I'm afraid.</think>",
|
||||
"start": "I've dispatched the Librarian to conduct some fresh research.",
|
||||
"success": "The Librarian has returned with findings, sir.",
|
||||
"error": "The research proved inconclusive, I'm afraid.",
|
||||
},
|
||||
ActionType.CREATE: {
|
||||
"start": "<think>I'm having the Librarian prepare a new entry.</think>",
|
||||
"success": "<think>The new material has been properly catalogued, sir.</think>",
|
||||
"error": "<think>I'm afraid there was difficulty filing the entry.</think>",
|
||||
"start": "I'm having the Librarian prepare a new entry.",
|
||||
"success": "The new material has been properly catalogued, sir.",
|
||||
"error": "I'm afraid there was difficulty filing the entry.",
|
||||
},
|
||||
},
|
||||
"biographer": {
|
||||
ActionType.RETRIEVE: {
|
||||
"start": "<think>Let me consult the household records.</think>",
|
||||
"success": "<think>The Biographer has located the relevant information, sir.</think>",
|
||||
"error": "<think>I'm unable to locate those particular records.</think>",
|
||||
"start": "Let me consult the household records.",
|
||||
"success": "The Biographer has located the relevant information, sir.",
|
||||
"error": "I'm unable to locate those particular records.",
|
||||
},
|
||||
ActionType.RECORD: {
|
||||
"start": "<think>I've asked the Biographer to take note of this, sir.</think>",
|
||||
"success": "<think>The household records have been updated accordingly.</think>",
|
||||
"error": "<think>I'm afraid there was difficulty recording the entry.</think>",
|
||||
"start": "I've asked the Biographer to take note of this, sir.",
|
||||
"success": "The household records have been updated accordingly.",
|
||||
"error": "I'm afraid there was difficulty recording the entry.",
|
||||
},
|
||||
},
|
||||
"housekeeper": {
|
||||
ActionType.RETRIEVE: {
|
||||
"start": "<think>Allow me to inquire with the household staff.</think>",
|
||||
"success": "<think>The staff reports the current status, sir.</think>",
|
||||
"error": "<think>The household staff is momentarily unavailable, I'm afraid.</think>",
|
||||
"start": "Allow me to inquire with the household staff.",
|
||||
"success": "The staff reports the current status, sir.",
|
||||
"error": "The household staff is momentarily unavailable, I'm afraid.",
|
||||
},
|
||||
ActionType.CONTROL: {
|
||||
"start": "<think>I'm instructing the household staff now, sir.</think>",
|
||||
"success": "<think>The household has been configured as requested.</think>",
|
||||
"error": "<think>I'm afraid the staff reports an issue with that request.</think>",
|
||||
"start": "I'm instructing the household staff now, sir.",
|
||||
"success": "The household has been configured as requested.",
|
||||
"error": "I'm afraid the staff reports an issue with that request.",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -139,7 +140,7 @@ def get_think_message(expert: str, task: str, phase: str) -> str:
|
||||
action_type = _detect_action_type(expert, task)
|
||||
expert_messages = HOUSEHOLD_THINK_MESSAGES.get(expert, {})
|
||||
action_messages = expert_messages.get(action_type, expert_messages.get(ActionType.RETRIEVE, {}))
|
||||
return action_messages.get(phase, f"<think>Consulting {expert}...</think>")
|
||||
return action_messages.get(phase, f"Consulting {expert}...")
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+13
-13
@@ -176,19 +176,19 @@ async def orchestrate_with_think_updates(
|
||||
if delegation_task.expert_name == "librarian":
|
||||
expert_display_name = "The Librarian"
|
||||
|
||||
yield f"<think>🤝 Consulting {expert_display_name}...</think>\n"
|
||||
yield f"🤝 Consulting {expert_display_name}...\n"
|
||||
|
||||
# Execute delegation (uses run() internally)
|
||||
result = await execute_delegation(delegation_task)
|
||||
|
||||
if result.success:
|
||||
yield f"<think>✅ {expert_display_name} completed research.</think>\n"
|
||||
yield f"✅ {expert_display_name} completed research.\n"
|
||||
|
||||
# Yield the expert's findings
|
||||
if result.output:
|
||||
yield f"\n{result.output}"
|
||||
else:
|
||||
yield f"<think>⚠️ {expert_display_name} encountered an issue: {result.error}</think>\n"
|
||||
yield f"⚠️ {expert_display_name} encountered an issue: {result.error}\n"
|
||||
|
||||
logger.info(
|
||||
"orchestration_complete",
|
||||
@@ -449,12 +449,12 @@ async def orchestrate_multi_expert(
|
||||
return
|
||||
|
||||
# Stream: Starting multi-expert coordination
|
||||
yield f"<think>🎯 Starting multi-expert coordination ({len(tasks)} tasks, {mode.value})...</think>\n"
|
||||
yield f"🎯 Starting multi-expert coordination ({len(tasks)} tasks, {mode.value})...\n"
|
||||
|
||||
if mode == ExecutionMode.PARALLEL:
|
||||
# Parallel execution - emit one update then run all at once
|
||||
expert_names = ", ".join(_get_display_name(t.expert_name) for t in tasks)
|
||||
yield f"<think>🔄 Consulting in parallel: {expert_names}...</think>\n"
|
||||
yield f"🔄 Consulting in parallel: {expert_names}...\n"
|
||||
|
||||
result = await execute_parallel(tasks)
|
||||
|
||||
@@ -462,9 +462,9 @@ async def orchestrate_multi_expert(
|
||||
for expert_name, expert_result in result.results.items():
|
||||
display_name = _get_display_name(expert_name)
|
||||
if expert_result.success:
|
||||
yield f"<think>✅ {display_name} completed.</think>\n"
|
||||
yield f"✅ {display_name} completed.\n"
|
||||
else:
|
||||
yield f"<think>⚠️ {display_name} failed: {expert_result.error}</think>\n"
|
||||
yield f"⚠️ {display_name} failed: {expert_result.error}\n"
|
||||
|
||||
else:
|
||||
# Sequential execution - emit updates for each task
|
||||
@@ -472,27 +472,27 @@ async def orchestrate_multi_expert(
|
||||
|
||||
for task in tasks:
|
||||
display_name = _get_display_name(task.expert_name)
|
||||
yield f"<think>🤝 Consulting {display_name}...</think>\n"
|
||||
yield f"🤝 Consulting {display_name}...\n"
|
||||
|
||||
task_result = await execute_delegation(task)
|
||||
result.add_result(task_result)
|
||||
|
||||
if task_result.success:
|
||||
yield f"<think>✅ {display_name} completed.</think>\n"
|
||||
yield f"✅ {display_name} completed.\n"
|
||||
else:
|
||||
yield f"<think>⚠️ {display_name} failed: {task_result.error}</think>\n"
|
||||
yield f"⚠️ {display_name} failed: {task_result.error}\n"
|
||||
if stop_on_failure:
|
||||
yield "<think>🛑 Stopping due to failure.</think>\n"
|
||||
yield "🛑 Stopping due to failure.\n"
|
||||
break
|
||||
|
||||
result.aggregate_outputs()
|
||||
|
||||
# Stream: Summary
|
||||
if result.all_succeeded:
|
||||
yield "<think>🎉 All experts completed successfully.</think>\n"
|
||||
yield "🎉 All experts completed successfully.\n"
|
||||
else:
|
||||
failed_names = ", ".join(_get_display_name(e) for e in result.failed_experts)
|
||||
yield f"<think>⚠️ Some experts failed: {failed_names}</think>\n"
|
||||
yield f"⚠️ Some experts failed: {failed_names}\n"
|
||||
|
||||
# Yield combined output
|
||||
if result.combined_output:
|
||||
|
||||
@@ -248,13 +248,16 @@ class TestHouseholdThinkMessages:
|
||||
assert "success" in messages, f"{expert}/{action_type} missing 'success'"
|
||||
assert "error" in messages, f"{expert}/{action_type} missing 'error'"
|
||||
|
||||
def test_messages_are_think_tags(self):
|
||||
"""Test messages are wrapped in <think> tags."""
|
||||
def test_messages_are_plain_text(self):
|
||||
"""Test messages are plain text (no <think> wrappers - those go to reasoning_content)."""
|
||||
for expert, action_types in HOUSEHOLD_THINK_MESSAGES.items():
|
||||
for action_type, messages in action_types.items():
|
||||
for phase, msg in messages.items():
|
||||
assert msg.startswith("<think>"), f"{expert}/{action_type}/{phase}"
|
||||
assert msg.endswith("</think>"), f"{expert}/{action_type}/{phase}"
|
||||
# Messages should NOT have <think> wrappers - they go to reasoning_content field
|
||||
assert "<think>" not in msg, f"{expert}/{action_type}/{phase} should not have <think> wrapper"
|
||||
assert "</think>" not in msg, f"{expert}/{action_type}/{phase} should not have </think> wrapper"
|
||||
# Messages should be non-empty strings
|
||||
assert isinstance(msg, str) and len(msg) > 0, f"{expert}/{action_type}/{phase}"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -310,31 +313,32 @@ class TestGetThinkMessage:
|
||||
def test_librarian_retrieve_start(self):
|
||||
"""Test getting librarian retrieve start message."""
|
||||
msg = get_think_message("librarian", "search for Docker", "start")
|
||||
assert "<think>" in msg
|
||||
assert "</think>" in msg
|
||||
# No <think> wrappers - messages go to reasoning_content field
|
||||
assert "<think>" not in msg
|
||||
assert "archives" in msg.lower() or "consult" in msg.lower()
|
||||
|
||||
def test_librarian_create_success(self):
|
||||
"""Test getting librarian create success message."""
|
||||
msg = get_think_message("librarian", "create a wiki page", "success")
|
||||
assert "<think>" in msg
|
||||
assert "<think>" not in msg
|
||||
assert "catalogued" in msg.lower()
|
||||
|
||||
def test_biographer_record_start(self):
|
||||
"""Test getting biographer record start message."""
|
||||
msg = get_think_message("biographer", "remember my preference", "start")
|
||||
assert "<think>" in msg
|
||||
assert "<think>" not in msg
|
||||
assert "note" in msg.lower() or "biographer" in msg.lower()
|
||||
|
||||
def test_housekeeper_control_success(self):
|
||||
"""Test getting housekeeper control success message."""
|
||||
msg = get_think_message("housekeeper", "turn on the lights", "success")
|
||||
assert "<think>" in msg
|
||||
assert "<think>" not in msg
|
||||
assert "configured" in msg.lower()
|
||||
|
||||
def test_unknown_expert_fallback(self):
|
||||
"""Test unknown expert gets fallback message."""
|
||||
msg = get_think_message("unknown_expert", "some task", "start")
|
||||
assert "<think>" in msg
|
||||
assert "<think>" not in msg
|
||||
assert "unknown_expert" in msg.lower()
|
||||
|
||||
|
||||
|
||||
@@ -208,8 +208,8 @@ class TestOrchestrateWithThinkUpdates:
|
||||
):
|
||||
updates.append(update)
|
||||
|
||||
# First update should be think tag about consulting
|
||||
assert any("<think>" in u and "Consulting" in u for u in updates)
|
||||
# First update should be about consulting (no <think> wrappers anymore)
|
||||
assert any("Consulting" in u for u in updates)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrate_emits_think_after_delegation(self):
|
||||
@@ -233,8 +233,8 @@ class TestOrchestrateWithThinkUpdates:
|
||||
):
|
||||
updates.append(update)
|
||||
|
||||
# Should have think tag about completion
|
||||
assert any("<think>" in u and "completed" in u for u in updates)
|
||||
# Should have message about completion (no <think> wrappers anymore)
|
||||
assert any("completed" in u for u in updates)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrate_yields_expert_output(self):
|
||||
|
||||
Reference in New Issue
Block a user