Files
replypier/backend/tests/knowledge/test_content_summarizer.py
T
WangShen007 54b949b392 chore(brand): rename the product to ReplyPier
The upstream project name is gone from code, assets, docs and build
output: brand strings, domains (replypier.site and its app/api/docs/help
subdomains), env prefixes, image and network names, the widget's public
JS API (window.ReplyPier, replypierId/BaseUrl/Config/Token), CSS tokens
(--rp-*), file and directory names, and the WordPress plugin package.

- widget loader is now webclient/replypier.min.js; both bundles rebuilt
  from source (backend/assets/widget.{js,css}, frontend/public/webclient)
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and config.dev.js
  rewritten rather than string-swapped; the demo config no longer ships
  the previous vendor's Firebase/Razorpay/Google keys
- new migration replypier_ai_model_001 adds REPLYPIER to the managed
  AIModelType and rewrites pre-rename rows
- local compose project, database, role and volume renamed; LOCAL_DEPLOY
  documents carrying the old local data over
2026-10-26 00:51:03 +08:00

380 lines
17 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.
"""
import pytest
from unittest.mock import patch, MagicMock
from app.knowledge.content_summarizer import ContentSummarizer, get_content_summarizer
class TestContentSummarizer:
"""Test suite for ContentSummarizer class"""
def test_init_disabled(self):
"""Test initialization when summarization is disabled"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = False
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = ""
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
summarizer = ContentSummarizer()
assert not summarizer.enabled
assert summarizer._agent is None
def test_init_enabled_no_api_key(self):
"""Test initialization when enabled but no API key"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = ""
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
summarizer = ContentSummarizer()
assert not summarizer.enabled
def test_init_enabled_with_api_key(self):
"""Test initialization when enabled with API key"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
summarizer = ContentSummarizer()
assert summarizer.enabled
assert summarizer.api_key == "test_api_key"
def test_init_openai_compatible_no_base_url(self):
"""OPENAI_COMPATIBLE without a base_url disables summarization, like a missing API key"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "OPENAI_COMPATIBLE"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "local-model"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.KNOWLEDGE_SUMMARY_BASE_URL = ""
summarizer = ContentSummarizer()
assert not summarizer.enabled
def test_init_openai_compatible_with_base_url(self):
"""OPENAI_COMPATIBLE with a base_url stays enabled and passes base_url to create_model"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings, \
patch('app.knowledge.content_summarizer.create_model') as mock_create_model:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "OPENAI_COMPATIBLE"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "local-model"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.KNOWLEDGE_SUMMARY_BASE_URL = "http://localhost:11434/v1"
summarizer = ContentSummarizer()
assert summarizer.enabled
summarizer._get_agent()
mock_create_model.assert_called_once_with(
model_type="OPENAI_COMPATIBLE",
api_key="test_api_key",
model_name="local-model",
max_tokens=4000,
base_url="http://localhost:11434/v1"
)
def test_summarize_disabled(self):
"""Test that original content is returned when summarization is disabled"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = False
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = ""
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
summarizer = ContentSummarizer()
content = "This is test content with a URL: https://example.com/product/123"
result = summarizer.summarize(content, "https://example.com")
assert result == content
def test_summarize_short_content(self):
"""Test that short content is not summarized"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
summarizer = ContentSummarizer()
# Content under 1000 chars
content = "Short content with URL: https://example.com/product/123"
result = summarizer.summarize(content, "https://example.com")
assert result == content
def test_summarize_preserves_urls(self):
"""Test that URLs are preserved in summarization"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings, \
patch('app.knowledge.content_summarizer.create_model') as mock_create_model:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.ENVIRONMENT = "development"
# Mock the model
mock_model = MagicMock()
mock_create_model.return_value = mock_model
# Create mock response with URLs preserved
mock_response = MagicMock()
mock_response.content = "Summary of the product. Check out the product at https://example.com/product/123 and related items at https://example.com/category/products"
# Mock the agent and its run method
mock_agent = MagicMock()
mock_agent.run.return_value = mock_response
summarizer = ContentSummarizer()
# Override the agent with our mock
with patch('app.knowledge.content_summarizer.Agent', return_value=mock_agent):
# Get the agent to initialize it
summarizer._get_agent()
# Long content with multiple URLs
content = """
This is a long piece of content about our products.
We have many great products available at https://example.com/product/123
and you can find more information at https://example.com/category/products.
Our documentation is available at https://docs.example.com/guide
and images can be found at https://cdn.example.com/images/product.jpg.
""" * 50 # Make it long enough to trigger summarization
result = summarizer.summarize(content, "https://example.com")
# Verify URLs are in the result
assert "https://example.com/product/123" in result
assert "https://example.com/category/products" in result
def test_summarize_adds_source_url(self):
"""Test that source URL is added if not present"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings, \
patch('app.knowledge.content_summarizer.create_model') as mock_create_model:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.ENVIRONMENT = "development"
# Mock the model
mock_model = MagicMock()
mock_create_model.return_value = mock_model
# Create mock response without source URL
mock_response = MagicMock()
mock_response.content = "Summary of the product without source URL"
# Mock the agent and its run method
mock_agent = MagicMock()
mock_agent.run.return_value = mock_response
summarizer = ContentSummarizer()
# Override the agent with our mock
with patch('app.knowledge.content_summarizer.Agent', return_value=mock_agent):
# Get the agent to initialize it
summarizer._get_agent()
# Long content
content = "This is a long piece of content about our products. " * 100
result = summarizer.summarize(content, "https://example.com/products")
# Verify source URL is added
assert "Source: https://example.com/products" in result
def test_summarize_error_handling(self):
"""Test that errors are handled gracefully and original content is returned"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings, \
patch('app.knowledge.content_summarizer.create_model') as mock_create_model:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.ENVIRONMENT = "development"
# Mock the model to raise an error
mock_create_model.side_effect = Exception("API Error")
summarizer = ContentSummarizer()
# Long content
content = "This is a long piece of content with URL: https://example.com/product/123 " * 100
result = summarizer.summarize(content, "https://example.com")
# Verify original content is returned on error
assert result == content
def test_get_content_summarizer_singleton(self):
"""Test that get_content_summarizer returns a singleton instance"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
# Reset the global instance
import app.knowledge.content_summarizer
app.knowledge.content_summarizer._summarizer = None
# Get the instance twice
instance1 = get_content_summarizer()
instance2 = get_content_summarizer()
# Verify it's the same instance
assert instance1 is instance2
def test_summarizer_instructions_include_url_preservation(self):
"""Test that the summarizer instructions emphasize URL preservation"""
with patch('app.knowledge.content_summarizer.settings') as mock_settings, \
patch('app.knowledge.content_summarizer.create_model') as mock_create_model, \
patch('app.knowledge.content_summarizer.Agent') as mock_agent_class:
mock_settings.KNOWLEDGE_SUMMARY_ENABLED = True
mock_settings.KNOWLEDGE_SUMMARY_MODEL_TYPE = "GROQ"
mock_settings.KNOWLEDGE_SUMMARY_MODEL_NAME = "llama-3.1-8b-instant"
mock_settings.KNOWLEDGE_SUMMARY_API_KEY = "test_api_key"
mock_settings.KNOWLEDGE_SUMMARY_MAX_TOKENS = 4000
mock_settings.ENVIRONMENT = "development"
# Mock the model
mock_model = MagicMock()
mock_create_model.return_value = mock_model
# Mock the agent
mock_agent_instance = MagicMock()
mock_agent_class.return_value = mock_agent_instance
summarizer = ContentSummarizer()
summarizer._get_agent()
# Verify Agent was called with instructions containing URL preservation
assert mock_agent_class.called
call_kwargs = mock_agent_class.call_args.kwargs
instructions = call_kwargs['instructions']
# Check that instructions emphasize URL preservation
assert 'PRESERVING ALL URLs' in instructions
assert 'Product URLs' in instructions
assert 'DO NOT remove, shorten, or modify ANY URLs' in instructions
class TestChatAgentURLInstructions:
"""Test suite for verifying chat agent includes URL instructions"""
def test_chat_agent_includes_url_instructions(self):
"""Test that chat agent instructions include URL preservation"""
from app.agents.chat_agent import ChatAgent
from uuid import uuid4
# Mock all database and external dependencies
with patch('app.agents.chat_agent.SessionLocal') as mock_session_local, \
patch('app.tools.knowledge_search_byagent.SessionLocal') as mock_knowledge_session_local, \
patch('app.agents.chat_agent.JiraRepository') as mock_jira_repo, \
patch('app.agents.chat_agent.AgentShopifyConfigRepository') as mock_shopify_repo, \
patch('app.repositories.lead_capture.LeadCaptureConfigRepository') as mock_lead_repo, \
patch('app.agents.chat_agent.EncryptedPostgresAgentStorage') as mock_storage, \
patch('app.agents.chat_agent.create_model') as mock_create_model, \
patch('app.agents.chat_agent.Agent') as mock_agent_class:
# No lead-capture config for this agent (returns None → capture off).
mock_lead_repo.return_value.get_by_agent.return_value = None
# Mock database session
mock_db = MagicMock()
mock_session_local.return_value.__enter__.return_value = mock_db
mock_session_local.return_value.__exit__.return_value = None
# Mock knowledge search database session
mock_knowledge_session_local.return_value.__enter__.return_value = mock_db
mock_knowledge_session_local.return_value.__exit__.return_value = None
# Mock Jira repository
from app.models.schemas.jira import AgentWithJiraConfig
from app.models.agent import AgentType
mock_agent_data = AgentWithJiraConfig(
id=uuid4(),
name="Test Agent",
display_name="Test Agent",
description="Test",
instructions=["You are a helpful assistant"],
tools=[],
agent_type=AgentType.CUSTOMER_SUPPORT,
is_default=False,
is_active=True,
organization_id=uuid4(),
transfer_to_human=False,
ask_for_rating=False,
knowledge=[],
jira_enabled=False,
jira_project_key=None,
jira_issue_type_id=None,
groups=[],
organization=None
)
mock_jira_repo.return_value.get_agent_with_jira_config.return_value = mock_agent_data
mock_shopify_repo.return_value.get_agent_shopify_config.return_value = None
# Mock storage and model
mock_storage.return_value = MagicMock()
mock_create_model.return_value = MagicMock()
mock_agent_instance = MagicMock()
mock_agent_class.return_value = mock_agent_instance
# Create chat agent
chat_agent = ChatAgent(
api_key="test_key",
model_name="gpt-4",
model_type="OPENAI",
org_id=str(uuid4()),
agent_id=str(mock_agent_data.id),
session_id=str(uuid4())
)
# Verify Agent was called with instructions containing URL guidance
assert mock_agent_class.called
call_kwargs = mock_agent_class.call_args.kwargs
instructions = call_kwargs['instructions']
# Check that instructions include URL preservation guidance
assert 'Include URLs in Responses' in instructions or 'MUST include these URLs' in instructions
assert 'URLs' in instructions