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
191 lines
7.2 KiB
Python
191 lines
7.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 typing import Optional
|
|
from agno.agent import Agent
|
|
from app.utils.agno_utils import create_model
|
|
from app.core.logger import get_logger
|
|
from app.core.config import settings
|
|
from app.core.model_catalog import requires_base_url
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class ContentSummarizer:
|
|
"""
|
|
Service for summarizing web content using LLM to reduce token usage.
|
|
Preserves important information and URLs for reference.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the content summarizer with configured model"""
|
|
self.enabled = settings.KNOWLEDGE_SUMMARY_ENABLED
|
|
self.model_type = settings.KNOWLEDGE_SUMMARY_MODEL_TYPE
|
|
self.model_name = settings.KNOWLEDGE_SUMMARY_MODEL_NAME
|
|
self.api_key = settings.KNOWLEDGE_SUMMARY_API_KEY
|
|
self.base_url = settings.KNOWLEDGE_SUMMARY_BASE_URL or None
|
|
self.max_tokens = settings.KNOWLEDGE_SUMMARY_MAX_TOKENS
|
|
self._agent = None
|
|
|
|
if self.enabled and not self.api_key:
|
|
logger.warning("Content summarization is enabled but KNOWLEDGE_SUMMARY_API_KEY is not set. Summarization will be disabled.")
|
|
self.enabled = False
|
|
|
|
if self.enabled and requires_base_url(self.model_type) and not self.base_url:
|
|
logger.warning(f"Content summarization model type is {self.model_type} but KNOWLEDGE_SUMMARY_BASE_URL is not set. Summarization will be disabled.")
|
|
self.enabled = False
|
|
|
|
def _get_agent(self) -> Optional[Agent]:
|
|
"""Lazy initialization of the summarization agent"""
|
|
if not self.enabled:
|
|
return None
|
|
|
|
if self._agent is None:
|
|
try:
|
|
model = create_model(
|
|
model_type=self.model_type,
|
|
api_key=self.api_key,
|
|
model_name=self.model_name,
|
|
max_tokens=self.max_tokens,
|
|
base_url=self.base_url
|
|
)
|
|
|
|
self._agent = Agent(
|
|
name="Content Summarizer",
|
|
model=model,
|
|
instructions="""You are an expert content summarizer for a knowledge base system.
|
|
|
|
Your task is to summarize web page content while:
|
|
1. **PRESERVING ALL URLs** - This is CRITICAL. Every single URL mentioned in the content MUST be kept in the summary:
|
|
- Product URLs
|
|
- Reference URLs
|
|
- Image URLs
|
|
- Documentation URLs
|
|
- Any links mentioned in the text
|
|
DO NOT remove, shorten, or modify ANY URLs. Keep them exactly as they appear.
|
|
|
|
2. Preserving ALL important information needed to answer user questions:
|
|
- Product names, descriptions, and details
|
|
- Pricing and availability information
|
|
- Technical specifications and features
|
|
- Contact information and addresses
|
|
- Instructions and procedures
|
|
- Important facts and data points
|
|
|
|
3. Reducing only redundant and verbose text:
|
|
- Remove excessive marketing language
|
|
- Eliminate repetitive phrases
|
|
- Condense wordy explanations
|
|
- Remove filler content
|
|
|
|
4. Maintaining the original context and structure so users can:
|
|
- Find specific products or information
|
|
- Access the original URLs for more details
|
|
- Get accurate answers to their questions
|
|
|
|
REMEMBER: URLs are the most critical element to preserve. Users need these links to access products and resources. Never remove or modify URLs under any circumstances.
|
|
|
|
Format your response as a concise summary that maintains all URLs and critical information.""",
|
|
markdown=False,
|
|
debug_mode=settings.ENVIRONMENT == "development"
|
|
)
|
|
logger.info(f"Initialized content summarizer with model: {self.model_type}/{self.model_name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize content summarizer: {str(e)}")
|
|
self.enabled = False
|
|
return None
|
|
|
|
return self._agent
|
|
|
|
def summarize(self, content: str, url: str) -> str:
|
|
"""
|
|
Summarize content while preserving important information and URLs.
|
|
|
|
Args:
|
|
content: The original content to summarize
|
|
url: The source URL for reference
|
|
|
|
Returns:
|
|
Summarized content or original content if summarization fails/disabled
|
|
"""
|
|
# If summarization is disabled, return original content
|
|
if not self.enabled:
|
|
return content
|
|
|
|
# If content is already short, don't summarize
|
|
if len(content) < 1000:
|
|
logger.debug(f"Content too short to summarize ({len(content)} chars), keeping original")
|
|
return content
|
|
|
|
try:
|
|
agent = self._get_agent()
|
|
if not agent:
|
|
logger.warning("Summarization agent not available, using original content")
|
|
return content
|
|
|
|
# Create the summarization prompt
|
|
prompt = f"""Summarize the following web page content from {url}:
|
|
|
|
Content:
|
|
{content}
|
|
|
|
CRITICAL REQUIREMENTS:
|
|
- **KEEP EVERY SINGLE URL EXACTLY AS IT APPEARS** - Do not remove, shorten, or modify any URLs
|
|
- Preserve all product URLs, reference links, and resource URLs
|
|
- Keep all important facts, product details, pricing, and specifications
|
|
- Maintain context for answering user questions
|
|
- Only reduce redundant/verbose text, never remove URLs or important information"""
|
|
|
|
logger.info(f"Summarizing content from {url} ({len(content)} chars)")
|
|
|
|
# Run the agent synchronously
|
|
response = agent.run(message=prompt)
|
|
|
|
# Extract the summary from response
|
|
if hasattr(response, 'content') and response.content:
|
|
summary = response.content
|
|
elif isinstance(response, dict) and 'content' in response:
|
|
summary = response['content']
|
|
elif isinstance(response, str):
|
|
summary = response
|
|
else:
|
|
logger.error(f"Unexpected response format from summarizer: {type(response)}")
|
|
return content
|
|
|
|
# Add source URL reference at the end if not already present
|
|
if url not in summary:
|
|
summary = f"{summary}\n\nSource: {url}"
|
|
|
|
logger.info(f"Summarized content: {len(content)} -> {len(summary)} chars ({100*len(summary)/len(content):.1f}%)")
|
|
|
|
return summary
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error summarizing content from {url}: {str(e)}")
|
|
logger.info("Falling back to original content")
|
|
return content
|
|
|
|
|
|
# Global instance
|
|
_summarizer = None
|
|
|
|
def get_content_summarizer() -> ContentSummarizer:
|
|
"""Get or create the global content summarizer instance"""
|
|
global _summarizer
|
|
if _summarizer is None:
|
|
_summarizer = ContentSummarizer()
|
|
return _summarizer
|