From 9980e4764c207fa5e2133538e8fb638cc079f27c Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 16 Dec 2025 09:12:17 +0100 Subject: [PATCH] fix: remove wrappers from think messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Messages in reasoning_content should be plain text, not wrapped in 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 --- CHANGELOG.md | 9 ++++++ pyproject.toml | 2 +- src/agents/delegation.py | 45 +++++++++++++++--------------- src/agents/orchestration.py | 26 ++++++++--------- tests/agents/test_delegation.py | 24 +++++++++------- tests/agents/test_orchestration.py | 8 +++--- 6 files changed, 64 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af61e8..8c68d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` wrappers from think messages** - Messages in `reasoning_content` should be plain text +- Removed `` wrappers from delegation.py household think messages +- Removed `` wrappers from orchestration.py status messages +- Think messages now appear cleanly in Open WebUI's reasoning block + ## [1.8.3] - 2025-12-16 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index cd48792..99ed950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [] diff --git a/src/agents/delegation.py b/src/agents/delegation.py index 0db89e4..7be59cb 100644 --- a/src/agents/delegation.py +++ b/src/agents/delegation.py @@ -40,45 +40,46 @@ class ActionType(Enum): # ============================================================================= HOUSEHOLD_THINK_MESSAGES: dict[str, dict[ActionType, dict[str, str]]] = { + # Note: No wrappers needed - these go to reasoning_content field "librarian": { ActionType.RETRIEVE: { - "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.", + "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": "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.", + "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": "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.", + "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": "Let me consult the household records.", - "success": "The Biographer has located the relevant information, sir.", - "error": "I'm unable to locate those particular records.", + "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": "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.", + "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": "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.", + "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": "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.", + "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"Consulting {expert}...") + return action_messages.get(phase, f"Consulting {expert}...") @dataclass diff --git a/src/agents/orchestration.py b/src/agents/orchestration.py index 32ffd80..abe728f 100644 --- a/src/agents/orchestration.py +++ b/src/agents/orchestration.py @@ -176,19 +176,19 @@ async def orchestrate_with_think_updates( if delegation_task.expert_name == "librarian": expert_display_name = "The Librarian" - yield f"🤝 Consulting {expert_display_name}...\n" + yield f"🤝 Consulting {expert_display_name}...\n" # Execute delegation (uses run() internally) result = await execute_delegation(delegation_task) if result.success: - yield f"✅ {expert_display_name} completed research.\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"⚠️ {expert_display_name} encountered an issue: {result.error}\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"🎯 Starting multi-expert coordination ({len(tasks)} tasks, {mode.value})...\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"🔄 Consulting in parallel: {expert_names}...\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"✅ {display_name} completed.\n" + yield f"✅ {display_name} completed.\n" else: - yield f"⚠️ {display_name} failed: {expert_result.error}\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"🤝 Consulting {display_name}...\n" + yield f"🤝 Consulting {display_name}...\n" task_result = await execute_delegation(task) result.add_result(task_result) if task_result.success: - yield f"✅ {display_name} completed.\n" + yield f"✅ {display_name} completed.\n" else: - yield f"⚠️ {display_name} failed: {task_result.error}\n" + yield f"⚠️ {display_name} failed: {task_result.error}\n" if stop_on_failure: - yield "🛑 Stopping due to failure.\n" + yield "🛑 Stopping due to failure.\n" break result.aggregate_outputs() # Stream: Summary if result.all_succeeded: - yield "🎉 All experts completed successfully.\n" + yield "🎉 All experts completed successfully.\n" else: failed_names = ", ".join(_get_display_name(e) for e in result.failed_experts) - yield f"⚠️ Some experts failed: {failed_names}\n" + yield f"⚠️ Some experts failed: {failed_names}\n" # Yield combined output if result.combined_output: diff --git a/tests/agents/test_delegation.py b/tests/agents/test_delegation.py index 1abe358..fb90259 100644 --- a/tests/agents/test_delegation.py +++ b/tests/agents/test_delegation.py @@ -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 tags.""" + def test_messages_are_plain_text(self): + """Test messages are plain text (no 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(""), f"{expert}/{action_type}/{phase}" - assert msg.endswith(""), f"{expert}/{action_type}/{phase}" + # Messages should NOT have wrappers - they go to reasoning_content field + assert "" not in msg, f"{expert}/{action_type}/{phase} should not have wrapper" + assert "" not in msg, f"{expert}/{action_type}/{phase} should not have 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 "" in msg - assert "" in msg + # No wrappers - messages go to reasoning_content field + assert "" 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 "" in msg + assert "" 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 "" in msg + assert "" 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 "" in msg + assert "" 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 "" in msg + assert "" not in msg assert "unknown_expert" in msg.lower() diff --git a/tests/agents/test_orchestration.py b/tests/agents/test_orchestration.py index 29d2e67..d5c6b7b 100644 --- a/tests/agents/test_orchestration.py +++ b/tests/agents/test_orchestration.py @@ -208,8 +208,8 @@ class TestOrchestrateWithThinkUpdates: ): updates.append(update) - # First update should be think tag about consulting - assert any("" in u and "Consulting" in u for u in updates) + # First update should be about consulting (no 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("" in u and "completed" in u for u in updates) + # Should have message about completion (no wrappers anymore) + assert any("completed" in u for u in updates) @pytest.mark.asyncio async def test_orchestrate_yields_expert_output(self):