Files
replypier/backend/tests/agents/test_chat_agent_lead_capture_prompt.py
T

146 lines
6.2 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from unittest.mock import MagicMock, Mock, patch
from uuid import uuid4
from sqlalchemy.orm import Session
from app.agents.chat_agent import ChatAgent
LEAD_FIELDS = [
{"key": "email", "standard": True, "enabled": True, "required": True},
{"key": "name", "standard": True, "enabled": True, "required": True},
{"key": "company", "standard": True, "enabled": True, "required": False},
]
def build_agent(known_email: str = None, known_name: str = None, trusted: bool = True) -> ChatAgent:
"""A ChatAgent with everything external stubbed, to read the prompt it built.
`known_email`/`known_name` stand for a visitor we already hold details for:
one who passed the email gate, or who an embedding app identified.
`trusted` is whether this conversation may be told them (widget_identity).
"""
agent_data = Mock()
agent_data.name = "Sales"
agent_data.jira_enabled = False
agent_data.transfer_to_human = False
agent_data.ask_for_rating = False
agent_data.ticketing_enabled = False
agent_data.instructions = ["You are a helpful assistant"]
lead_config = Mock()
lead_config.enabled = True
lead_config.fields = LEAD_FIELDS
lead_config.require_consent = True
lead_config.guidance = None
customer = Mock()
customer.email = known_email
customer.full_name = known_name
storage = Mock()
storage.get_session_state.return_value = {"status": "active"}
customer_repo = MagicMock()
customer_repo.return_value.get_by_id.return_value = customer
# The real staticmethod decides what counts as a stand-in address.
customer_repo.is_placeholder_email = staticmethod(
lambda email: (not email) or ('@noemail.com' in email) or email.endswith('.channel')
)
lead_repo = MagicMock()
lead_repo.return_value.get_by_agent.return_value = lead_config
with patch('app.agents.chat_agent.SessionLocal') as mock_session_local, \
patch('app.agents.chat_agent.JiraRepository') as mock_jira_repo, \
patch('app.repositories.customer.CustomerRepository', customer_repo), \
patch('app.repositories.lead_capture.LeadCaptureConfigRepository', lead_repo), \
patch('app.services.lead_capture.has_captured_lead', return_value=False), \
patch('app.agents.chat_agent.trusts_identity_in_session', return_value=trusted), \
patch('app.tools.knowledge_search_byagent.KnowledgeSearchByAgent', return_value=Mock()), \
patch('app.agents.chat_agent.EncryptedPostgresAgentStorage', return_value=storage), \
patch('app.agents.chat_agent.settings.DATABASE_URL', "mock://test"), \
patch.dict('os.environ', {}, clear=True):
mock_session_local.return_value.__enter__.return_value = MagicMock(spec=Session)
mock_jira_repo.return_value.get_agent_with_jira_config.return_value = agent_data
return ChatAgent(
api_key="test-key",
model_name="gpt-4",
model_type="openai",
org_id=str(uuid4()),
agent_id=str(uuid4()),
customer_id=str(uuid4()),
session_id=str(uuid4()),
)
def prompt_of(agent: ChatAgent) -> str:
"""The built prompt on one line: the blocks are indented heredocs, so a
sentence is only contiguous once the wrapping whitespace is collapsed."""
instructions = agent.agent.instructions
text = "\n".join(instructions) if isinstance(instructions, list) else instructions
return " ".join((text or "").split())
class TestLeadCapturePrompt:
"""A visitor who passed the email gate, or who an embedding app identified,
was still asked to type their address again — and then told "Email: Not
provided". The details we already hold have to reach the lead-capture block
the same way they reach the ticket block."""
def test_a_known_email_is_handed_over_not_asked_for(self):
agent = build_agent(known_email="ravi@academy.example", known_name="Ravi Kumar")
prompt = prompt_of(agent)
assert "ALREADY KNOWN" in prompt
assert 'their email is "ravi@academy.example" - put exactly that in lead_email' in prompt
assert 'their name is "Ravi Kumar" - put exactly that in lead_name' in prompt
assert "Do NOT ask for these" in prompt
# It must still count toward recording, or the agent stalls waiting for
# a detail it was told not to ask for.
assert "count them as collected" in prompt
# And the visitor can still correct us.
assert "volunteers a different value" in prompt
def test_details_are_not_recited_to_a_visitor_who_only_claimed_the_customer(self):
"""Someone who typed another person's email into the widget is bound to
that customer, but must not hear their name or address from the bot."""
prompt = prompt_of(build_agent(known_email="ravi@academy.example", known_name="Ravi Kumar",
trusted=False))
assert "ALREADY KNOWN" not in prompt
assert "Ravi Kumar" not in prompt
assert "ravi@academy.example" not in prompt
def test_an_unknown_visitor_is_still_asked(self):
prompt = prompt_of(build_agent())
assert "ALREADY KNOWN" not in prompt
# The ordinary collection instructions are untouched.
assert "LEAD CAPTURE (IMPORTANT)" in prompt
assert "REQUIRED: email, name" in prompt
def test_a_placeholder_address_does_not_count_as_known(self):
"""Anonymous widget visitors carry a …@noemail.com stand-in. Handing that
to the agent as the lead's email would record an address nobody can reach."""
prompt = prompt_of(build_agent(known_email="1738000000000@noemail.com"))
assert "ALREADY KNOWN" not in prompt
assert "noemail.com" not in prompt