ask-handoff

Stop duplicate handoff via topic observation (prompt-only)

slug ticket-108129-handoff-duplicado project sophia-bot source cursor updated 10/29/2026, 12:05:59 PM

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:

  1. User: "Gostaria de enviar um currículo"
  2. Topic automation fires (TALK_TO_ATTENDANT, "Certo! Estou chamando…")
  3. Observation previously echoed that phrase inside <message_to_user> while also saying "do not transfer"
  4. Model still called explicit_transfer_conversation_request_tool → second humanHandoff ("Claro! Estou te encaminhando…")
  5. check_ai_has_no_content did 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_text still set
  • Integration (LLM): curriculum topic → topic tool ran; explicit transfer not called; no humanHandoff action

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_handoff only 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 + (message or showForm), not raw if 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).