Stop duplicate handoff via topic observation — Implementation Plan
Goal: After topic-based automation already executes a human handoff, Reason must not also call explicit_transfer_conversation_request_tool in the same turn (one user-facing handoff message only).
Task: TICKET-108129
Handoff: 108129
Spec: none — root cause from prod canary logs 2026-10-22
Status: implemented on branch fix/TICKET-108129-dedupe-handoff-after-topic (commits 5acfd04, e8f307b)
Architecture (final)
Prompt-only at the topic tool observation. When a resolved hours handoff ran (message or showForm), build_topic_automation_response returns an observation that says handoff is DONE, demands empty final answer, forbids transfer tools, and does not echo the handoff copy. body.response_text still carries the phrase for Generic Bot.
Superseded (do not reintroduce): code dedupe in tool_result_processor, short-circuit in explicit_traveler_request_tool, fake intermediate_steps, tool masking, or changing the topic tool description business rule (“continue with other tools when needed”).
Tech: sophia-bot Python / LangGraph / pytest
Test layout: unit under tests/test_unit/; integration under tests/test_integration/ with @pytest.mark.llm
Root cause (evidence)
Conversation 555189605971-5551994287155 (hotel-suarez-executive), canary:
- User: "Gostaria de enviar um currículo"
- Topic automation fires (
TALK_TO_ATTENDANT, "Certo! Estou chamando…") - Observation previously echoed that phrase inside
<message_to_user>while also saying "do not transfer" - Model still called
explicit_transfer_conversation_request_tool→ secondhumanHandoff("Claro! Estou te encaminhando…") check_ai_has_no_contentdid not fire — N1 brief was wrong
Cheap-first: Prompt already forbade the second call but failed because the observation quoted the handoff phrase. Fix the observation (stop echoing + stronger DONE directive) before adding processor/tool guards. Config (disable EXPLICIT) breaks legitimate traveler requests.
Task 1: Handoff observation without echoing copy
Files:
- Modify:
src/tools/topic_based_automation/topic_based_automation_service.py - Test:
tests/test_unit/test_topic_based_automation_service.py - Test:
tests/test_integration/test_reason_topic_handoff_no_duplicate_transfer.py
Acceptance:
- Resolved handoff → observation forbids transfer / empty answer; handoff phrase not in observation;
body.response_textstill set - Integration (LLM): curriculum topic → topic tool ran; explicit transfer not called; no
humanHandoffaction
Commit: 5acfd04
Risk: soft — LLM can still ignore the directive (A/B); no hard code gate
Task 2: Gate DONE directive on resolved hours config
Files: same service + unit test
Acceptance:
human_handoffonly for opposite hours slot → no DONE/empty-answer directive; falls through to "No predefined response" so the model can still help / transfer if needed- Gate:
_resolve_handoff_config+ (messageorshowForm), not rawif human_handoff
Commit: e8f307b
Risk: mech
Verify
pipenv run python -m pytest tests/test_unit/test_topic_based_automation_service.py -q
# with LLM keys:
pipenv run python -m pytest tests/test_integration/test_reason_topic_handoff_no_duplicate_transfer.py -q
Smoke
Currículo/emprego on hotel-suarez-executive → one handoff message (automation only).