devyugensys commited on
Commit
fe108cd
·
verified ·
1 Parent(s): 8188cb4

Upload 29 files

Browse files
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