Spaces:
Runtime error
Phase 2: Multi-agent implementation complete (#1)
Browse filesImplemented complete multi-agent system with three specialized agents:
Research Agent:
- Tavily search integration for web research
- Specialized methods for company info, competitors, market trends
- Gathers and formats data from 20+ sources
Analysis Agent:
- SWOT analysis generation
- Competitive matrix creation
- Market positioning analysis
- Strategic recommendations with priority levels
Writer Agent:
- Executive summary generation (200-300 words)
- Full markdown report creation
- Professional formatting with citations
- Metadata tracking
Core Infrastructure:
- BaseAgent class with OpenRouter LLM integration
- Cost tracking for all agent operations
- Tavily search tool wrapper
- Structured prompts for each agent role
Testing:
- 18/18 unit tests passing
- Complete pipeline test successful on real query
- Generated professional report with 28 sources
- Free tier model working (Grok) - /bin/zsh cost
Files Added:
- src/agents/base.py - Base agent class
- src/agents/researcher.py - Research agent
- src/agents/analyst.py - Analysis agent
- src/agents/writer.py - Writer agent
- src/tools/search.py - Tavily search wrapper
- tests/unit/test_base_agent.py - Agent tests
- test_agents.py - Complete pipeline test
Ready for Phase 3: LangGraph workflow orchestration
- src/agents/analyst.py +313 -0
- src/agents/base.py +156 -0
- src/agents/researcher.py +250 -0
- src/agents/writer.py +251 -0
- src/tools/search.py +217 -0
- test_agents.py +133 -0
- test_report.md +205 -0
- tests/unit/test_base_agent.py +99 -0
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Analysis Agent for competitive intelligence and SWOT analysis."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from src.agents.base import BaseAgent
|
| 6 |
+
from src.utils.cost_tracker import CostTracker
|
| 7 |
+
from src.utils.logging import setup_logger
|
| 8 |
+
|
| 9 |
+
logger = setup_logger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AnalysisAgent(BaseAgent):
|
| 13 |
+
"""
|
| 14 |
+
Analysis Agent responsible for strategic business analysis.
|
| 15 |
+
|
| 16 |
+
Takes research data and produces:
|
| 17 |
+
- SWOT analysis
|
| 18 |
+
- Competitive matrix
|
| 19 |
+
- Market positioning analysis
|
| 20 |
+
- Strategic recommendations
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
model: Optional[str] = None,
|
| 26 |
+
temperature: float = 0.4, # Balanced for analytical reasoning
|
| 27 |
+
cost_tracker: Optional[CostTracker] = None,
|
| 28 |
+
):
|
| 29 |
+
"""
|
| 30 |
+
Initialize Analysis Agent.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
model: LLM model to use
|
| 34 |
+
temperature: Sampling temperature
|
| 35 |
+
cost_tracker: Cost tracker instance
|
| 36 |
+
"""
|
| 37 |
+
super().__init__(
|
| 38 |
+
name="AnalysisAgent",
|
| 39 |
+
model=model,
|
| 40 |
+
temperature=temperature,
|
| 41 |
+
cost_tracker=cost_tracker,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
def get_system_prompt(self) -> str:
|
| 45 |
+
"""Get system prompt for analysis agent."""
|
| 46 |
+
return """You are a strategic business analyst specializing in competitive intelligence and market analysis.
|
| 47 |
+
|
| 48 |
+
Your role is to analyze research data and provide actionable strategic insights.
|
| 49 |
+
|
| 50 |
+
When performing analysis, you should:
|
| 51 |
+
1. Use structured frameworks (SWOT, competitive matrices, positioning maps)
|
| 52 |
+
2. Identify clear patterns and trends
|
| 53 |
+
3. Provide specific, actionable recommendations
|
| 54 |
+
4. Support conclusions with evidence from the research
|
| 55 |
+
5. Consider multiple perspectives (competitors, customers, market forces)
|
| 56 |
+
|
| 57 |
+
Your analysis should be:
|
| 58 |
+
- Objective and data-driven
|
| 59 |
+
- Structured and easy to scan
|
| 60 |
+
- Focused on business impact
|
| 61 |
+
- Actionable for decision-makers
|
| 62 |
+
|
| 63 |
+
Use bullet points, clear headings, and strategic language."""
|
| 64 |
+
|
| 65 |
+
async def run(
|
| 66 |
+
self,
|
| 67 |
+
research_data: Dict[str, Any],
|
| 68 |
+
) -> Dict[str, Any]:
|
| 69 |
+
"""
|
| 70 |
+
Perform comprehensive analysis on research data.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
research_data: Output from ResearchAgent containing:
|
| 74 |
+
- company_overview
|
| 75 |
+
- competitors
|
| 76 |
+
- market_trends
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Dictionary with analysis results:
|
| 80 |
+
- swot: SWOT analysis
|
| 81 |
+
- competitive_matrix: Competitor comparison
|
| 82 |
+
- positioning: Market positioning analysis
|
| 83 |
+
- strategic_recommendations: Action items
|
| 84 |
+
"""
|
| 85 |
+
company_name = research_data.get("company_name", "Unknown Company")
|
| 86 |
+
logger.info(f"Starting analysis for: {company_name}")
|
| 87 |
+
|
| 88 |
+
results = {
|
| 89 |
+
"company_name": company_name,
|
| 90 |
+
"swot": "",
|
| 91 |
+
"competitive_matrix": "",
|
| 92 |
+
"positioning": "",
|
| 93 |
+
"strategic_recommendations": "",
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
# 1. SWOT Analysis
|
| 98 |
+
swot = await self._perform_swot_analysis(research_data)
|
| 99 |
+
results["swot"] = swot
|
| 100 |
+
|
| 101 |
+
# 2. Competitive Matrix
|
| 102 |
+
matrix = await self._create_competitive_matrix(research_data)
|
| 103 |
+
results["competitive_matrix"] = matrix
|
| 104 |
+
|
| 105 |
+
# 3. Market Positioning
|
| 106 |
+
positioning = await self._analyze_market_positioning(research_data)
|
| 107 |
+
results["positioning"] = positioning
|
| 108 |
+
|
| 109 |
+
# 4. Strategic Recommendations
|
| 110 |
+
recommendations = await self._generate_recommendations(research_data, swot)
|
| 111 |
+
results["strategic_recommendations"] = recommendations
|
| 112 |
+
|
| 113 |
+
logger.info(f"Analysis complete for {company_name}")
|
| 114 |
+
|
| 115 |
+
return results
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"Analysis failed for {company_name}: {e}")
|
| 119 |
+
raise
|
| 120 |
+
|
| 121 |
+
async def _perform_swot_analysis(
|
| 122 |
+
self,
|
| 123 |
+
research_data: Dict[str, Any],
|
| 124 |
+
) -> str:
|
| 125 |
+
"""
|
| 126 |
+
Generate SWOT analysis from research data.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
research_data: Research results
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
SWOT analysis text
|
| 133 |
+
"""
|
| 134 |
+
company_name = research_data.get("company_name")
|
| 135 |
+
company_overview = research_data.get("company_overview", "")
|
| 136 |
+
competitors = research_data.get("competitors", "")
|
| 137 |
+
market_trends = research_data.get("market_trends", "")
|
| 138 |
+
|
| 139 |
+
user_message = f"""Based on the research data, perform a comprehensive SWOT analysis for {company_name}.
|
| 140 |
+
|
| 141 |
+
Research Data:
|
| 142 |
+
|
| 143 |
+
COMPANY OVERVIEW:
|
| 144 |
+
{company_overview}
|
| 145 |
+
|
| 146 |
+
COMPETITORS:
|
| 147 |
+
{competitors}
|
| 148 |
+
|
| 149 |
+
MARKET TRENDS:
|
| 150 |
+
{market_trends}
|
| 151 |
+
|
| 152 |
+
Provide a detailed SWOT analysis with:
|
| 153 |
+
|
| 154 |
+
STRENGTHS (internal positive factors):
|
| 155 |
+
- List 4-6 key strengths
|
| 156 |
+
- Focus on competitive advantages, resources, capabilities
|
| 157 |
+
|
| 158 |
+
WEAKNESSES (internal negative factors):
|
| 159 |
+
- List 4-6 key weaknesses
|
| 160 |
+
- Include operational limits, resource constraints, vulnerabilities
|
| 161 |
+
|
| 162 |
+
OPPORTUNITIES (external positive factors):
|
| 163 |
+
- List 4-6 market opportunities
|
| 164 |
+
- Consider market trends, gaps, emerging needs
|
| 165 |
+
|
| 166 |
+
THREATS (external negative factors):
|
| 167 |
+
- List 4-6 threats
|
| 168 |
+
- Include competitive threats, market risks, industry challenges
|
| 169 |
+
|
| 170 |
+
Use bullet points and be specific with evidence."""
|
| 171 |
+
|
| 172 |
+
messages = self._create_messages(user_message)
|
| 173 |
+
response = await self._invoke_llm(messages)
|
| 174 |
+
|
| 175 |
+
return response
|
| 176 |
+
|
| 177 |
+
async def _create_competitive_matrix(
|
| 178 |
+
self,
|
| 179 |
+
research_data: Dict[str, Any],
|
| 180 |
+
) -> str:
|
| 181 |
+
"""
|
| 182 |
+
Create competitive comparison matrix.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
research_data: Research results
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Competitive matrix as formatted text
|
| 189 |
+
"""
|
| 190 |
+
company_name = research_data.get("company_name")
|
| 191 |
+
competitors_info = research_data.get("competitors", "")
|
| 192 |
+
|
| 193 |
+
user_message = f"""Based on the competitor research, create a competitive matrix comparing {company_name} with its main competitors.
|
| 194 |
+
|
| 195 |
+
Competitor Research:
|
| 196 |
+
{competitors_info}
|
| 197 |
+
|
| 198 |
+
Create a comparison matrix with these dimensions:
|
| 199 |
+
1. Market Share/Size
|
| 200 |
+
2. Product Range
|
| 201 |
+
3. Pricing Strategy
|
| 202 |
+
4. Technology/Innovation
|
| 203 |
+
5. Customer Segments
|
| 204 |
+
6. Strengths
|
| 205 |
+
7. Weaknesses
|
| 206 |
+
|
| 207 |
+
Format as a clear table or structured comparison.
|
| 208 |
+
Include 3-5 main competitors plus {company_name}.
|
| 209 |
+
Use "High/Medium/Low" or specific data points where available."""
|
| 210 |
+
|
| 211 |
+
messages = self._create_messages(user_message)
|
| 212 |
+
response = await self._invoke_llm(messages)
|
| 213 |
+
|
| 214 |
+
return response
|
| 215 |
+
|
| 216 |
+
async def _analyze_market_positioning(
|
| 217 |
+
self,
|
| 218 |
+
research_data: Dict[str, Any],
|
| 219 |
+
) -> str:
|
| 220 |
+
"""
|
| 221 |
+
Analyze market positioning strategy.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
research_data: Research results
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
Positioning analysis
|
| 228 |
+
"""
|
| 229 |
+
company_name = research_data.get("company_name")
|
| 230 |
+
company_overview = research_data.get("company_overview", "")
|
| 231 |
+
competitors = research_data.get("competitors", "")
|
| 232 |
+
|
| 233 |
+
user_message = f"""Analyze the market positioning of {company_name}.
|
| 234 |
+
|
| 235 |
+
Company Overview:
|
| 236 |
+
{company_overview}
|
| 237 |
+
|
| 238 |
+
Competitive Landscape:
|
| 239 |
+
{competitors}
|
| 240 |
+
|
| 241 |
+
Provide analysis covering:
|
| 242 |
+
|
| 243 |
+
1. CURRENT POSITIONING
|
| 244 |
+
- How is {company_name} currently positioned in the market?
|
| 245 |
+
- What is their value proposition?
|
| 246 |
+
- What customer segments do they target?
|
| 247 |
+
|
| 248 |
+
2. COMPETITIVE DIFFERENTIATION
|
| 249 |
+
- What makes {company_name} different from competitors?
|
| 250 |
+
- What is their unique selling proposition (USP)?
|
| 251 |
+
- Where do they fit in the competitive landscape?
|
| 252 |
+
|
| 253 |
+
3. POSITIONING GAPS
|
| 254 |
+
- Are there market segments they're missing?
|
| 255 |
+
- Are there positioning opportunities?
|
| 256 |
+
- How could they strengthen their position?
|
| 257 |
+
|
| 258 |
+
Be specific and strategic."""
|
| 259 |
+
|
| 260 |
+
messages = self._create_messages(user_message)
|
| 261 |
+
response = await self._invoke_llm(messages)
|
| 262 |
+
|
| 263 |
+
return response
|
| 264 |
+
|
| 265 |
+
async def _generate_recommendations(
|
| 266 |
+
self,
|
| 267 |
+
research_data: Dict[str, Any],
|
| 268 |
+
swot: str,
|
| 269 |
+
) -> str:
|
| 270 |
+
"""
|
| 271 |
+
Generate strategic recommendations.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
research_data: Research results
|
| 275 |
+
swot: SWOT analysis
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
Strategic recommendations
|
| 279 |
+
"""
|
| 280 |
+
company_name = research_data.get("company_name")
|
| 281 |
+
market_trends = research_data.get("market_trends", "")
|
| 282 |
+
|
| 283 |
+
user_message = f"""Based on the SWOT analysis and market trends, provide strategic recommendations for {company_name}.
|
| 284 |
+
|
| 285 |
+
SWOT ANALYSIS:
|
| 286 |
+
{swot}
|
| 287 |
+
|
| 288 |
+
MARKET TRENDS:
|
| 289 |
+
{market_trends}
|
| 290 |
+
|
| 291 |
+
Provide 5-7 actionable strategic recommendations organized by priority:
|
| 292 |
+
|
| 293 |
+
HIGH PRIORITY (immediate action needed):
|
| 294 |
+
- Recommendation 1 (with rationale)
|
| 295 |
+
- Recommendation 2 (with rationale)
|
| 296 |
+
|
| 297 |
+
MEDIUM PRIORITY (next 6-12 months):
|
| 298 |
+
- Recommendation 3 (with rationale)
|
| 299 |
+
- Recommendation 4 (with rationale)
|
| 300 |
+
|
| 301 |
+
LONG-TERM (strategic initiatives):
|
| 302 |
+
- Recommendation 5 (with rationale)
|
| 303 |
+
|
| 304 |
+
Each recommendation should:
|
| 305 |
+
- Be specific and actionable
|
| 306 |
+
- Address a key opportunity or threat
|
| 307 |
+
- Leverage strengths or address weaknesses
|
| 308 |
+
- Include expected impact"""
|
| 309 |
+
|
| 310 |
+
messages = self._create_messages(user_message)
|
| 311 |
+
response = await self._invoke_llm(messages)
|
| 312 |
+
|
| 313 |
+
return response
|
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base agent class for all agents in the system."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import Any, Dict, Optional
|
| 5 |
+
|
| 6 |
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
|
| 9 |
+
from src.utils.config import get_settings
|
| 10 |
+
from src.utils.cost_tracker import CostTracker
|
| 11 |
+
from src.utils.logging import setup_logger
|
| 12 |
+
|
| 13 |
+
logger = setup_logger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class BaseAgent(ABC):
|
| 17 |
+
"""
|
| 18 |
+
Base class for all agents in the multi-agent system.
|
| 19 |
+
|
| 20 |
+
Provides common functionality for LLM interaction, cost tracking,
|
| 21 |
+
and error handling.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
name: str,
|
| 27 |
+
model: Optional[str] = None,
|
| 28 |
+
temperature: float = 0.7,
|
| 29 |
+
cost_tracker: Optional[CostTracker] = None,
|
| 30 |
+
):
|
| 31 |
+
"""
|
| 32 |
+
Initialize base agent.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
name: Agent name for logging
|
| 36 |
+
model: LLM model to use (defaults to config default)
|
| 37 |
+
temperature: LLM sampling temperature (0-1)
|
| 38 |
+
cost_tracker: Optional cost tracker instance
|
| 39 |
+
"""
|
| 40 |
+
self.name = name
|
| 41 |
+
self.cost_tracker = cost_tracker or CostTracker()
|
| 42 |
+
|
| 43 |
+
settings = get_settings()
|
| 44 |
+
self.model_name = model or settings.default_model
|
| 45 |
+
|
| 46 |
+
# Initialize LLM via OpenRouter
|
| 47 |
+
self.llm = ChatOpenAI(
|
| 48 |
+
model=self.model_name,
|
| 49 |
+
temperature=temperature,
|
| 50 |
+
openai_api_key=settings.openrouter_api_key,
|
| 51 |
+
openai_api_base=settings.openrouter_base_url,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
logger.info(f"Initialized {name} with model {self.model_name}")
|
| 55 |
+
|
| 56 |
+
@abstractmethod
|
| 57 |
+
def get_system_prompt(self) -> str:
|
| 58 |
+
"""
|
| 59 |
+
Get the system prompt for this agent.
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
System prompt string
|
| 63 |
+
"""
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
@abstractmethod
|
| 67 |
+
async def run(self, **kwargs) -> Dict[str, Any]:
|
| 68 |
+
"""
|
| 69 |
+
Execute the agent's main task.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
**kwargs: Agent-specific parameters
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Dictionary with results
|
| 76 |
+
"""
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
async def _invoke_llm(
|
| 80 |
+
self,
|
| 81 |
+
messages: list[BaseMessage],
|
| 82 |
+
**llm_kwargs,
|
| 83 |
+
) -> str:
|
| 84 |
+
"""
|
| 85 |
+
Invoke LLM and track costs.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
messages: List of messages to send
|
| 89 |
+
**llm_kwargs: Additional LLM parameters
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
LLM response text
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
response = await self.llm.ainvoke(messages, **llm_kwargs)
|
| 96 |
+
|
| 97 |
+
# Track usage if available
|
| 98 |
+
if hasattr(response, "response_metadata"):
|
| 99 |
+
usage = response.response_metadata.get("usage", {})
|
| 100 |
+
if usage:
|
| 101 |
+
self.cost_tracker.track_usage(
|
| 102 |
+
model=self.model_name,
|
| 103 |
+
input_tokens=usage.get("prompt_tokens", 0),
|
| 104 |
+
output_tokens=usage.get("completion_tokens", 0),
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
logger.info(
|
| 108 |
+
f"{self.name} LLM call complete",
|
| 109 |
+
extra={
|
| 110 |
+
"extra_fields": {
|
| 111 |
+
"model": self.model_name,
|
| 112 |
+
"total_cost": self.cost_tracker.total_cost,
|
| 113 |
+
}
|
| 114 |
+
},
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
return response.content
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.error(f"{self.name} LLM call failed: {e}")
|
| 121 |
+
raise
|
| 122 |
+
|
| 123 |
+
def _create_messages(
|
| 124 |
+
self,
|
| 125 |
+
user_message: str,
|
| 126 |
+
system_prompt: Optional[str] = None,
|
| 127 |
+
) -> list[BaseMessage]:
|
| 128 |
+
"""
|
| 129 |
+
Create message list for LLM.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
user_message: User message content
|
| 133 |
+
system_prompt: Optional system prompt (uses default if None)
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
List of messages
|
| 137 |
+
"""
|
| 138 |
+
messages = []
|
| 139 |
+
|
| 140 |
+
# Add system message
|
| 141 |
+
prompt = system_prompt or self.get_system_prompt()
|
| 142 |
+
messages.append(SystemMessage(content=prompt))
|
| 143 |
+
|
| 144 |
+
# Add user message
|
| 145 |
+
messages.append(HumanMessage(content=user_message))
|
| 146 |
+
|
| 147 |
+
return messages
|
| 148 |
+
|
| 149 |
+
def get_cost_summary(self) -> Dict[str, Any]:
|
| 150 |
+
"""
|
| 151 |
+
Get cost summary for this agent's operations.
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
Cost summary dictionary
|
| 155 |
+
"""
|
| 156 |
+
return self.cost_tracker.get_summary()
|
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Research Agent for gathering market intelligence data."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, Optional
|
| 4 |
+
|
| 5 |
+
from src.agents.base import BaseAgent
|
| 6 |
+
from src.tools.search import TavilySearchTool
|
| 7 |
+
from src.utils.cost_tracker import CostTracker
|
| 8 |
+
from src.utils.logging import setup_logger
|
| 9 |
+
|
| 10 |
+
logger = setup_logger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ResearchAgent(BaseAgent):
|
| 14 |
+
"""
|
| 15 |
+
Research Agent responsible for gathering data from web sources.
|
| 16 |
+
|
| 17 |
+
Uses Tavily API for web search and can gather:
|
| 18 |
+
- Company information
|
| 19 |
+
- Competitor analysis data
|
| 20 |
+
- Market trends and insights
|
| 21 |
+
- Industry news
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
model: Optional[str] = None,
|
| 27 |
+
temperature: float = 0.3, # Lower for more factual responses
|
| 28 |
+
cost_tracker: Optional[CostTracker] = None,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Initialize Research Agent.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
model: LLM model to use
|
| 35 |
+
temperature: Sampling temperature (lower for research)
|
| 36 |
+
cost_tracker: Cost tracker instance
|
| 37 |
+
"""
|
| 38 |
+
super().__init__(
|
| 39 |
+
name="ResearchAgent",
|
| 40 |
+
model=model,
|
| 41 |
+
temperature=temperature,
|
| 42 |
+
cost_tracker=cost_tracker,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.search_tool = TavilySearchTool()
|
| 46 |
+
|
| 47 |
+
def get_system_prompt(self) -> str:
|
| 48 |
+
"""Get system prompt for research agent."""
|
| 49 |
+
return """You are a professional business research analyst specializing in competitive intelligence.
|
| 50 |
+
|
| 51 |
+
Your role is to gather and synthesize information about companies, markets, and competitors from web sources.
|
| 52 |
+
|
| 53 |
+
When analyzing search results, you should:
|
| 54 |
+
1. Focus on factual, verifiable information
|
| 55 |
+
2. Identify key data points: revenue, employees, products, market position
|
| 56 |
+
3. Note dates and sources for important claims
|
| 57 |
+
4. Distinguish between facts and opinions
|
| 58 |
+
5. Highlight competitive advantages and weaknesses
|
| 59 |
+
|
| 60 |
+
Provide your analysis in a structured format with clear sections and bullet points.
|
| 61 |
+
Always cite sources when making specific claims."""
|
| 62 |
+
|
| 63 |
+
async def run(
|
| 64 |
+
self,
|
| 65 |
+
company_name: str,
|
| 66 |
+
industry: Optional[str] = None,
|
| 67 |
+
research_depth: str = "comprehensive",
|
| 68 |
+
) -> Dict[str, Any]:
|
| 69 |
+
"""
|
| 70 |
+
Gather research data about a company.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
company_name: Target company name
|
| 74 |
+
industry: Optional industry context
|
| 75 |
+
research_depth: "basic" or "comprehensive"
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Dictionary with research results:
|
| 79 |
+
- company_overview: Company information
|
| 80 |
+
- competitors: Competitor analysis
|
| 81 |
+
- market_trends: Industry trends
|
| 82 |
+
- raw_sources: List of sources used
|
| 83 |
+
"""
|
| 84 |
+
logger.info(f"Starting research for: {company_name}")
|
| 85 |
+
|
| 86 |
+
results = {
|
| 87 |
+
"company_name": company_name,
|
| 88 |
+
"industry": industry,
|
| 89 |
+
"company_overview": "",
|
| 90 |
+
"competitors": "",
|
| 91 |
+
"market_trends": "",
|
| 92 |
+
"raw_sources": [],
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
# 1. Company Overview
|
| 97 |
+
company_data = await self.search_tool.get_company_info(
|
| 98 |
+
company_name=company_name,
|
| 99 |
+
max_results=10 if research_depth == "comprehensive" else 5,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
results["raw_sources"].extend(company_data.get("results", []))
|
| 103 |
+
|
| 104 |
+
# Analyze company data with LLM
|
| 105 |
+
company_context = self.search_tool.format_results_for_llm(company_data)
|
| 106 |
+
company_analysis = await self._analyze_company(
|
| 107 |
+
company_name, company_context
|
| 108 |
+
)
|
| 109 |
+
results["company_overview"] = company_analysis
|
| 110 |
+
|
| 111 |
+
# 2. Competitor Analysis
|
| 112 |
+
competitor_data = await self.search_tool.get_competitor_info(
|
| 113 |
+
company_name=company_name,
|
| 114 |
+
industry=industry,
|
| 115 |
+
max_results=10 if research_depth == "comprehensive" else 5,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
results["raw_sources"].extend(competitor_data.get("results", []))
|
| 119 |
+
|
| 120 |
+
competitor_context = self.search_tool.format_results_for_llm(
|
| 121 |
+
competitor_data
|
| 122 |
+
)
|
| 123 |
+
competitor_analysis = await self._analyze_competitors(
|
| 124 |
+
company_name, competitor_context
|
| 125 |
+
)
|
| 126 |
+
results["competitors"] = competitor_analysis
|
| 127 |
+
|
| 128 |
+
# 3. Market Trends (if industry provided)
|
| 129 |
+
if industry:
|
| 130 |
+
trend_data = await self.search_tool.get_market_trends(
|
| 131 |
+
industry=industry,
|
| 132 |
+
max_results=8 if research_depth == "comprehensive" else 4,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
results["raw_sources"].extend(trend_data.get("results", []))
|
| 136 |
+
|
| 137 |
+
trend_context = self.search_tool.format_results_for_llm(trend_data)
|
| 138 |
+
trend_analysis = await self._analyze_trends(industry, trend_context)
|
| 139 |
+
results["market_trends"] = trend_analysis
|
| 140 |
+
|
| 141 |
+
logger.info(
|
| 142 |
+
f"Research complete for {company_name}. "
|
| 143 |
+
f"Processed {len(results['raw_sources'])} sources"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
return results
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"Research failed for {company_name}: {e}")
|
| 150 |
+
raise
|
| 151 |
+
|
| 152 |
+
async def _analyze_company(
|
| 153 |
+
self,
|
| 154 |
+
company_name: str,
|
| 155 |
+
search_context: str,
|
| 156 |
+
) -> str:
|
| 157 |
+
"""
|
| 158 |
+
Analyze company information from search results.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
company_name: Company name
|
| 162 |
+
search_context: Formatted search results
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
Structured company analysis
|
| 166 |
+
"""
|
| 167 |
+
user_message = f"""Analyze the following search results about {company_name}.
|
| 168 |
+
|
| 169 |
+
Provide a structured analysis covering:
|
| 170 |
+
1. Company Overview (founded, headquarters, size)
|
| 171 |
+
2. Products & Services (main offerings)
|
| 172 |
+
3. Business Model (how they make money)
|
| 173 |
+
4. Market Position (market share, ranking)
|
| 174 |
+
5. Key Metrics (revenue, employees, growth)
|
| 175 |
+
|
| 176 |
+
Search Results:
|
| 177 |
+
{search_context}
|
| 178 |
+
|
| 179 |
+
Provide your analysis in clear sections with bullet points. Cite sources for specific claims."""
|
| 180 |
+
|
| 181 |
+
messages = self._create_messages(user_message)
|
| 182 |
+
response = await self._invoke_llm(messages)
|
| 183 |
+
|
| 184 |
+
return response
|
| 185 |
+
|
| 186 |
+
async def _analyze_competitors(
|
| 187 |
+
self,
|
| 188 |
+
company_name: str,
|
| 189 |
+
search_context: str,
|
| 190 |
+
) -> str:
|
| 191 |
+
"""
|
| 192 |
+
Analyze competitor landscape.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
company_name: Target company
|
| 196 |
+
search_context: Formatted search results
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
Competitor analysis
|
| 200 |
+
"""
|
| 201 |
+
user_message = f"""Analyze the competitive landscape for {company_name}.
|
| 202 |
+
|
| 203 |
+
Based on the search results, identify:
|
| 204 |
+
1. Main Competitors (list 3-5 key competitors)
|
| 205 |
+
2. Competitive Positioning (how each differs)
|
| 206 |
+
3. Market Dynamics (who leads, who follows)
|
| 207 |
+
4. Differentiation Factors (what makes each unique)
|
| 208 |
+
|
| 209 |
+
Search Results:
|
| 210 |
+
{search_context}
|
| 211 |
+
|
| 212 |
+
Format as a structured list with clear comparisons."""
|
| 213 |
+
|
| 214 |
+
messages = self._create_messages(user_message)
|
| 215 |
+
response = await self._invoke_llm(messages)
|
| 216 |
+
|
| 217 |
+
return response
|
| 218 |
+
|
| 219 |
+
async def _analyze_trends(
|
| 220 |
+
self,
|
| 221 |
+
industry: str,
|
| 222 |
+
search_context: str,
|
| 223 |
+
) -> str:
|
| 224 |
+
"""
|
| 225 |
+
Analyze market trends.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
industry: Industry name
|
| 229 |
+
search_context: Formatted search results
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Trend analysis
|
| 233 |
+
"""
|
| 234 |
+
user_message = f"""Analyze market trends for the {industry} industry.
|
| 235 |
+
|
| 236 |
+
Identify:
|
| 237 |
+
1. Key Trends (major shifts in the market)
|
| 238 |
+
2. Growth Drivers (what's fueling growth)
|
| 239 |
+
3. Challenges (obstacles facing the industry)
|
| 240 |
+
4. Future Outlook (predictions for next 1-2 years)
|
| 241 |
+
|
| 242 |
+
Search Results:
|
| 243 |
+
{search_context}
|
| 244 |
+
|
| 245 |
+
Provide analysis with clear trends and supporting evidence."""
|
| 246 |
+
|
| 247 |
+
messages = self._create_messages(user_message)
|
| 248 |
+
response = await self._invoke_llm(messages)
|
| 249 |
+
|
| 250 |
+
return response
|
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Writer Agent for generating professional market intelligence reports."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Any, Dict, Optional
|
| 5 |
+
|
| 6 |
+
from src.agents.base import BaseAgent
|
| 7 |
+
from src.utils.cost_tracker import CostTracker
|
| 8 |
+
from src.utils.logging import setup_logger
|
| 9 |
+
|
| 10 |
+
logger = setup_logger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class WriterAgent(BaseAgent):
|
| 14 |
+
"""
|
| 15 |
+
Writer Agent responsible for generating final reports.
|
| 16 |
+
|
| 17 |
+
Takes research and analysis data and creates:
|
| 18 |
+
- Executive summary
|
| 19 |
+
- Comprehensive market intelligence report
|
| 20 |
+
- Properly formatted markdown with citations
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
model: Optional[str] = None,
|
| 26 |
+
temperature: float = 0.6, # Higher for better writing quality
|
| 27 |
+
cost_tracker: Optional[CostTracker] = None,
|
| 28 |
+
):
|
| 29 |
+
"""
|
| 30 |
+
Initialize Writer Agent.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
model: LLM model to use
|
| 34 |
+
temperature: Sampling temperature
|
| 35 |
+
cost_tracker: Cost tracker instance
|
| 36 |
+
"""
|
| 37 |
+
super().__init__(
|
| 38 |
+
name="WriterAgent",
|
| 39 |
+
model=model,
|
| 40 |
+
temperature=temperature,
|
| 41 |
+
cost_tracker=cost_tracker,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
def get_system_prompt(self) -> str:
|
| 45 |
+
"""Get system prompt for writer agent."""
|
| 46 |
+
return """You are a professional business report writer specializing in market intelligence and competitive analysis.
|
| 47 |
+
|
| 48 |
+
Your role is to transform research and analysis into polished, executive-ready reports.
|
| 49 |
+
|
| 50 |
+
When writing reports, you should:
|
| 51 |
+
1. Use clear, professional business language
|
| 52 |
+
2. Structure content logically with proper headings
|
| 53 |
+
3. Include executive summaries for busy stakeholders
|
| 54 |
+
4. Use bullet points and tables for scannability
|
| 55 |
+
5. Cite sources properly
|
| 56 |
+
6. Make insights actionable
|
| 57 |
+
|
| 58 |
+
Report format guidelines:
|
| 59 |
+
- Use markdown formatting
|
| 60 |
+
- Include clear section headers (#, ##, ###)
|
| 61 |
+
- Use tables for competitive comparisons
|
| 62 |
+
- Include bullet points for lists
|
| 63 |
+
- Add citations [source]
|
| 64 |
+
- Keep executive summary to 200-300 words
|
| 65 |
+
|
| 66 |
+
Write for senior executives and decision-makers."""
|
| 67 |
+
|
| 68 |
+
async def run(
|
| 69 |
+
self,
|
| 70 |
+
research_data: Dict[str, Any],
|
| 71 |
+
analysis_data: Dict[str, Any],
|
| 72 |
+
) -> Dict[str, Any]:
|
| 73 |
+
"""
|
| 74 |
+
Generate comprehensive market intelligence report.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
research_data: Output from ResearchAgent
|
| 78 |
+
analysis_data: Output from AnalysisAgent
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
Dictionary with report components:
|
| 82 |
+
- executive_summary: Brief overview
|
| 83 |
+
- full_report: Complete markdown report
|
| 84 |
+
- metadata: Report metadata (date, sources count, etc.)
|
| 85 |
+
"""
|
| 86 |
+
company_name = research_data.get("company_name", "Unknown Company")
|
| 87 |
+
logger.info(f"Starting report generation for: {company_name}")
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# Generate report sections
|
| 91 |
+
exec_summary = await self._write_executive_summary(
|
| 92 |
+
research_data, analysis_data
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
full_report = await self._write_full_report(
|
| 96 |
+
research_data, analysis_data, exec_summary
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Gather metadata
|
| 100 |
+
metadata = {
|
| 101 |
+
"company_name": company_name,
|
| 102 |
+
"industry": research_data.get("industry"),
|
| 103 |
+
"generated_date": datetime.now().isoformat(),
|
| 104 |
+
"sources_count": len(research_data.get("raw_sources", [])),
|
| 105 |
+
"model_used": self.model_name,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
logger.info(f"Report generation complete for {company_name}")
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
"executive_summary": exec_summary,
|
| 112 |
+
"full_report": full_report,
|
| 113 |
+
"metadata": metadata,
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logger.error(f"Report generation failed for {company_name}: {e}")
|
| 118 |
+
raise
|
| 119 |
+
|
| 120 |
+
async def _write_executive_summary(
|
| 121 |
+
self,
|
| 122 |
+
research_data: Dict[str, Any],
|
| 123 |
+
analysis_data: Dict[str, Any],
|
| 124 |
+
) -> str:
|
| 125 |
+
"""
|
| 126 |
+
Write executive summary (200-300 words).
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
research_data: Research results
|
| 130 |
+
analysis_data: Analysis results
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
Executive summary text
|
| 134 |
+
"""
|
| 135 |
+
company_name = research_data.get("company_name")
|
| 136 |
+
|
| 137 |
+
user_message = f"""Write a concise executive summary for a market intelligence report on {company_name}.
|
| 138 |
+
|
| 139 |
+
Use this information:
|
| 140 |
+
|
| 141 |
+
COMPANY OVERVIEW:
|
| 142 |
+
{research_data.get("company_overview", "")}
|
| 143 |
+
|
| 144 |
+
KEY INSIGHTS FROM SWOT:
|
| 145 |
+
{analysis_data.get("swot", "")}
|
| 146 |
+
|
| 147 |
+
STRATEGIC RECOMMENDATIONS:
|
| 148 |
+
{analysis_data.get("strategic_recommendations", "")}
|
| 149 |
+
|
| 150 |
+
Requirements:
|
| 151 |
+
- 200-300 words
|
| 152 |
+
- Cover: company overview, market position, key findings, main recommendations
|
| 153 |
+
- Written for senior executives (clear, actionable)
|
| 154 |
+
- Professional business tone
|
| 155 |
+
|
| 156 |
+
Start directly with content (no "Executive Summary" heading)."""
|
| 157 |
+
|
| 158 |
+
messages = self._create_messages(user_message)
|
| 159 |
+
response = await self._invoke_llm(messages)
|
| 160 |
+
|
| 161 |
+
return response
|
| 162 |
+
|
| 163 |
+
async def _write_full_report(
|
| 164 |
+
self,
|
| 165 |
+
research_data: Dict[str, Any],
|
| 166 |
+
analysis_data: Dict[str, Any],
|
| 167 |
+
exec_summary: str,
|
| 168 |
+
) -> str:
|
| 169 |
+
"""
|
| 170 |
+
Write complete markdown report.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
research_data: Research results
|
| 174 |
+
analysis_data: Analysis results
|
| 175 |
+
exec_summary: Executive summary
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
Full report in markdown format
|
| 179 |
+
"""
|
| 180 |
+
company_name = research_data.get("company_name")
|
| 181 |
+
industry = research_data.get("industry") or "Market"
|
| 182 |
+
|
| 183 |
+
# Build comprehensive context
|
| 184 |
+
context = f"""
|
| 185 |
+
COMPANY: {company_name}
|
| 186 |
+
INDUSTRY: {industry}
|
| 187 |
+
|
| 188 |
+
RESEARCH DATA:
|
| 189 |
+
Company Overview: {research_data.get("company_overview", "")}
|
| 190 |
+
Competitors: {research_data.get("competitors", "")}
|
| 191 |
+
Market Trends: {research_data.get("market_trends", "")}
|
| 192 |
+
|
| 193 |
+
ANALYSIS DATA:
|
| 194 |
+
SWOT: {analysis_data.get("swot", "")}
|
| 195 |
+
Competitive Matrix: {analysis_data.get("competitive_matrix", "")}
|
| 196 |
+
Market Positioning: {analysis_data.get("positioning", "")}
|
| 197 |
+
Strategic Recommendations: {analysis_data.get("strategic_recommendations", "")}
|
| 198 |
+
|
| 199 |
+
EXECUTIVE SUMMARY:
|
| 200 |
+
{exec_summary}
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
user_message = f"""Create a comprehensive market intelligence report for {company_name} in markdown format.
|
| 204 |
+
|
| 205 |
+
Use all the provided research and analysis data.
|
| 206 |
+
|
| 207 |
+
Structure the report as follows:
|
| 208 |
+
|
| 209 |
+
# Market Intelligence Report: {company_name}
|
| 210 |
+
|
| 211 |
+
## Executive Summary
|
| 212 |
+
[Insert the provided executive summary]
|
| 213 |
+
|
| 214 |
+
## 1. Company Overview
|
| 215 |
+
[Detailed company information from research]
|
| 216 |
+
|
| 217 |
+
## 2. Competitive Landscape
|
| 218 |
+
[Competitor analysis and competitive matrix]
|
| 219 |
+
|
| 220 |
+
## 3. SWOT Analysis
|
| 221 |
+
[Detailed SWOT with clear sections: Strengths, Weaknesses, Opportunities, Threats]
|
| 222 |
+
|
| 223 |
+
## 4. Market Positioning
|
| 224 |
+
[Positioning analysis and differentiation]
|
| 225 |
+
|
| 226 |
+
## 5. Market Trends & Insights
|
| 227 |
+
[Industry trends and market dynamics]
|
| 228 |
+
|
| 229 |
+
## 6. Strategic Recommendations
|
| 230 |
+
[Prioritized recommendations with rationale]
|
| 231 |
+
|
| 232 |
+
## 7. Sources
|
| 233 |
+
[List key sources used]
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
Report generated: {datetime.now().strftime("%B %d, %Y")}
|
| 237 |
+
|
| 238 |
+
Data to use:
|
| 239 |
+
{context}
|
| 240 |
+
|
| 241 |
+
Format requirements:
|
| 242 |
+
- Use proper markdown (headers, bullets, tables)
|
| 243 |
+
- Make it professional and polished
|
| 244 |
+
- Include all relevant details
|
| 245 |
+
- Cite sources where appropriate
|
| 246 |
+
- Make it actionable for executives"""
|
| 247 |
+
|
| 248 |
+
messages = self._create_messages(user_message)
|
| 249 |
+
response = await self._invoke_llm(messages)
|
| 250 |
+
|
| 251 |
+
return response
|
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Search tools for web research using Tavily API."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from tavily import TavilyClient
|
| 6 |
+
|
| 7 |
+
from src.utils.config import get_settings
|
| 8 |
+
from src.utils.logging import setup_logger
|
| 9 |
+
|
| 10 |
+
logger = setup_logger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TavilySearchTool:
|
| 14 |
+
"""
|
| 15 |
+
Wrapper for Tavily search API optimized for research agents.
|
| 16 |
+
|
| 17 |
+
Tavily is designed for AI agents and provides clean, structured
|
| 18 |
+
results ideal for LLM consumption.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 22 |
+
"""
|
| 23 |
+
Initialize Tavily search tool.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
api_key: Optional Tavily API key (uses config if None)
|
| 27 |
+
"""
|
| 28 |
+
settings = get_settings()
|
| 29 |
+
self.api_key = api_key or settings.tavily_api_key
|
| 30 |
+
self.client = TavilyClient(api_key=self.api_key)
|
| 31 |
+
|
| 32 |
+
logger.info("Tavily search tool initialized")
|
| 33 |
+
|
| 34 |
+
async def search(
|
| 35 |
+
self,
|
| 36 |
+
query: str,
|
| 37 |
+
max_results: int = 5,
|
| 38 |
+
search_depth: str = "advanced",
|
| 39 |
+
include_domains: Optional[List[str]] = None,
|
| 40 |
+
exclude_domains: Optional[List[str]] = None,
|
| 41 |
+
) -> Dict:
|
| 42 |
+
"""
|
| 43 |
+
Perform web search using Tavily.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
query: Search query
|
| 47 |
+
max_results: Maximum number of results to return
|
| 48 |
+
search_depth: "basic" or "advanced" (advanced is more comprehensive)
|
| 49 |
+
include_domains: Optional list of domains to include
|
| 50 |
+
exclude_domains: Optional list of domains to exclude
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Dictionary with search results:
|
| 54 |
+
- results: List of search results
|
| 55 |
+
- query: Original query
|
| 56 |
+
- answer: Tavily's AI-generated answer (if available)
|
| 57 |
+
"""
|
| 58 |
+
try:
|
| 59 |
+
logger.info(f"Tavily search: {query}")
|
| 60 |
+
|
| 61 |
+
response = self.client.search(
|
| 62 |
+
query=query,
|
| 63 |
+
max_results=max_results,
|
| 64 |
+
search_depth=search_depth,
|
| 65 |
+
include_domains=include_domains,
|
| 66 |
+
exclude_domains=exclude_domains,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
logger.info(f"Tavily returned {len(response.get('results', []))} results")
|
| 70 |
+
|
| 71 |
+
return response
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"Tavily search failed: {e}")
|
| 75 |
+
raise
|
| 76 |
+
|
| 77 |
+
async def get_company_info(
|
| 78 |
+
self,
|
| 79 |
+
company_name: str,
|
| 80 |
+
max_results: int = 10,
|
| 81 |
+
) -> Dict:
|
| 82 |
+
"""
|
| 83 |
+
Get comprehensive company information.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
company_name: Company name to research
|
| 87 |
+
max_results: Maximum results to retrieve
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Search results focused on company information
|
| 91 |
+
"""
|
| 92 |
+
query = f"{company_name} company overview products services business model"
|
| 93 |
+
return await self.search(
|
| 94 |
+
query=query,
|
| 95 |
+
max_results=max_results,
|
| 96 |
+
search_depth="advanced",
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
async def get_competitor_info(
|
| 100 |
+
self,
|
| 101 |
+
company_name: str,
|
| 102 |
+
industry: Optional[str] = None,
|
| 103 |
+
max_results: int = 10,
|
| 104 |
+
) -> Dict:
|
| 105 |
+
"""
|
| 106 |
+
Find competitors for a given company.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
company_name: Company name
|
| 110 |
+
industry: Optional industry context
|
| 111 |
+
max_results: Maximum results
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Search results about competitors
|
| 115 |
+
"""
|
| 116 |
+
industry_context = f"in {industry}" if industry else ""
|
| 117 |
+
query = f"{company_name} competitors alternatives {industry_context}"
|
| 118 |
+
|
| 119 |
+
return await self.search(
|
| 120 |
+
query=query,
|
| 121 |
+
max_results=max_results,
|
| 122 |
+
search_depth="advanced",
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
async def get_market_trends(
|
| 126 |
+
self,
|
| 127 |
+
industry: str,
|
| 128 |
+
year: Optional[str] = "2025",
|
| 129 |
+
max_results: int = 8,
|
| 130 |
+
) -> Dict:
|
| 131 |
+
"""
|
| 132 |
+
Get market trends for an industry.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
industry: Industry name
|
| 136 |
+
year: Year for trends (default: 2025)
|
| 137 |
+
max_results: Maximum results
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
Search results about market trends
|
| 141 |
+
"""
|
| 142 |
+
query = f"{industry} market trends {year} growth forecast opportunities"
|
| 143 |
+
|
| 144 |
+
return await self.search(
|
| 145 |
+
query=query,
|
| 146 |
+
max_results=max_results,
|
| 147 |
+
search_depth="advanced",
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
def format_results_for_llm(self, search_response: Dict) -> str:
|
| 151 |
+
"""
|
| 152 |
+
Format search results for LLM consumption.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
search_response: Tavily search response
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Formatted string with search results
|
| 159 |
+
"""
|
| 160 |
+
results = search_response.get("results", [])
|
| 161 |
+
|
| 162 |
+
if not results:
|
| 163 |
+
return "No search results found."
|
| 164 |
+
|
| 165 |
+
formatted = []
|
| 166 |
+
for i, result in enumerate(results, 1):
|
| 167 |
+
title = result.get("title", "No title")
|
| 168 |
+
url = result.get("url", "")
|
| 169 |
+
content = result.get("content", "No content")
|
| 170 |
+
score = result.get("score", 0)
|
| 171 |
+
|
| 172 |
+
formatted.append(
|
| 173 |
+
f"[{i}] {title}\n"
|
| 174 |
+
f"URL: {url}\n"
|
| 175 |
+
f"Relevance: {score:.2f}\n"
|
| 176 |
+
f"Content: {content}\n"
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
# Add AI answer if available
|
| 180 |
+
if answer := search_response.get("answer"):
|
| 181 |
+
formatted.insert(0, f"AI Summary: {answer}\n\n")
|
| 182 |
+
|
| 183 |
+
return "\n".join(formatted)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class WikipediaSearchTool:
|
| 187 |
+
"""
|
| 188 |
+
Wikipedia search for factual company/product information.
|
| 189 |
+
|
| 190 |
+
Note: This is a simple wrapper. For production, consider using
|
| 191 |
+
the wikipedia-api library for more robust access.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
def __init__(self):
|
| 195 |
+
"""Initialize Wikipedia search tool."""
|
| 196 |
+
logger.info("Wikipedia search tool initialized")
|
| 197 |
+
|
| 198 |
+
async def search(self, query: str, max_results: int = 3) -> Dict:
|
| 199 |
+
"""
|
| 200 |
+
Search Wikipedia (placeholder for now).
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
query: Search query
|
| 204 |
+
max_results: Maximum results
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
Search results dictionary
|
| 208 |
+
"""
|
| 209 |
+
# TODO: Implement actual Wikipedia API integration
|
| 210 |
+
# For now, we'll use Tavily which can search Wikipedia
|
| 211 |
+
logger.info(f"Wikipedia search: {query}")
|
| 212 |
+
|
| 213 |
+
return {
|
| 214 |
+
"query": query,
|
| 215 |
+
"results": [],
|
| 216 |
+
"note": "Wikipedia integration pending - using Tavily for now",
|
| 217 |
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script to run agents manually on a real query.
|
| 3 |
+
|
| 4 |
+
This demonstrates the agent pipeline without LangGraph orchestration.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
from src.agents.researcher import ResearchAgent
|
| 11 |
+
from src.agents.analyst import AnalysisAgent
|
| 12 |
+
from src.agents.writer import WriterAgent
|
| 13 |
+
from src.utils.cost_tracker import CostTracker
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def test_complete_pipeline():
|
| 17 |
+
"""Test the complete agent pipeline."""
|
| 18 |
+
|
| 19 |
+
# Query from user
|
| 20 |
+
query = "going viral on Instagram using AI without showing face"
|
| 21 |
+
industry = "Social Media Marketing"
|
| 22 |
+
|
| 23 |
+
print("=" * 80)
|
| 24 |
+
print(f"TESTING AGENT PIPELINE")
|
| 25 |
+
print(f"Query: {query}")
|
| 26 |
+
print(f"Industry: {industry}")
|
| 27 |
+
print("=" * 80)
|
| 28 |
+
print()
|
| 29 |
+
|
| 30 |
+
# Shared cost tracker
|
| 31 |
+
cost_tracker = CostTracker()
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
# Step 1: Research Agent
|
| 35 |
+
print("\n[STEP 1] Running Research Agent...")
|
| 36 |
+
print("-" * 80)
|
| 37 |
+
researcher = ResearchAgent(cost_tracker=cost_tracker)
|
| 38 |
+
|
| 39 |
+
research_results = await researcher.run(
|
| 40 |
+
company_name=query, industry=industry, research_depth="comprehensive"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
print(f"\nResearch completed!")
|
| 44 |
+
print(f"Sources gathered: {len(research_results.get('raw_sources', []))}")
|
| 45 |
+
print(f"\nCompany Overview (first 500 chars):")
|
| 46 |
+
print(research_results["company_overview"][:500] + "...")
|
| 47 |
+
|
| 48 |
+
# Step 2: Analysis Agent
|
| 49 |
+
print("\n\n[STEP 2] Running Analysis Agent...")
|
| 50 |
+
print("-" * 80)
|
| 51 |
+
analyst = AnalysisAgent(cost_tracker=cost_tracker)
|
| 52 |
+
|
| 53 |
+
analysis_results = await analyst.run(research_data=research_results)
|
| 54 |
+
|
| 55 |
+
print(f"\nAnalysis completed!")
|
| 56 |
+
print(f"\nSWOT Analysis (first 500 chars):")
|
| 57 |
+
print(analysis_results["swot"][:500] + "...")
|
| 58 |
+
|
| 59 |
+
# Step 3: Writer Agent
|
| 60 |
+
print("\n\n[STEP 3] Running Writer Agent...")
|
| 61 |
+
print("-" * 80)
|
| 62 |
+
writer = WriterAgent(cost_tracker=cost_tracker)
|
| 63 |
+
|
| 64 |
+
report_results = await writer.run(
|
| 65 |
+
research_data=research_results, analysis_data=analysis_results
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
print(f"\nReport generation completed!")
|
| 69 |
+
|
| 70 |
+
# Display results
|
| 71 |
+
print("\n\n" + "=" * 80)
|
| 72 |
+
print("EXECUTIVE SUMMARY")
|
| 73 |
+
print("=" * 80)
|
| 74 |
+
print(report_results["executive_summary"])
|
| 75 |
+
|
| 76 |
+
print("\n\n" + "=" * 80)
|
| 77 |
+
print("FULL REPORT")
|
| 78 |
+
print("=" * 80)
|
| 79 |
+
print(report_results["full_report"])
|
| 80 |
+
|
| 81 |
+
# Cost summary
|
| 82 |
+
print("\n\n" + "=" * 80)
|
| 83 |
+
print("COST SUMMARY")
|
| 84 |
+
print("=" * 80)
|
| 85 |
+
summary = cost_tracker.get_summary()
|
| 86 |
+
print(f"Total Cost: ${summary['total_cost']:.4f}")
|
| 87 |
+
print(f"Total Tokens: {summary['total_tokens']:,}")
|
| 88 |
+
print(f"API Calls: {summary['calls']}")
|
| 89 |
+
print(f"\nBreakdown by model:")
|
| 90 |
+
for model, data in summary["by_model"].items():
|
| 91 |
+
print(f" {model}:")
|
| 92 |
+
print(f" Input: {data['input_tokens']:,} tokens")
|
| 93 |
+
print(f" Output: {data['output_tokens']:,} tokens")
|
| 94 |
+
print(f" Cost: ${data['cost']:.4f}")
|
| 95 |
+
|
| 96 |
+
# Save report
|
| 97 |
+
output_file = "test_report.md"
|
| 98 |
+
with open(output_file, "w") as f:
|
| 99 |
+
f.write(f"# Market Intelligence Report\n\n")
|
| 100 |
+
f.write(f"**Query:** {query}\n\n")
|
| 101 |
+
f.write(f"**Industry:** {industry}\n\n")
|
| 102 |
+
f.write(f"---\n\n")
|
| 103 |
+
f.write(f"## Executive Summary\n\n")
|
| 104 |
+
f.write(report_results["executive_summary"])
|
| 105 |
+
f.write(f"\n\n---\n\n")
|
| 106 |
+
f.write(report_results["full_report"])
|
| 107 |
+
f.write(f"\n\n---\n\n")
|
| 108 |
+
f.write(f"**Total Cost:** ${summary['total_cost']:.4f}\n")
|
| 109 |
+
|
| 110 |
+
print(f"\n\nReport saved to: {output_file}")
|
| 111 |
+
|
| 112 |
+
return True
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"\n\nERROR: {e}")
|
| 116 |
+
import traceback
|
| 117 |
+
|
| 118 |
+
traceback.print_exc()
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
print("\nStarting agent test...")
|
| 124 |
+
print("Make sure you have .env file with API keys set up!\n")
|
| 125 |
+
|
| 126 |
+
result = asyncio.run(test_complete_pipeline())
|
| 127 |
+
|
| 128 |
+
if result:
|
| 129 |
+
print("\n\nTest PASSED!")
|
| 130 |
+
sys.exit(0)
|
| 131 |
+
else:
|
| 132 |
+
print("\n\nTest FAILED!")
|
| 133 |
+
sys.exit(1)
|
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Market Intelligence Report
|
| 2 |
+
|
| 3 |
+
**Query:** going viral on Instagram using AI without showing face
|
| 4 |
+
|
| 5 |
+
**Industry:** Social Media Marketing
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Executive Summary
|
| 10 |
+
|
| 11 |
+
This executive summary outlines the market intelligence on leveraging AI tools for faceless Instagram content to achieve virality, targeting rapid growth in short-form video amid a $32.55B influencer market (57.1% Instagram share) [4].
|
| 12 |
+
|
| 13 |
+
**Key Players and Landscape**: Leading SaaS tools include Invideo AI (text-to-Reel generation in ~20 minutes), Virlo (faceless workflows with Leonardo.ai/Eleven Labs integrations), and Zebracat (viral niches like pet videos/AI art). Supporting platforms: TailorTalk (DM automation), Madgicx (trend forecasting), Juicer (content/engagement AI). AI influencers like Lil Miquela (3M+ followers) exemplify success via brand deals [1][2][3][9].
|
| 14 |
+
|
| 15 |
+
**Market Position**: Faceless AI content commands premium engagement—5% for AI art (vs. 2% Instagram average), 3% uplift over humans—driven by scalability, 24/7 operation, and low risks. High-growth niche amid 93% marketer video spend increase ($140B in 2025), but faces authenticity gaps and saturation [3][9].
|
| 16 |
+
|
| 17 |
+
**Key Findings (SWOT Highlights)**: **Strengths**: Rapid production, superior metrics, multi-platform repurposing. **Weaknesses**: AI fatigue, tool dependency. **Opportunities**: Social commerce ($2.9T by 2026), niche virality. **Threats**: Consumer distrust (50% pullback), algorithm volatility [1-10].
|
| 18 |
+
|
| 19 |
+
**Strategic Recommendations** (Prioritized for Execution):
|
| 20 |
+
- **Immediate (0-3 Months)**: Deploy workflows in 2-3 niches (e.g., pet motivation) for 5-10 Reels/week; automate outbound engagement for 3-5x reach, targeting 10K followers [1][3][6].
|
| 21 |
+
- **Medium (6-12 Months)**: Blend UGC (30%) for authenticity; launch Instagram Shops/affiliates for $10K+ quarterly revenue [2][4].
|
| 22 |
+
- **Long-Term**: Cross-post to TikTok/YouTube; build proprietary prompts for 1M+ followers, 7% engagement [5][6].
|
| 23 |
+
|
| 24 |
+
Actionable path: Start with Invideo/Z zebracat pilots to capture first-mover edges, mitigating risks for scalable, passive income. (248 words)
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
# Market Intelligence Report: Going Viral on Instagram Using AI Without Showing Face
|
| 29 |
+
|
| 30 |
+
## Executive Summary
|
| 31 |
+
This executive summary outlines the market intelligence on leveraging AI tools for faceless Instagram content to achieve virality, targeting rapid growth in short-form video amid a $32.55B influencer market (57.1% Instagram share) [4].
|
| 32 |
+
|
| 33 |
+
**Key Players and Landscape**: Leading SaaS tools include Invideo AI (text-to-Reel generation in ~20 minutes), Virlo (faceless workflows with Leonardo.ai/Eleven Labs integrations), and Zebracat (viral niches like pet videos/AI art). Supporting platforms: TailorTalk (DM automation), Madgicx (trend forecasting), Juicer (content/engagement AI). AI influencers like Lil Miquela (3M+ followers) exemplify success via brand deals [1][2][3][9].
|
| 34 |
+
|
| 35 |
+
**Market Position**: Faceless AI content commands premium engagement—5% for AI art (vs. 2% Instagram average), 3% uplift over humans—driven by scalability, 24/7 operation, and low risks. High-growth niche amid 93% marketer video spend increase ($140B in 2025), but faces authenticity gaps and saturation [3][9].
|
| 36 |
+
|
| 37 |
+
**Key Findings (SWOT Highlights)**: **Strengths**: Rapid production, superior metrics, multi-platform repurposing. **Weaknesses**: AI fatigue, tool dependency. **Opportunities**: Social commerce ($2.9T by 2026), niche virality. **Threats**: Consumer distrust (50% pullback), algorithm volatility [1-10].
|
| 38 |
+
|
| 39 |
+
**Strategic Recommendations** (Prioritized for Execution):
|
| 40 |
+
- **Immediate (0-3 Months)**: Deploy workflows in 2-3 niches (e.g., pet motivation) for 5-10 Reels/week; automate outbound engagement for 3-5x reach, targeting 10K followers [1][3][6].
|
| 41 |
+
- **Medium (6-12 Months)**: Blend UGC (30%) for authenticity; launch Instagram Shops/affiliates for $10K+ quarterly revenue [2][4].
|
| 42 |
+
- **Long-Term**: Cross-post to TikTok/YouTube; build proprietary prompts for 1M+ followers, 7% engagement [5][6].
|
| 43 |
+
|
| 44 |
+
Actionable path: Start with Invideo/Zebracat pilots to capture first-mover edges, mitigating risks for scalable, passive income. (248 words)
|
| 45 |
+
|
| 46 |
+
## 1. Company Overview
|
| 47 |
+
"Going Viral on Instagram Using AI Without Showing Face" represents an emerging strategy and ecosystem in the social media marketing industry, leveraging AI-powered SaaS tools to create scalable, anonymous content (e.g., Reels, AI art, videos) for virality and monetization. No formal company structure (e.g., founding date, headquarters, employee count) is established, as it is a niche approach powered by specialized platforms rather than a single entity [1][2][3].
|
| 48 |
+
|
| 49 |
+
### Key Offerings (Products & Services)
|
| 50 |
+
- **Invideo AI**: Generates faceless Instagram Reels from text prompts in ~20 minutes; supports monetization via digital products, affiliates, and brand partnerships [1].
|
| 51 |
+
- **Virlo**: AI workflows for faceless accounts, integrating Leonardo.ai (imagery), Eleven Labs (voice-overs), and Flarecut (video transformation); enables Instagram Shop setup for merchandise [2].
|
| 52 |
+
- **Zebracat**: AI video generator for viral faceless niches (e.g., pets, AI art, study-with-me, PowerPoint tutorials); focuses on psychological virality triggers [3].
|
| 53 |
+
- Supporting tools: TailorTalk (Instagram automation for DMs/comments), Madgicx (audience growth/trend forecasting), Juicer (content creation/engagement), Copy.ai (copywriting), Descript (editing), Mentionlytics (social listening) [5][6][7][8].
|
| 54 |
+
|
| 55 |
+
### Business Model
|
| 56 |
+
- Primarily SaaS/subscription-based for tools (e.g., Invideo AI, Zebracat sign-ups) [1][3].
|
| 57 |
+
- Monetization strategies: Digital products (spreadsheets, courses), affiliates, brand deals (e.g., AI influencers like Aitana Lopez with Fenty Beauty), Instagram Shops, AdSense [1][2][4][9].
|
| 58 |
+
- Enables 24/7 scalable engagement for passive income without human limitations [4][9].
|
| 59 |
+
|
| 60 |
+
### Market Position and Key Metrics
|
| 61 |
+
- Niche high-growth positioning: Faceless AI art achieves ~5% engagement (vs. Instagram 2% average); AI influencers yield 3% higher engagement than humans [3][9].
|
| 62 |
+
- Examples: Lil Miquela (3M+ followers); Zebracat cases (509K followers, millions of views) [3][9].
|
| 63 |
+
- Growth drivers: Efficiency, multi-platform repurposing, no reputational risks [1][2][3].
|
| 64 |
+
|
| 65 |
+
## 2. Competitive Landscape
|
| 66 |
+
The competitive landscape for AI-driven faceless Instagram virality is emerging and fragmented, dominated by niche SaaS tools focused on content generation, automation, and optimization. No dominant player holds quantifiable market share, but Zebracat leads in proven virality [1][3].
|
| 67 |
+
|
| 68 |
+
### Main Competitors
|
| 69 |
+
- **Zebracat**: AI video for niches like pets/AI art; high engagement (5%) [1].
|
| 70 |
+
- **BigMotion AI**: Full automation for Reels (generation to auto-posting) [3].
|
| 71 |
+
- **Faceless.video**: Faceless video ads with auto-posting/social integration [4].
|
| 72 |
+
- **Crayo AI**: Viral short-form Reels ideation/production by faceless experts [6].
|
| 73 |
+
|
| 74 |
+
### Competitive Positioning and Dynamics
|
| 75 |
+
- **Leaders**: Zebracat (creative niches, proven metrics); BigMotion AI (automation for passive income).
|
| 76 |
+
- **Followers**: Faceless.video (ads), Crayo (ideation).
|
| 77 |
+
- Differentiation: Zebracat (niche templates), BigMotion (free tier/auto-post), Faceless.video (analytics), Crayo (expert-backed).
|
| 78 |
+
- Market: Fast-evolving; general tools (Canva, Hootsuite) lag in faceless virality [1][3][4][6].
|
| 79 |
+
|
| 80 |
+
### Competitive Matrix
|
| 81 |
+
| Dimension | FacelessViral IG (Ours) | Zebracat [1] | BigMotion AI [3] | Faceless.video [4] | Crayo AI [6] |
|
| 82 |
+
|------------------------|------------------------------------------|---------------------------------------|---------------------------------------|---------------------------------------|---------------------------------------|
|
| 83 |
+
| **Market Share/Size** | Low (Emerging; IG-specific focus) | **High** (509K followers, 5% engagement) | Medium-High (Free multi-platform) | Low (No IG cases) | Low (Emerging) |
|
| 84 |
+
| **Product Range** | Medium (Reels virality) | Medium (Niche templates) | **High** (Gen-to-post automation) | Medium (Ads/auto-post) | Medium (Ideation/production) |
|
| 85 |
+
| **Pricing Strategy** | Unknown (Competitive SaaS) | Medium (Beginner-friendly) | **High** (Free tier) | Medium (Tiered SaaS) | Medium (No free tier) |
|
| 86 |
+
| **Technology/Innovation** | Medium (Reels optimization) | **High** (Niche workflows) | **High** (Algo adaptation) | Medium (Analytics integration) | Medium-High (Viral ideation) |
|
| 87 |
+
| **Customer Segments** | IG viral creators (passive focus) | Beginners (pets/AI art) | Passive income (multi-platform) | Ad marketers | Short-form creators |
|
| 88 |
+
| **Strengths** | IG-exclusive virality, anonymity | Proven metrics, templates | Automation, free tier | Ad optimization | Expert-backed virals |
|
| 89 |
+
| **Weaknesses** | Narrow scope, no metrics | Niche-limited | Less creative depth | Ad-centric | Less automation |
|
| 90 |
+
|
| 91 |
+
**Insights**: Differentiate via IG-exclusive focus; pursue hybrid automation/creatives [1][3].
|
| 92 |
+
|
| 93 |
+
## 3. SWOT Analysis
|
| 94 |
+
|
| 95 |
+
#### Strengths (Internal Positive Factors)
|
| 96 |
+
- **Rapid, Scalable Content Production**: AI tools generate Reels in ~20 minutes; workflows enable high-volume output [1][2][3].
|
| 97 |
+
- **Superior Engagement Metrics**: 5% for AI art, 3% uplift for AI influencers [3][9].
|
| 98 |
+
- **Low Reputational and Operational Risks**: No face exposure; 24/7 scalability [2][3][9].
|
| 99 |
+
- **Cost-Effective Monetization Pathways**: Affiliates, Shops, digital products [1][2][4].
|
| 100 |
+
- **Trend-First-Mover Capabilities**: Real-time forecasting [6][7].
|
| 101 |
+
- **Multi-Platform Repurposing**: Easy adaptation [1][3].
|
| 102 |
+
|
| 103 |
+
#### Weaknesses (Internal Negative Factors)
|
| 104 |
+
- **Perceived Lack of Authenticity**: Risks AI fatigue [3][9][10].
|
| 105 |
+
- **Dependency on Third-Party Tools**: Pricing/outage vulnerabilities [1][2][3].
|
| 106 |
+
- **Steep Learning Curve**: Prompt mastery needed [2][3].
|
| 107 |
+
- **Limited Metrics for New Entrants**: Unproven scaling [1][3].
|
| 108 |
+
- **Content Saturation Risk**: Oversupply in niches [3].
|
| 109 |
+
- **Narrow Human Oversight**: Lacks nuance [5].
|
| 110 |
+
|
| 111 |
+
#### Opportunities (External Positive Factors)
|
| 112 |
+
- **Short-Form Video and Influencer Boom**: $140B video ads, $32.55B influencers [4].
|
| 113 |
+
- **Social Commerce Expansion**: $2.9T by 2026 [2][3].
|
| 114 |
+
- **Niche Virality and AI Art Demand**: Psychological hooks [1][3].
|
| 115 |
+
- **AI Efficiency Amid Burnout**: 50% leaders adopting [2][4][6].
|
| 116 |
+
- **Platform Discovery Prioritization**: Organic reach [1][2].
|
| 117 |
+
- **UGC and Private Communities**: Retention boost [1][4].
|
| 118 |
+
|
| 119 |
+
#### Threats (External Negative Factors)
|
| 120 |
+
- **Consumer Distrust and Pullback**: 50% limiting interactions [2][4].
|
| 121 |
+
- **Intense Niche Competition**: Zebracat/BigMotion dominance [1][3][4][6].
|
| 122 |
+
- **Algorithm Volatility**: Interaction shifts [1][3][4].
|
| 123 |
+
- **Market Saturation and Fatigue**: AI erosion [1][4][7][10].
|
| 124 |
+
- **Regulatory/Economic Pressures**: Ad spend benefits incumbents [2][4][9].
|
| 125 |
+
- **Resource Gaps**: Investment needs [2][5][8].
|
| 126 |
+
|
| 127 |
+
## 4. Market Positioning
|
| 128 |
+
Faceless AI Virality positions as an **accessible, low-risk Instagram monetization strategy** via anonymous Reels, yielding 5% engagement [3].
|
| 129 |
+
|
| 130 |
+
### 1. Current Positioning
|
| 131 |
+
- **Value Proposition**: 20-min virals, passive income, 2.5x engagement [1][2][3][9].
|
| 132 |
+
- **Target Segments**:
|
| 133 |
+
| Segment | Description | Examples |
|
| 134 |
+
|----------------------|--------------------------------------|---------------------------|
|
| 135 |
+
| Beginner Creators | Quick virality/passive income | Pet/AI art [1][3] |
|
| 136 |
+
| Passive Income Seekers | Avoid personal branding | Affiliates [1][2] |
|
| 137 |
+
| Niche Producers | Specialized themes | Tutorials [3] |
|
| 138 |
+
| AI Influencer Teams | Virtual personas | Aitana Lopez [2][4] |
|
| 139 |
+
|
| 140 |
+
### 2. Competitive Differentiation
|
| 141 |
+
- **Positioning Map** (Automation vs. Virality):
|
| 142 |
+
| Tool/Player | Automation | Virality Focus | Key Diff |
|
| 143 |
+
|-------------------|------------|----------------|---------------------------|
|
| 144 |
+
| Zebracat | Medium | High | Proven metrics [1] |
|
| 145 |
+
| BigMotion AI | High | Medium | Free auto-post [3] |
|
| 146 |
+
| Faceless.video | High | Low-Medium | Ads [4] |
|
| 147 |
+
| Crayo AI | Medium | High | Ideation [6] |
|
| 148 |
+
| **Ours** | **High** | **Highest** | Anonymity + hooks [3][9] |
|
| 149 |
+
|
| 150 |
+
- **USP**: Anonymity + scaling outperforms humans [9].
|
| 151 |
+
|
| 152 |
+
### 3. Positioning Gaps and Opportunities
|
| 153 |
+
- Gaps: Enterprise, global, e-commerce.
|
| 154 |
+
- Recommendations: Hybrid SaaS, B2B pivot, authenticity boosters [1][5].
|
| 155 |
+
|
| 156 |
+
## 5. Market Trends & Insights
|
| 157 |
+
### 1. Key Trends
|
| 158 |
+
- Short-form video dominance (78% preference) [4].
|
| 159 |
+
- Authenticity/UGC/niches rise [1][3][4].
|
| 160 |
+
- AI integration, social commerce, discovery engines [2][3][7].
|
| 161 |
+
|
| 162 |
+
### 2. Growth Drivers
|
| 163 |
+
- Ad/video spend ($276.7B/$140B in 2025) [2][4].
|
| 164 |
+
- Influencers ($32.55B), commerce ($2.9T) [3][4].
|
| 165 |
+
- AI innovations, niche storytelling [2][5][6].
|
| 166 |
+
|
| 167 |
+
### 3. Challenges
|
| 168 |
+
- Consumer pullback (50%), algorithm uncertainty, saturation [1][2][4].
|
| 169 |
+
- Burnout, authenticity fatigue [2][5].
|
| 170 |
+
|
| 171 |
+
### 4. Future Outlook
|
| 172 |
+
- Growth through 2026; AI enabler with authenticity focus; fragmentation risks [2][4][5].
|
| 173 |
+
|
| 174 |
+
## 6. Strategic Recommendations
|
| 175 |
+
|
| 176 |
+
#### HIGH PRIORITY (0-3 Months)
|
| 177 |
+
- **1**: Niche workflows (5-10 Reels/week); 3-5x reach [1][3][6].
|
| 178 |
+
- **2**: Auto-posting/outbound; 2-4% lift [3][9].
|
| 179 |
+
|
| 180 |
+
#### MEDIUM PRIORITY (6-12 Months)
|
| 181 |
+
- **3**: 30% UGC blend; 25% churn reduction [1][4].
|
| 182 |
+
- **4**: Shops/affiliates; $10K+ revenue [2][4].
|
| 183 |
+
|
| 184 |
+
#### LONG-TERM (12+ Months)
|
| 185 |
+
- **5**: Multi-platform; 2x audience [1][4].
|
| 186 |
+
- **6**: Proprietary prompts; 7% engagement [2][5][6].
|
| 187 |
+
|
| 188 |
+
## 7. Sources
|
| 189 |
+
- [1] Zebracat case studies and examples.
|
| 190 |
+
- [2] Virlo workflows and AI influencer context.
|
| 191 |
+
- [3] Invideo AI, BigMotion AI features; faceless engagement metrics.
|
| 192 |
+
- [4] Faceless.video; market trends (e.g., Gartner, HypeAuditor).
|
| 193 |
+
- [5] TailorTalk automation.
|
| 194 |
+
- [6] Madgicx, Crayo AI.
|
| 195 |
+
- [7] Juicer social listening.
|
| 196 |
+
- [8] Copy.ai, Descript, Mentionlytics.
|
| 197 |
+
- [9] AI influencers (Lil Miquela, Aitana Lopez).
|
| 198 |
+
- [10] General tools (Canva, Hootsuite); authenticity research.
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
Report generated: November 28, 2025
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
**Total Cost:** $0.0000
|
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for base agent class."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from unittest.mock import AsyncMock, MagicMock, patch
|
| 5 |
+
|
| 6 |
+
from src.agents.base import BaseAgent
|
| 7 |
+
from src.utils.cost_tracker import CostTracker
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestAgent(BaseAgent):
|
| 11 |
+
"""Concrete test agent for testing base class."""
|
| 12 |
+
|
| 13 |
+
def get_system_prompt(self) -> str:
|
| 14 |
+
return "Test system prompt"
|
| 15 |
+
|
| 16 |
+
async def run(self, **kwargs):
|
| 17 |
+
return {"result": "test"}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@pytest.mark.asyncio
|
| 21 |
+
async def test_base_agent_initialization():
|
| 22 |
+
"""Test base agent initializes correctly."""
|
| 23 |
+
tracker = CostTracker()
|
| 24 |
+
|
| 25 |
+
with patch("src.agents.base.get_settings") as mock_settings:
|
| 26 |
+
mock_settings.return_value = MagicMock(
|
| 27 |
+
default_model="x-ai/grok-4.1-fast:free",
|
| 28 |
+
openrouter_api_key="test-key",
|
| 29 |
+
openrouter_base_url="https://openrouter.ai/api/v1",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
agent = TestAgent(
|
| 33 |
+
name="TestAgent",
|
| 34 |
+
model="openai/gpt-5-mini",
|
| 35 |
+
temperature=0.5,
|
| 36 |
+
cost_tracker=tracker,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
assert agent.name == "TestAgent"
|
| 40 |
+
assert agent.model_name == "openai/gpt-5-mini"
|
| 41 |
+
assert agent.cost_tracker == tracker
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@pytest.mark.asyncio
|
| 45 |
+
async def test_base_agent_uses_default_model():
|
| 46 |
+
"""Test agent uses default model from config."""
|
| 47 |
+
with patch("src.agents.base.get_settings") as mock_settings:
|
| 48 |
+
mock_settings.return_value = MagicMock(
|
| 49 |
+
default_model="x-ai/grok-4.1-fast:free",
|
| 50 |
+
openrouter_api_key="test-key",
|
| 51 |
+
openrouter_base_url="https://openrouter.ai/api/v1",
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
agent = TestAgent(name="TestAgent")
|
| 55 |
+
|
| 56 |
+
assert agent.model_name == "x-ai/grok-4.1-fast:free"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@pytest.mark.asyncio
|
| 60 |
+
async def test_create_messages():
|
| 61 |
+
"""Test message creation."""
|
| 62 |
+
with patch("src.agents.base.get_settings") as mock_settings:
|
| 63 |
+
mock_settings.return_value = MagicMock(
|
| 64 |
+
default_model="test-model",
|
| 65 |
+
openrouter_api_key="test-key",
|
| 66 |
+
openrouter_base_url="https://test.com",
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
agent = TestAgent(name="TestAgent")
|
| 70 |
+
|
| 71 |
+
messages = agent._create_messages("test user message")
|
| 72 |
+
|
| 73 |
+
assert len(messages) == 2
|
| 74 |
+
assert messages[0].content == "Test system prompt"
|
| 75 |
+
assert messages[1].content == "test user message"
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@pytest.mark.asyncio
|
| 79 |
+
async def test_get_cost_summary():
|
| 80 |
+
"""Test cost summary retrieval."""
|
| 81 |
+
tracker = CostTracker()
|
| 82 |
+
|
| 83 |
+
with patch("src.agents.base.get_settings") as mock_settings:
|
| 84 |
+
mock_settings.return_value = MagicMock(
|
| 85 |
+
default_model="test-model",
|
| 86 |
+
openrouter_api_key="test-key",
|
| 87 |
+
openrouter_base_url="https://test.com",
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
agent = TestAgent(name="TestAgent", cost_tracker=tracker)
|
| 91 |
+
|
| 92 |
+
# Track some usage
|
| 93 |
+
tracker.track_usage("openai/gpt-5-mini", 1000, 500)
|
| 94 |
+
|
| 95 |
+
summary = agent.get_cost_summary()
|
| 96 |
+
|
| 97 |
+
assert summary["total_input_tokens"] == 1000
|
| 98 |
+
assert summary["total_output_tokens"] == 500
|
| 99 |
+
assert summary["calls"] == 1
|