Skip to content

feat(transfer_to_human): guarantee the customer notice before transferring - #57

Open
mateusbellozupko wants to merge 3 commits into
evolution-foundation:developfrom
mateusbellozupko:feat/transfer-notify-customer-guaranteed
Open

feat(transfer_to_human): guarantee the customer notice before transferring#57
mateusbellozupko wants to merge 3 commits into
evolution-foundation:developfrom
mateusbellozupko:feat/transfer-notify-customer-guaranteed

Conversation

@mateusbellozupko

@mateusbellozupko mateusbellozupko commented Aug 29, 2026

Copy link
Copy Markdown

Summary

Every transfer rule's own instructions field asks the agent to notify the customer before transferring, but that depended entirely on the model also producing reply text in the same turn as its tool calls. In practice, when a model decides to call tools it frequently omits customer-facing text — observed live: manage_conversation_labels + transfer_to_human fired together with a null/empty assistant message, so the transfer happened completely silently, with no "vou te encaminhar..." notice ever sent.

Fix

Add a message_to_customer parameter to transfer_to_human. When provided, the tool sends it as a real outgoing message before performing the team/agent assignment — so "notify, then transfer" becomes one atomic tool action instead of two separate things the model has to remember to sequence correctly. The docstring now instructs the model to always pass this when the matched rule's instructions ask for a customer notice. The response includes message_to_customer_sent/message_to_customer_error so a failed notice doesn't silently disappear (and doesn't block the transfer itself — better routed without the notice than not routed at all).

Stacked on

This branches from #55 (rule_index), since the docstring and rule-selection logic it touches were added there.

Testing notes

Verified the message-send call reuses the same POST /conversations/{id}/messages pattern as the existing send_private_message tool, just with private: false for a customer-visible outgoing message.

🤖 Generated with Claude Code

Summary by Sourcery

Guarantee customer notification before transfers and improve selection of the configured destination rule.

New Features:

  • Allow transfer tools to send a customer-facing notice before assigning the conversation to a human or team.
  • Expose whether the customer notice was sent successfully or failed while allowing the transfer to proceed.

Bug Fixes:

  • Prevent transfers from silently omitting the required customer notification when tool calls are not accompanied by assistant reply text.
  • Improve configured transfer-rule selection to honor the model-selected rule and avoid routing every transfer to the first rule by default.

Enhancements:

  • Require explicit transfer-rule selection when configured and support safer keyword-based matching when no rule index is provided.

When the model called transfer_to_human with only `reason` (no explicit
team_id/assignee_id — the common case, since it only knows team NAMES from
the docstring, not their opaque IDs), the tool silently picked whichever
transfer rule happened to be first in the configured list, regardless of
the actual reason. Every escalation for an agent with multiple transfer
rules was routed to rule evolution-foundation#1 — e.g. an "Imposto de Renda" request got sent
to "Dep. Contábil/Fiscal" (rule evolution-foundation#1) instead of the dedicated IRPF team
(rule evolution-foundation#4), because the code never evaluated rule conditions at all (the
old comment literally said "In the future, this could be enhanced to
evaluate rule conditions").

Fix:
- Add a `rule_index` parameter and instruct the model (via the numbered
  rule list already in its docstring) to always pass it when transfer_rules
  are configured.
- If rule_index is omitted, fall back to matching `reason` against each
  rule's own `instructions` text by keyword overlap.
- Only if neither yields a match, fall back to the first configured rule
  (previous behavior), now logged as a warning so misroutes are visible.

Reproduced live: an "Imposto de Renda Pessoa Física" request was
transferred to "Dep. Contábil/Fiscal" instead of the dedicated team.
…rring

Every transfer rule's own instructions ask the agent to notify the
customer before transferring, but that depended entirely on the model
also producing reply text in the same turn as its tool calls — and when
the model decides to call tools, it frequently skips the customer-facing
text entirely (observed live: manage_conversation_labels + transfer_to_human
fired together with a null/empty assistant message, so the transfer
happened completely silently).

Add a message_to_customer parameter: when provided, the tool sends it as
a real outgoing message BEFORE performing the team/agent assignment, so
"notify then transfer" is one atomic tool action instead of two separate
things the model has to remember to do in sequence. Docstring now
instructs the model to always pass it when the matched rule's
instructions ask for a customer notice.
@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates human transfer handling so the tool can reliably send a customer-facing notice before assignment, surfaces notice delivery failures without blocking routing, and guides the model toward explicit, rule-aware destination selection.

Sequence diagram for customer notice before human transfer

sequenceDiagram
    participant Model
    participant Tool as transfer_to_human
    participant CRM as CRM API

    Model->>Tool: transfer_to_human(message_to_customer, rule_index)
    Tool->>Tool: Select transfer rule
    opt message_to_customer provided
        Tool->>CRM: POST /conversations/{id}/messages(private: false)
        alt Notice sent
            CRM-->>Tool: Success
        else Notice fails
            CRM-->>Tool: Error
            Tool->>Tool: Record message_to_customer_error
        end
    end
    Tool->>CRM: Assign conversation to agent or team
    CRM-->>Tool: Transfer result
    Tool-->>Model: status, message_to_customer_sent, message_to_customer_error
Loading

Flow diagram for rule-aware transfer selection

flowchart TD
    A[Configured transfer rules] --> B{Valid rule_index provided?}
    B -->|Yes| C[Select indexed rule]
    B -->|No| D{Reason matches rule instructions?}
    D -->|Yes| E[Select matching rule]
    D -->|No| F[Fall back to first valid rule]
    C --> G[Resolve agent or team destination]
    E --> G
    F --> G
    G --> H[Send customer notice]
    H --> I[Assign conversation]
Loading

File-Level Changes

Change Details Files
Makes customer notification an explicit, ordered part of human transfer execution.
  • Adds optional customer-facing message input and sends it via the conversation messages endpoint before assignment.
  • Formats notices as public outgoing messages and exposes sent/error status in the successful transfer result.
  • Allows notice-send failures to be reported without preventing the transfer.
src/services/adk/tools/evo_crm/transfer_to_human.py
Improves transfer-rule selection to target the rule chosen by the model or inferred from the transfer reason.
  • Adds 1-based rule_index selection and documents it as required when configured rules are used.
  • Adds keyword matching against rule instructions and retains a warning-producing fallback for unmatched requests.
  • Preserves selected rule destination and instructions when resolving assignee or team.
src/services/adk/tools/evo_crm/transfer_to_human.py
Updates the tool's dynamically generated guidance to enforce correct notification and rule-selection behavior.
  • Lists numbered configured rules and instructs the model to pass the matching rule_index.
  • Explains that message_to_customer must be used when rule instructions require a pre-transfer notice.
  • Documents the new arguments and transfer result behavior.
src/services/adk/tools/evo_crm/transfer_to_human.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/services/adk/tools/evo_crm/transfer_to_human.py" line_range="177-180" />
<code_context>
+                    reason_lower = reason.lower()
+                    for rule in available_transfer_rules:
+                        instructions = (rule.get("instructions") or "").lower()
+                        if instructions and any(
+                            word in instructions
+                            for word in reason_lower.split()
+                            if len(word) > 3
+                        ):
+                            selected_rule = rule
</code_context>
<issue_to_address>
**issue (bug_risk):** The fallback matcher treats any substring overlap with any reason word longer than three characters as a rule match, so common words such as “para”, “cliente”, or “humano” select the first rule containing that word even when the rule is unrelated to the escalation. This misroutes transfers whenever `rule_index` is omitted and multiple rules share ordinary instruction wording.

**Triggers:** When the model omits `rule_index` and the reason contains a common word present in an earlier rule's instructions.

**Suggested fix:** Tokenize and normalize meaningful keywords or use an explicit ambiguity check instead of accepting the first arbitrary substring overlap.
</issue_to_address>

### Comment 2
<location path="src/services/adk/tools/evo_crm/transfer_to_human.py" line_range="163-165" />
<code_context>
+
+                # Preferred path: the model picked a specific rule from the
+                # numbered list in its own docstring (see transfer_rules_doc).
+                if rule_index is not None and 1 <= rule_index <= len(available_transfer_rules):
+                    selected_rule = available_transfer_rules[rule_index - 1]
+                    logger.info(f"Using transfer rule #{rule_index} selected by the model")
+
+                # No explicit index: previously this silently fell back to
</code_context>
<issue_to_address>
**issue (bug_risk):** An out-of-range or zero `rule_index` is silently ignored and the code proceeds to reason matching or the first valid configured rule, so an invalid model selection still performs a potentially incorrect transfer instead of reporting that the requested rule does not exist.

**Triggers:** When the model supplies `rule_index` as 0 or a number greater than the configured rule count.

**Suggested fix:** Validate any non-`None` `rule_index` and return an error before selecting a fallback rule when it is outside the configured range.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and the change can send a customer-facing message before the transfer, and an incorrect or unintended notice cannot be fully undone by reverting the code. Its rule-selection changes can also route a conversation to the wrong configured team or agent, although the resulting transfer is otherwise bounded and can be corrected manually.

Blocking findings: src/services/adk/tools/evo_crm/transfer_to_human.py:180, src/services/adk/tools/evo_crm/transfer_to_human.py:165


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/services/adk/tools/evo_crm/transfer_to_human.py Outdated
Comment thread src/services/adk/tools/evo_crm/transfer_to_human.py Outdated
- Explicit but out-of-range rule_index now returns an error instead of
  silently falling through to keyword matching or the first rule.
- Keyword fallback scores rules by count of shared meaningful,
  non-stopword tokens and picks the best match instead of stopping at
  the first rule containing any word longer than 3 chars.
- The agent-level system prompt said "the tool will automatically use
  the configured transfer rules, so you don't need to specify assignee_id
  or team_id" — directly contradicting the rule_index requirement. Now
  numbers the rules and explicitly requires rule_index + message_to_customer.
@mateusbellozupko

Copy link
Copy Markdown
Author

Fixed in d49a281 — same rule_index validation + scored keyword matching + prompt fix as #55, carried onto this stacked branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant