Spaces:
Sleeping
Sleeping
Upload 29 files
Browse files- app/__init__.py +8 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-313.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/config.py +36 -0
- app/main.py +304 -0
- app/models/__pycache__/enums.cpython-311.pyc +0 -0
- app/models/__pycache__/enums.cpython-313.pyc +0 -0
- app/models/__pycache__/schemas.cpython-311.pyc +0 -0
- app/models/__pycache__/schemas.cpython-313.pyc +0 -0
- app/models/enums.py +15 -0
- app/models/schemas.py +71 -0
- app/services/__pycache__/llm_client.cpython-311.pyc +0 -0
- app/services/__pycache__/llm_client.cpython-313.pyc +0 -0
- app/services/__pycache__/search.cpython-311.pyc +0 -0
- app/services/__pycache__/search.cpython-313.pyc +0 -0
- app/services/llm_client.py +180 -0
- app/services/search.py +64 -0
- app/utils/__pycache__/charts.cpython-311.pyc +0 -0
- app/utils/__pycache__/charts.cpython-313.pyc +0 -0
- app/utils/__pycache__/pdf_generator.cpython-311.pyc +0 -0
- app/utils/__pycache__/pdf_generator.cpython-313.pyc +0 -0
- app/utils/charts.py +100 -0
- app/utils/pdf_generator.py +50 -0
- logging_config.py +62 -0
- logs/rival_lens.log +0 -0
app/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RivalLens - Competitor Intelligence API
|
| 3 |
+
|
| 4 |
+
This package provides functionality for analyzing and comparing companies
|
| 5 |
+
in a given market space using various data sources and LLM-powered analysis.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
__version__ = "0.1.0"
|
app/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (388 Bytes). View file
|
|
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (412 Bytes). View file
|
|
|
app/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.98 kB). View file
|
|
|
app/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (1.81 kB). View file
|
|
|
app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (14.2 kB). View file
|
|
|
app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
app/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration settings for the RivalLens application."""
|
| 2 |
+
import os
|
| 3 |
+
from pydantic_settings import BaseSettings
|
| 4 |
+
from pydantic import HttpUrl
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
class Settings(BaseSettings):
|
| 8 |
+
# API Keys
|
| 9 |
+
DEEPSEEK_API_KEY: str
|
| 10 |
+
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
|
| 11 |
+
DEEPSEEK_ENDPOINT: Optional[str] = None # For backward compatibility
|
| 12 |
+
NEWS_API_KEY: Optional[str] = None
|
| 13 |
+
CRUNCHBASE_API_KEY: Optional[str] = None
|
| 14 |
+
|
| 15 |
+
# Application settings
|
| 16 |
+
APP_NAME: str = "RivalLens API"
|
| 17 |
+
DEBUG: bool = False
|
| 18 |
+
|
| 19 |
+
# API settings
|
| 20 |
+
API_PREFIX: str = "/api/v1"
|
| 21 |
+
MAX_COMPETITORS: int = 5
|
| 22 |
+
DEFAULT_CITATION_DEPTH: int = 3
|
| 23 |
+
|
| 24 |
+
class Config:
|
| 25 |
+
env_file = ".env"
|
| 26 |
+
env_file_encoding = 'utf-8'
|
| 27 |
+
case_sensitive = True
|
| 28 |
+
extra = 'ignore' # Ignore extra environment variables
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def customise_sources(cls, init_settings, env_settings, file_secret_settings):
|
| 32 |
+
# This ensures that .env file is loaded with higher priority than environment variables
|
| 33 |
+
return (env_settings, init_settings, file_secret_settings)
|
| 34 |
+
|
| 35 |
+
# Create settings instance
|
| 36 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI application for RivalLens - Competitor Intelligence API
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
|
| 11 |
+
from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 14 |
+
|
| 15 |
+
# Configure logging before other imports to ensure all modules use it
|
| 16 |
+
from logging_config import configure_logging
|
| 17 |
+
logger = configure_logging()
|
| 18 |
+
|
| 19 |
+
# Now import other modules
|
| 20 |
+
from app.models.schemas import UserPayload, ReportResponse, CompanyData, CompetitorInsight
|
| 21 |
+
from app.services.llm_client import llm
|
| 22 |
+
from app.services.search import search_adapter
|
| 23 |
+
from app.utils import pdf_generator, charts
|
| 24 |
+
from app.models.enums import InfoCategory
|
| 25 |
+
from app.config import settings
|
| 26 |
+
|
| 27 |
+
# Log configuration status
|
| 28 |
+
logger.info(f"Starting {settings.APP_NAME}")
|
| 29 |
+
logger.debug(f"Debug mode: {settings.DEBUG}")
|
| 30 |
+
logger.debug(f"Using API URL: {settings.DEEPSEEK_API_URL}")
|
| 31 |
+
logger.debug(f"API Key configured: {'Yes' if settings.DEEPSEEK_API_KEY else 'No'}")
|
| 32 |
+
|
| 33 |
+
# Initialize FastAPI app
|
| 34 |
+
app = FastAPI(
|
| 35 |
+
title=settings.APP_NAME,
|
| 36 |
+
description="API for generating competitive intelligence reports",
|
| 37 |
+
version="1.0.0",
|
| 38 |
+
debug=settings.DEBUG
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Log application startup
|
| 42 |
+
logger.info(f"{settings.APP_NAME} v1.0.0 starting up...")
|
| 43 |
+
logger.info(f"Environment: {'development' if settings.DEBUG else 'production'}")
|
| 44 |
+
logger.info(f"API Key: {'Configured' if settings.DEEPSEEK_API_KEY else 'Not configured'}")
|
| 45 |
+
|
| 46 |
+
# Add CORS middleware
|
| 47 |
+
app.add_middleware(
|
| 48 |
+
CORSMiddleware,
|
| 49 |
+
allow_origins=["*"],
|
| 50 |
+
allow_credentials=True,
|
| 51 |
+
allow_methods=["*"],
|
| 52 |
+
allow_headers=["*"],
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Helper functions
|
| 56 |
+
async def build_system_prompt(company_name: str, insight_selection: List[str], deep_dive: Optional[List[str]] = None) -> str:
|
| 57 |
+
"""Build a robust system prompt for the LLM based on user payload."""
|
| 58 |
+
categories = ", ".join(insight_selection)
|
| 59 |
+
deep_dive_text = ""
|
| 60 |
+
if deep_dive:
|
| 61 |
+
deep_dive_text = f"\nFor a deeper analysis, focus on: {', '.join(deep_dive)}."
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
f"You are a competitive intelligence analyst for business strategy. "
|
| 65 |
+
f"Analyze the company '{company_name}' and its competitors. "
|
| 66 |
+
f"Focus specifically on these categories: {categories}.{deep_dive_text}\n"
|
| 67 |
+
f"Provide actionable insights, highlight trends, gaps, and opportunities. "
|
| 68 |
+
f"Be concise, professional, and data-driven. "
|
| 69 |
+
f"Include an executive summary, detailed competitor insights, and a side-by-side comparison where possible. "
|
| 70 |
+
f"Use real metrics and notes to synthesize meaningful analysis. "
|
| 71 |
+
f"Do not use generic phrases like 'leading company' or 'based on available data'."
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
async def generate_insights(company: CompanyData, categories: list, business_name: str = "your business") -> CompetitorInsight:
|
| 76 |
+
"""Generate insights for a single company using the LLM.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
company: The company data to analyze
|
| 80 |
+
categories: List of categories to focus the analysis on
|
| 81 |
+
business_name: Name of the business being analyzed (for context in the prompt)
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
CompetitorInsight: Detailed insights about the company
|
| 85 |
+
|
| 86 |
+
Raises:
|
| 87 |
+
ValueError: If the API key is not configured
|
| 88 |
+
httpx.HTTPStatusError: If the API request fails
|
| 89 |
+
Exception: For any other unexpected errors
|
| 90 |
+
"""
|
| 91 |
+
logger = logging.getLogger(__name__)
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
# Build a detailed prompt for the LLM
|
| 95 |
+
system_prompt = (
|
| 96 |
+
f"You are a competitive intelligence analyst for business strategy. "
|
| 97 |
+
f"Analyze the company '{company.name}' as a competitor to '{business_name}'. "
|
| 98 |
+
f"Focus specifically on these categories: {', '.join(categories)}. "
|
| 99 |
+
f"Provide actionable insights, highlight trends, gaps, and opportunities. "
|
| 100 |
+
f"Be concise, professional, and data-driven. "
|
| 101 |
+
f"Use the company's metrics and notes to provide specific, meaningful analysis. "
|
| 102 |
+
f"Do not use generic phrases like 'leading company' or 'based on available data'."
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
user_prompt = (
|
| 106 |
+
f"Company: {company.name}\n"
|
| 107 |
+
f"Description: {company.description or 'No description available'}\n"
|
| 108 |
+
f"Metrics: {company.metrics or 'No metrics available'}\n"
|
| 109 |
+
f"Notes: {', '.join(company.notes) if company.notes else 'No notes available'}\n\n"
|
| 110 |
+
f"Please provide a detailed analysis including: "
|
| 111 |
+
f"1. A comprehensive summary of {company.name}'s competitive position\n"
|
| 112 |
+
f"2. Analysis for each of these categories: {', '.join(categories)}\n"
|
| 113 |
+
f"3. Key strengths and weaknesses compared to {business_name}"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
logger.info(f"Generating insights for {company.name}...")
|
| 117 |
+
llm_response = await llm.summarize(system_prompt, user_prompt)
|
| 118 |
+
|
| 119 |
+
# Process the LLM response
|
| 120 |
+
if not llm_response:
|
| 121 |
+
raise ValueError("Empty response received from LLM")
|
| 122 |
+
|
| 123 |
+
# Use the full response as the summary
|
| 124 |
+
summary = llm_response
|
| 125 |
+
|
| 126 |
+
# Create a category breakdown that includes the full analysis for each category
|
| 127 |
+
category_breakdown = {}
|
| 128 |
+
lines = [line.strip() for line in llm_response.split('\n') if line.strip()]
|
| 129 |
+
|
| 130 |
+
# If we have categories, try to find sections for each one
|
| 131 |
+
if categories:
|
| 132 |
+
for category in categories:
|
| 133 |
+
# Find all lines that start with the category name or a heading marker
|
| 134 |
+
category_lines = []
|
| 135 |
+
in_category = False
|
| 136 |
+
|
| 137 |
+
for line in lines:
|
| 138 |
+
# Check if this line starts a new category section
|
| 139 |
+
if (line.lower().startswith(f"{category.lower()}:") or
|
| 140 |
+
line.lower().startswith(f"**{category.lower()}**") or
|
| 141 |
+
line.lower().startswith(f"### {category}")):
|
| 142 |
+
in_category = True
|
| 143 |
+
category_lines.append(line)
|
| 144 |
+
# If we're in a category section, add lines until we hit another category
|
| 145 |
+
elif in_category and any(line.lower().startswith(f"{cat.lower()}: ") for cat in categories if cat != category):
|
| 146 |
+
in_category = False
|
| 147 |
+
break
|
| 148 |
+
elif in_category:
|
| 149 |
+
category_lines.append(line)
|
| 150 |
+
|
| 151 |
+
# If we found lines for this category, join them. Otherwise, use the full response.
|
| 152 |
+
if category_lines:
|
| 153 |
+
category_breakdown[category] = '\n'.join(category_lines)
|
| 154 |
+
else:
|
| 155 |
+
category_breakdown[category] = llm_response
|
| 156 |
+
else:
|
| 157 |
+
# If no specific categories, include the full response for a default category
|
| 158 |
+
category_breakdown["analysis"] = llm_response
|
| 159 |
+
|
| 160 |
+
logger.info(f"Successfully generated insights for {company.name}")
|
| 161 |
+
return CompetitorInsight(
|
| 162 |
+
company=company,
|
| 163 |
+
summary=summary,
|
| 164 |
+
confidence="high", # Since we're using real LLM now
|
| 165 |
+
category_breakdown=category_breakdown,
|
| 166 |
+
sources=company.sources[:3] # Limit to top 3 sources
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
error_msg = f"Failed to generate insights for {company.name}: {str(e)}"
|
| 171 |
+
logger.error(error_msg, exc_info=True)
|
| 172 |
+
raise
|
| 173 |
+
|
| 174 |
+
# API Endpoints
|
| 175 |
+
@app.get("/")
|
| 176 |
+
async def root():
|
| 177 |
+
"""Root endpoint with basic API information."""
|
| 178 |
+
return {
|
| 179 |
+
"app": settings.APP_NAME,
|
| 180 |
+
"status": "running",
|
| 181 |
+
"version": "1.0.0"
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
@app.post("/api/v1/analyze", response_model=ReportResponse)
|
| 185 |
+
async def analyze_competitors(
|
| 186 |
+
payload: UserPayload,
|
| 187 |
+
background_tasks: BackgroundTasks
|
| 188 |
+
):
|
| 189 |
+
"""
|
| 190 |
+
Main endpoint for competitor analysis.
|
| 191 |
+
"""
|
| 192 |
+
# Generate a unique request ID
|
| 193 |
+
request_id = str(uuid.uuid4())
|
| 194 |
+
|
| 195 |
+
# Get company info
|
| 196 |
+
company_name = payload.company_info.name
|
| 197 |
+
company_website = payload.company_info.website or ""
|
| 198 |
+
|
| 199 |
+
# Get competitors
|
| 200 |
+
competitors = []
|
| 201 |
+
if payload.competitor_choice and payload.competitor_choice.competitors:
|
| 202 |
+
competitors = payload.competitor_choice.competitors
|
| 203 |
+
else:
|
| 204 |
+
# Auto-discover competitors if none provided
|
| 205 |
+
competitors = await search_adapter.discover_competitors(
|
| 206 |
+
company_name,
|
| 207 |
+
payload.business_category,
|
| 208 |
+
None, # geography can be added later
|
| 209 |
+
settings.MAX_COMPETITORS
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
# Check if we have competitors to analyze
|
| 213 |
+
if not competitors:
|
| 214 |
+
raise HTTPException(
|
| 215 |
+
status_code=400,
|
| 216 |
+
detail="No competitors found or provided for analysis"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Generate insights for each competitor
|
| 220 |
+
tasks = []
|
| 221 |
+
for competitor in competitors:
|
| 222 |
+
company_data = await search_adapter.enrich_company(
|
| 223 |
+
competitor,
|
| 224 |
+
citation_depth=3, # Default citation depth
|
| 225 |
+
geography=None # Can be updated if needed
|
| 226 |
+
)
|
| 227 |
+
task = generate_insights(
|
| 228 |
+
company=company_data,
|
| 229 |
+
categories=payload.insight_selection,
|
| 230 |
+
business_name=company_name
|
| 231 |
+
)
|
| 232 |
+
tasks.append(task)
|
| 233 |
+
|
| 234 |
+
# Run all tasks concurrently
|
| 235 |
+
insights = await asyncio.gather(*tasks, return_exceptions=True)
|
| 236 |
+
|
| 237 |
+
# Handle any errors
|
| 238 |
+
valid_insights = []
|
| 239 |
+
for i, insight in enumerate(insights):
|
| 240 |
+
if isinstance(insight, Exception):
|
| 241 |
+
logger.error(f"Error processing {competitors[i]}: {str(insight)}")
|
| 242 |
+
else:
|
| 243 |
+
valid_insights.append(insight)
|
| 244 |
+
|
| 245 |
+
if not valid_insights:
|
| 246 |
+
raise HTTPException(
|
| 247 |
+
status_code=500,
|
| 248 |
+
detail="Failed to generate insights for any competitors"
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Prepare response with valid insights
|
| 252 |
+
report = ReportResponse(
|
| 253 |
+
request_id=request_id,
|
| 254 |
+
executive_summary=f"Analysis of {len(valid_insights)} competitors for {company_name}.",
|
| 255 |
+
top_insights=[
|
| 256 |
+
f"{insight.company.name}: {insight.summary.splitlines()[0] if insight.summary else 'No summary available'}"
|
| 257 |
+
for insight in valid_insights
|
| 258 |
+
],
|
| 259 |
+
detailed={
|
| 260 |
+
insight.company.name: {
|
| 261 |
+
"company": insight.company.dict(),
|
| 262 |
+
"summary": insight.summary,
|
| 263 |
+
"confidence": insight.confidence,
|
| 264 |
+
"category_breakdown": insight.category_breakdown,
|
| 265 |
+
"sources": [
|
| 266 |
+
source.dict() if hasattr(source, 'dict') else source
|
| 267 |
+
for source in insight.sources
|
| 268 |
+
]
|
| 269 |
+
}
|
| 270 |
+
for insight in valid_insights
|
| 271 |
+
},
|
| 272 |
+
comparison_table=[], # This would be populated in a real implementation
|
| 273 |
+
generated_at=datetime.utcnow(),
|
| 274 |
+
sources=[], # This would aggregate sources in a real implementation
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Handle export if requested
|
| 278 |
+
if payload.preferences and payload.preferences.export_format:
|
| 279 |
+
export_format = payload.preferences.export_format.lower()
|
| 280 |
+
chart_tasks = []
|
| 281 |
+
|
| 282 |
+
# For now, we'll just log the export request
|
| 283 |
+
# In a real implementation, you would generate the appropriate export
|
| 284 |
+
logger.info(f"Export requested in format: {export_format}")
|
| 285 |
+
|
| 286 |
+
if export_format == 'pdf':
|
| 287 |
+
# In a real implementation, you would generate charts and PDF here
|
| 288 |
+
# For now, we'll just add a placeholder
|
| 289 |
+
report.pdf_url = f"/api/v1/exports/{request_id}.pdf"
|
| 290 |
+
|
| 291 |
+
return report
|
| 292 |
+
|
| 293 |
+
async def generate_pdf_export(request_id: str, report_data: dict, charts: list):
|
| 294 |
+
"""Background task to generate and store PDF report."""
|
| 295 |
+
# In a real implementation, you would:
|
| 296 |
+
# 1. Generate the PDF
|
| 297 |
+
# 2. Store it in a persistent storage (S3, filesystem, etc.)
|
| 298 |
+
# 3. Update the report status in your database
|
| 299 |
+
pass
|
| 300 |
+
|
| 301 |
+
# Example usage
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
import uvicorn
|
| 304 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
app/models/__pycache__/enums.cpython-311.pyc
ADDED
|
Binary file (1.12 kB). View file
|
|
|
app/models/__pycache__/enums.cpython-313.pyc
ADDED
|
Binary file (1.04 kB). View file
|
|
|
app/models/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (5.16 kB). View file
|
|
|
app/models/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (4.36 kB). View file
|
|
|
app/models/enums.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Enums and constants for the RivalLens application."""
|
| 2 |
+
from enum import Enum, auto
|
| 3 |
+
|
| 4 |
+
class InfoCategory(str, Enum):
|
| 5 |
+
"""Allowed parent categories for competitor information."""
|
| 6 |
+
MARKET_PRESENCE = "Market Presence"
|
| 7 |
+
FINANCIAL_HEALTH = "Financial Health"
|
| 8 |
+
PRODUCTS = "Products & Offerings"
|
| 9 |
+
MARKETING = "Marketing & Branding"
|
| 10 |
+
TECH = "Technology & Innovation"
|
| 11 |
+
CUSTOMER_SENTIMENT = "Customer Sentiment"
|
| 12 |
+
HIRING = "Hiring & Organization"
|
| 13 |
+
|
| 14 |
+
# Set of all allowed categories for validation
|
| 15 |
+
ALLOWED_CATEGORIES = {category.value for category in InfoCategory}
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic models for request/response schemas."""
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from typing import List, Dict, Any, Optional
|
| 4 |
+
from pydantic import BaseModel, Field, HttpUrl
|
| 5 |
+
from .enums import InfoCategory
|
| 6 |
+
|
| 7 |
+
class CompanyInfo(BaseModel):
|
| 8 |
+
"""Company information model."""
|
| 9 |
+
name: str = Field(..., description="Name of the company")
|
| 10 |
+
website: Optional[str] = Field(None, description="Company website URL")
|
| 11 |
+
|
| 12 |
+
class CompetitorChoice(BaseModel):
|
| 13 |
+
"""Competitor selection model."""
|
| 14 |
+
competitors: List[str] = Field(..., description="List of competitor names")
|
| 15 |
+
|
| 16 |
+
class Preferences(BaseModel):
|
| 17 |
+
"""User preferences for the analysis."""
|
| 18 |
+
export_format: Optional[str] = Field(
|
| 19 |
+
None,
|
| 20 |
+
description="Export format: PDF, Slide Deck, or None",
|
| 21 |
+
example="PDF"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
class UserPayload(BaseModel):
|
| 25 |
+
"""Request payload from the user."""
|
| 26 |
+
business_category: str = Field(..., description="Business category/industry")
|
| 27 |
+
company_info: CompanyInfo = Field(..., description="Information about the company")
|
| 28 |
+
competitor_choice: Optional[CompetitorChoice] = Field(
|
| 29 |
+
None,
|
| 30 |
+
description="Competitor selection (required for manual competitor selection)"
|
| 31 |
+
)
|
| 32 |
+
insight_selection: List[str] = Field(
|
| 33 |
+
...,
|
| 34 |
+
description="List of insights to include in the report"
|
| 35 |
+
)
|
| 36 |
+
deep_dive: Optional[List[str]] = Field(
|
| 37 |
+
None,
|
| 38 |
+
description="List of areas for deeper analysis"
|
| 39 |
+
)
|
| 40 |
+
preferences: Optional[Preferences] = Field(
|
| 41 |
+
None,
|
| 42 |
+
description="User preferences for the analysis"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
class CompanyData(BaseModel):
|
| 46 |
+
"""Data structure for company information."""
|
| 47 |
+
name: str
|
| 48 |
+
website: Optional[str] = None
|
| 49 |
+
description: Optional[str] = None
|
| 50 |
+
metrics: Dict[str, Any] = {}
|
| 51 |
+
notes: List[str] = []
|
| 52 |
+
sources: List[Dict[str, Any]] = []
|
| 53 |
+
|
| 54 |
+
class CompetitorInsight(BaseModel):
|
| 55 |
+
"""Detailed insights for a single competitor."""
|
| 56 |
+
company: CompanyData
|
| 57 |
+
summary: str
|
| 58 |
+
confidence: str
|
| 59 |
+
category_breakdown: Dict[str, str] = {}
|
| 60 |
+
sources: List[Dict[str, Any]] = []
|
| 61 |
+
|
| 62 |
+
class ReportResponse(BaseModel):
|
| 63 |
+
"""Response model for the analysis report."""
|
| 64 |
+
request_id: str
|
| 65 |
+
executive_summary: str
|
| 66 |
+
top_insights: List[str]
|
| 67 |
+
detailed: Dict[str, CompetitorInsight]
|
| 68 |
+
comparison_table: List[Dict[str, Any]]
|
| 69 |
+
generated_at: datetime
|
| 70 |
+
sources: List[Dict[str, Any]]
|
| 71 |
+
pdf_url: Optional[str] = None
|
app/services/__pycache__/llm_client.cpython-311.pyc
ADDED
|
Binary file (8.1 kB). View file
|
|
|
app/services/__pycache__/llm_client.cpython-313.pyc
ADDED
|
Binary file (7.36 kB). View file
|
|
|
app/services/__pycache__/search.cpython-311.pyc
ADDED
|
Binary file (3.83 kB). View file
|
|
|
app/services/__pycache__/search.cpython-313.pyc
ADDED
|
Binary file (3.47 kB). View file
|
|
|
app/services/llm_client.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM client for interacting with Deepseek API."""
|
| 2 |
+
import httpx
|
| 3 |
+
import time
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
+
from app.config import settings
|
| 8 |
+
|
| 9 |
+
# Get the root logger
|
| 10 |
+
logger = logging.getLogger()
|
| 11 |
+
|
| 12 |
+
class LLMClient:
|
| 13 |
+
"""Client for interacting with LLM services."""
|
| 14 |
+
|
| 15 |
+
def __init__(self,
|
| 16 |
+
api_url: Optional[str] = settings.DEEPSEEK_API_URL,
|
| 17 |
+
api_key: Optional[str] = settings.DEEPSEEK_API_KEY):
|
| 18 |
+
self.api_url = api_url
|
| 19 |
+
self.api_key = api_key
|
| 20 |
+
|
| 21 |
+
async def summarize(self, system_prompt: str, user_prompt: str, max_tokens: int = 600) -> str:
|
| 22 |
+
"""
|
| 23 |
+
Call LLM to produce a summary. Returns text.
|
| 24 |
+
Raises:
|
| 25 |
+
ValueError: If API key is not configured
|
| 26 |
+
httpx.HTTPStatusError: If the API request fails
|
| 27 |
+
"""
|
| 28 |
+
# Log API key status (masking the actual key for security)
|
| 29 |
+
if not self.api_key:
|
| 30 |
+
error_msg = "Deepseek API key is not configured. Please set DEEPSEEK_API_KEY in your environment variables."
|
| 31 |
+
logger.error(error_msg)
|
| 32 |
+
raise ValueError(error_msg)
|
| 33 |
+
|
| 34 |
+
api_key_display = f"{self.api_key[:4]}...{self.api_key[-4:]}"
|
| 35 |
+
|
| 36 |
+
# Log request details
|
| 37 |
+
logger.info("=== LLM API Request ===")
|
| 38 |
+
# logger.info(f"API Endpoint: {self.api_url}")
|
| 39 |
+
# logger.info(f"API Key: {api_key_display}")
|
| 40 |
+
# logger.info("System Prompt:")
|
| 41 |
+
# logger.info(system_prompt)
|
| 42 |
+
logger.info("\nUser Prompt:")
|
| 43 |
+
# logger.info(user_prompt)
|
| 44 |
+
# logger.info(f"Max Tokens: {max_tokens}")
|
| 45 |
+
|
| 46 |
+
# Record start time for performance tracking
|
| 47 |
+
start_time = time.time()
|
| 48 |
+
|
| 49 |
+
headers = {
|
| 50 |
+
"Content-Type": "application/json",
|
| 51 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 52 |
+
}
|
| 53 |
+
payload = {
|
| 54 |
+
"model": "deepseek-chat",
|
| 55 |
+
"messages": [
|
| 56 |
+
{"role": "system", "content": system_prompt},
|
| 57 |
+
{"role": "user", "content": user_prompt}
|
| 58 |
+
],
|
| 59 |
+
"max_tokens": max_tokens,
|
| 60 |
+
"temperature": 0.7
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Prepare request data
|
| 64 |
+
request_data = {
|
| 65 |
+
"model": "deepseek-chat",
|
| 66 |
+
"messages": [
|
| 67 |
+
{"role": "system", "content": system_prompt},
|
| 68 |
+
{"role": "user", "content": user_prompt}
|
| 69 |
+
],
|
| 70 |
+
"max_tokens": max_tokens,
|
| 71 |
+
"temperature": 0.7,
|
| 72 |
+
"top_p": 1.0,
|
| 73 |
+
"frequency_penalty": 0.0,
|
| 74 |
+
"presence_penalty": 0.0
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
headers = {
|
| 78 |
+
"Content-Type": "application/json",
|
| 79 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 80 |
+
"Accept": "application/json"
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# Log full request payload
|
| 84 |
+
logger.info("\nRequest Payload:")
|
| 85 |
+
logger.info(json.dumps(request_data, indent=2, ensure_ascii=False))
|
| 86 |
+
logger.info("\nSending request...")
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 90 |
+
# Make the API request
|
| 91 |
+
resp = await client.post(
|
| 92 |
+
self.api_url,
|
| 93 |
+
json=request_data,
|
| 94 |
+
headers=headers,
|
| 95 |
+
timeout=60.0
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Calculate request duration
|
| 99 |
+
duration = time.time() - start_time
|
| 100 |
+
|
| 101 |
+
# Log response status and timing
|
| 102 |
+
logger.info(f"\n=== LLM API Response ===")
|
| 103 |
+
logger.info(f"Status Code: {resp.status_code}")
|
| 104 |
+
logger.info(f"Response Time: {duration:.2f} seconds")
|
| 105 |
+
|
| 106 |
+
# Parse response
|
| 107 |
+
resp.raise_for_status()
|
| 108 |
+
data = resp.json()
|
| 109 |
+
|
| 110 |
+
# Log full response
|
| 111 |
+
logger.info("\nResponse Headers:")
|
| 112 |
+
for header, value in resp.headers.items():
|
| 113 |
+
logger.info(f" {header}: {value}")
|
| 114 |
+
|
| 115 |
+
logger.info("\nResponse Body:")
|
| 116 |
+
logger.info(json.dumps(data, indent=2, ensure_ascii=False))
|
| 117 |
+
|
| 118 |
+
# Extract and log content
|
| 119 |
+
content = None
|
| 120 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 121 |
+
content = data["choices"][0]["message"]["content"]
|
| 122 |
+
logger.info("\nGenerated Content:")
|
| 123 |
+
logger.info(content)
|
| 124 |
+
elif "text" in data:
|
| 125 |
+
content = data["text"]
|
| 126 |
+
elif "output" in data:
|
| 127 |
+
content = data["output"]
|
| 128 |
+
else:
|
| 129 |
+
# If we get here, the response format is unexpected
|
| 130 |
+
error_msg = f"Unexpected API response format: {data}"
|
| 131 |
+
logger.error(error_msg)
|
| 132 |
+
raise ValueError(error_msg)
|
| 133 |
+
|
| 134 |
+
# Log token usage if available
|
| 135 |
+
# if "usage" in data:
|
| 136 |
+
# usage = data["usage"]
|
| 137 |
+
# logger.info("\nToken Usage:")
|
| 138 |
+
# logger.info(f"Prompt Tokens: {usage.get('prompt_tokens', 'N/A')}")
|
| 139 |
+
# logger.info(f"Completion Tokens: {usage.get('completion_tokens', 'N/A')}")
|
| 140 |
+
# logger.info(f"Total Tokens: {usage.get('total_tokens', 'N/A')}")
|
| 141 |
+
|
| 142 |
+
logger.info("=" * 50) # End of request/response log
|
| 143 |
+
|
| 144 |
+
if content is None:
|
| 145 |
+
raise ValueError("No content found in the response")
|
| 146 |
+
|
| 147 |
+
return content
|
| 148 |
+
|
| 149 |
+
except httpx.HTTPStatusError as e:
|
| 150 |
+
duration = time.time() - start_time
|
| 151 |
+
error_msg = f"API request failed with status {e.response.status_code} after {duration:.2f}s"
|
| 152 |
+
logger.error(error_msg)
|
| 153 |
+
try:
|
| 154 |
+
error_data = e.response.json()
|
| 155 |
+
logger.error("Error details: %s", json.dumps(error_data, indent=2))
|
| 156 |
+
except:
|
| 157 |
+
logger.error("Response text: %s", e.response.text)
|
| 158 |
+
raise
|
| 159 |
+
|
| 160 |
+
except httpx.RequestError as e:
|
| 161 |
+
duration = time.time() - start_time
|
| 162 |
+
error_msg = f"Failed to connect to the API after {duration:.2f}s: {str(e)}"
|
| 163 |
+
logger.error(error_msg, exc_info=True)
|
| 164 |
+
raise ConnectionError(error_msg) from e
|
| 165 |
+
|
| 166 |
+
except json.JSONDecodeError as e:
|
| 167 |
+
duration = time.time() - start_time
|
| 168 |
+
error_msg = f"Failed to parse API response after {duration:.2f}s: {str(e)}"
|
| 169 |
+
logger.error(error_msg)
|
| 170 |
+
logger.error("Response text: %s", getattr(resp, 'text', 'No response content'))
|
| 171 |
+
raise ValueError("Invalid JSON response from API") from e
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
duration = time.time() - start_time
|
| 175 |
+
error_msg = f"Unexpected error after {duration:.2f}s during API call: {str(e)}"
|
| 176 |
+
logger.error(error_msg, exc_info=True)
|
| 177 |
+
raise
|
| 178 |
+
|
| 179 |
+
# Singleton instance
|
| 180 |
+
llm = LLMClient()
|
app/services/search.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Search functionality for company data and competitor discovery."""
|
| 2 |
+
from typing import List, Optional, Dict, Any
|
| 3 |
+
from app.models.schemas import CompanyData
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
class SearchAdapter:
|
| 7 |
+
"""Provides company discovery and enrichment functions."""
|
| 8 |
+
|
| 9 |
+
def __init__(self,
|
| 10 |
+
news_api_key: Optional[str] = settings.NEWS_API_KEY,
|
| 11 |
+
crunchbase_key: Optional[str] = settings.CRUNCHBASE_API_KEY):
|
| 12 |
+
self.news_api_key = news_api_key
|
| 13 |
+
self.crunchbase_key = crunchbase_key
|
| 14 |
+
|
| 15 |
+
async def discover_competitors(self,
|
| 16 |
+
business_name: str,
|
| 17 |
+
business_desc: str,
|
| 18 |
+
geography: Optional[str] = None,
|
| 19 |
+
limit: int = 5) -> List[str]:
|
| 20 |
+
"""Discover potential competitors for a business."""
|
| 21 |
+
if self.crunchbase_key:
|
| 22 |
+
# Placeholder for actual Crunchbase API integration
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
# Fallback mock strategy
|
| 26 |
+
keywords = business_desc.lower()
|
| 27 |
+
if "hr" in keywords or "human resources" in keywords or "payroll" in keywords:
|
| 28 |
+
candidates = ["BambooHR", "Gusto", "Rippling", "Zoho People", "UKG"]
|
| 29 |
+
elif "saas" in keywords and "analytics" in keywords:
|
| 30 |
+
candidates = ["Mixpanel", "Amplitude", "Heap", "Pendo", "Looker"]
|
| 31 |
+
elif "ecommerce" in keywords or "shop" in keywords:
|
| 32 |
+
candidates = ["Shopify", "BigCommerce", "Magento", "Wix eCommerce", "WooCommerce"]
|
| 33 |
+
else:
|
| 34 |
+
candidates = [f"Competitor {chr(65 + i)}" for i in range(5)]
|
| 35 |
+
|
| 36 |
+
return candidates[:limit]
|
| 37 |
+
|
| 38 |
+
async def enrich_company(self,
|
| 39 |
+
company_name: str,
|
| 40 |
+
citation_depth: int = 3,
|
| 41 |
+
geography: Optional[str] = None) -> CompanyData:
|
| 42 |
+
"""Gather structured and unstructured info for a company."""
|
| 43 |
+
# Mock implementation - replace with actual API calls
|
| 44 |
+
return CompanyData(
|
| 45 |
+
name=company_name,
|
| 46 |
+
website=f"https://{company_name.lower().replace(' ', '')}.example.com",
|
| 47 |
+
description=f"A leading company in their industry, {company_name} provides excellent services.",
|
| 48 |
+
metrics={
|
| 49 |
+
"employees": 1000,
|
| 50 |
+
"revenue": "$10M - $50M",
|
| 51 |
+
"founded": 2010
|
| 52 |
+
},
|
| 53 |
+
notes=[
|
| 54 |
+
f"{company_name} recently expanded to new markets.",
|
| 55 |
+
"Strong social media presence with growing engagement."
|
| 56 |
+
],
|
| 57 |
+
sources=[
|
| 58 |
+
{"type": "web", "url": f"https://{company_name.lower().replace(' ', '')}.com/about"},
|
| 59 |
+
{"type": "news", "title": f"{company_name} announces new product line"}
|
| 60 |
+
][:citation_depth]
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Singleton instance
|
| 64 |
+
search_adapter = SearchAdapter()
|
app/utils/__pycache__/charts.cpython-311.pyc
ADDED
|
Binary file (4.9 kB). View file
|
|
|
app/utils/__pycache__/charts.cpython-313.pyc
ADDED
|
Binary file (4.43 kB). View file
|
|
|
app/utils/__pycache__/pdf_generator.cpython-311.pyc
ADDED
|
Binary file (3.25 kB). View file
|
|
|
app/utils/__pycache__/pdf_generator.cpython-313.pyc
ADDED
|
Binary file (2.76 kB). View file
|
|
|
app/utils/charts.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chart generation utilities."""
|
| 2 |
+
import io
|
| 3 |
+
from typing import List, Dict, Any, Optional
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import numpy as np
|
| 6 |
+
from app.models.schemas import CompanyData
|
| 7 |
+
|
| 8 |
+
def generate_bar_chart(
|
| 9 |
+
data: Dict[str, float],
|
| 10 |
+
title: str,
|
| 11 |
+
x_label: str,
|
| 12 |
+
y_label: str
|
| 13 |
+
) -> bytes:
|
| 14 |
+
"""Generate a bar chart and return as PNG bytes."""
|
| 15 |
+
plt.figure(figsize=(10, 6))
|
| 16 |
+
|
| 17 |
+
# Prepare data
|
| 18 |
+
labels = list(data.keys())
|
| 19 |
+
values = list(data.values())
|
| 20 |
+
|
| 21 |
+
# Create bar chart
|
| 22 |
+
bars = plt.bar(labels, values, color='skyblue')
|
| 23 |
+
|
| 24 |
+
# Add value labels on top of each bar
|
| 25 |
+
for bar in bars:
|
| 26 |
+
height = bar.get_height()
|
| 27 |
+
plt.text(bar.get_x() + bar.get_width()/2., height,
|
| 28 |
+
f'{height:,.0f}',
|
| 29 |
+
ha='center', va='bottom')
|
| 30 |
+
|
| 31 |
+
# Customize the chart
|
| 32 |
+
plt.title(title, fontsize=14, pad=20)
|
| 33 |
+
plt.xlabel(x_label, fontsize=12)
|
| 34 |
+
plt.ylabel(y_label, fontsize=12)
|
| 35 |
+
plt.xticks(rotation=45, ha='right')
|
| 36 |
+
plt.tight_layout()
|
| 37 |
+
|
| 38 |
+
# Save to bytes
|
| 39 |
+
buffer = io.BytesIO()
|
| 40 |
+
plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight')
|
| 41 |
+
plt.close()
|
| 42 |
+
|
| 43 |
+
buffer.seek(0)
|
| 44 |
+
return buffer.getvalue()
|
| 45 |
+
|
| 46 |
+
async def generate_company_metrics_charts(
|
| 47 |
+
companies: List[CompanyData],
|
| 48 |
+
metrics: List[str]
|
| 49 |
+
) -> List[bytes]:
|
| 50 |
+
"""Generate charts for company metrics."""
|
| 51 |
+
charts = []
|
| 52 |
+
|
| 53 |
+
# Example: Employee count comparison
|
| 54 |
+
employee_data = {}
|
| 55 |
+
revenue_data = {}
|
| 56 |
+
|
| 57 |
+
for company in companies:
|
| 58 |
+
if 'employees' in company.metrics:
|
| 59 |
+
try:
|
| 60 |
+
employee_data[company.name] = float(company.metrics['employees'])
|
| 61 |
+
except (ValueError, TypeError):
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
if 'revenue' in company.metrics and isinstance(company.metrics['revenue'], str):
|
| 65 |
+
# Simple revenue parsing (in a real app, use a proper currency parser)
|
| 66 |
+
rev_str = company.metrics['revenue'].replace('$', '').replace(',', '').replace(' ', '')
|
| 67 |
+
if '-' in rev_str:
|
| 68 |
+
rev_avg = sum(float(x) for x in rev_str.split('-')) / 2
|
| 69 |
+
revenue_data[company.name] = rev_avg
|
| 70 |
+
else:
|
| 71 |
+
try:
|
| 72 |
+
revenue_data[company.name] = float(rev_str)
|
| 73 |
+
except (ValueError, TypeError):
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
# Generate employee chart if we have data
|
| 77 |
+
if employee_data:
|
| 78 |
+
charts.append(
|
| 79 |
+
generate_bar_chart(
|
| 80 |
+
employee_data,
|
| 81 |
+
"Employee Count Comparison",
|
| 82 |
+
"Company",
|
| 83 |
+
"Number of Employees"
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Generate revenue chart if we have data
|
| 88 |
+
if revenue_data:
|
| 89 |
+
# Convert to millions for better readability
|
| 90 |
+
revenue_millions = {k: v / 1_000_000 for k, v in revenue_data.items()}
|
| 91 |
+
charts.append(
|
| 92 |
+
generate_bar_chart(
|
| 93 |
+
revenue_millions,
|
| 94 |
+
"Estimated Annual Revenue (Millions)",
|
| 95 |
+
"Company",
|
| 96 |
+
"Revenue ($M)"
|
| 97 |
+
)
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return charts
|
app/utils/pdf_generator.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PDF generation utilities for reports."""
|
| 2 |
+
import io
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
from reportlab.lib.pagesizes import letter
|
| 6 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table
|
| 7 |
+
from reportlab.lib.styles import getSampleStyleSheet
|
| 8 |
+
from reportlab.lib import colors
|
| 9 |
+
|
| 10 |
+
async def generate_pdf_report(
|
| 11 |
+
request_id: str,
|
| 12 |
+
report_data: dict,
|
| 13 |
+
charts: Optional[List[bytes]] = None
|
| 14 |
+
) -> bytes:
|
| 15 |
+
"""Generate a PDF report from the analysis results."""
|
| 16 |
+
buffer = io.BytesIO()
|
| 17 |
+
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
| 18 |
+
styles = getSampleStyleSheet()
|
| 19 |
+
elements = []
|
| 20 |
+
|
| 21 |
+
# Title
|
| 22 |
+
title = Paragraph("Competitor Analysis Report", styles['Title'])
|
| 23 |
+
elements.append(title)
|
| 24 |
+
|
| 25 |
+
# Report metadata
|
| 26 |
+
elements.append(Paragraph(f"Report ID: {request_id}", styles['Normal']))
|
| 27 |
+
elements.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 28 |
+
styles['Normal']))
|
| 29 |
+
elements.append(Spacer(1, 20))
|
| 30 |
+
|
| 31 |
+
# Executive Summary
|
| 32 |
+
elements.append(Paragraph("Executive Summary", styles['Heading1']))
|
| 33 |
+
elements.append(Paragraph(report_data.get('executive_summary', ''), styles['Normal']))
|
| 34 |
+
|
| 35 |
+
# Add charts if available
|
| 36 |
+
if charts:
|
| 37 |
+
elements.append(Spacer(1, 20))
|
| 38 |
+
elements.append(Paragraph("Key Metrics", styles['Heading2']))
|
| 39 |
+
for chart in charts:
|
| 40 |
+
try:
|
| 41 |
+
img = Image(io.BytesIO(chart), width=400, height=300)
|
| 42 |
+
elements.append(img)
|
| 43 |
+
elements.append(Spacer(1, 10))
|
| 44 |
+
except:
|
| 45 |
+
continue
|
| 46 |
+
|
| 47 |
+
# Build PDF
|
| 48 |
+
doc.build(elements)
|
| 49 |
+
buffer.seek(0)
|
| 50 |
+
return buffer.getvalue()
|
logging_config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging configuration for RivalLens."""
|
| 2 |
+
import logging
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# Create logs directory if it doesn't exist
|
| 7 |
+
log_dir = Path("logs")
|
| 8 |
+
log_dir.mkdir(exist_ok=True)
|
| 9 |
+
|
| 10 |
+
# Logging configuration
|
| 11 |
+
LOGGING_CONFIG = {
|
| 12 |
+
"version": 1,
|
| 13 |
+
"disable_existing_loggers": False,
|
| 14 |
+
"formatters": {
|
| 15 |
+
"standard": {
|
| 16 |
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 17 |
+
"datefmt": "%Y-%m-%d %H:%M:%S"
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
"handlers": {
|
| 21 |
+
"console": {
|
| 22 |
+
"level": "INFO",
|
| 23 |
+
"formatter": "standard",
|
| 24 |
+
"class": "logging.StreamHandler",
|
| 25 |
+
"stream": sys.stdout,
|
| 26 |
+
},
|
| 27 |
+
"file": {
|
| 28 |
+
"level": "DEBUG",
|
| 29 |
+
"formatter": "standard",
|
| 30 |
+
"class": "logging.handlers.RotatingFileHandler",
|
| 31 |
+
"filename": log_dir / "rival_lens.log",
|
| 32 |
+
"maxBytes": 10485760, # 10MB
|
| 33 |
+
"backupCount": 5,
|
| 34 |
+
"encoding": "utf8"
|
| 35 |
+
},
|
| 36 |
+
},
|
| 37 |
+
"loggers": {
|
| 38 |
+
"": { # root logger
|
| 39 |
+
"handlers": ["console", "file"],
|
| 40 |
+
"level": "DEBUG",
|
| 41 |
+
"propagate": True
|
| 42 |
+
},
|
| 43 |
+
"app": {
|
| 44 |
+
"handlers": ["console", "file"],
|
| 45 |
+
"level": "DEBUG",
|
| 46 |
+
"propagate": False
|
| 47 |
+
},
|
| 48 |
+
"__main__": {
|
| 49 |
+
"handlers": ["console", "file"],
|
| 50 |
+
"level": "DEBUG",
|
| 51 |
+
"propagate": False
|
| 52 |
+
},
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
def configure_logging():
|
| 57 |
+
"""Configure logging for the application."""
|
| 58 |
+
import logging.config
|
| 59 |
+
logging.config.dictConfig(LOGGING_CONFIG)
|
| 60 |
+
logger = logging.getLogger(__name__)
|
| 61 |
+
logger.info("Logging configured")
|
| 62 |
+
return logger
|
logs/rival_lens.log
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|