Spaces:
Runtime error
Runtime error
File size: 17,193 Bytes
c895509 cca1560 8ac8a9d cfa7ab8 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 cca1560 c895509 8ac8a9d c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 8ac8a9d c895509 8ac8a9d c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 cca1560 c895509 8ac8a9d cca1560 8ac8a9d c895509 8ac8a9d c895509 8ac8a9d cca1560 c895509 cca1560 c895509 cca1560 8ac8a9d c895509 cca1560 c895509 cca1560 cfa7ab8 cca1560 cfa7ab8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | """Gradio UI for Market Intelligence System.
F1: Research Type Selection β The gateway feature.
Uses Material Design Icons (MDI) as specified in CLAUDE.md.
"""
import asyncio
import logging
import os
import tempfile
from datetime import datetime
import gradio as gr
from src.utils.logging import setup_logger
from src.workflows.market_analysis import MarketIntelligenceWorkflow
from src.workflows.types import ResearchType
logger = setup_logger(__name__)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Material Design Icons (MDI) - CLAUDE.md specification
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MDI_CDN = (
"https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css"
)
MDI_ICONS = {
"domain": "mdi-domain",
"compare": "mdi-compare",
"earth": "mdi-earth",
"sword-cross": "mdi-sword-cross",
"cash-multiple": "mdi-cash-multiple",
"help-circle": "mdi-help-circle",
}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Research Type Configuration
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RESEARCH_TYPE_OPTIONS = {
"Company Analysis": ResearchType.COMPANY_ANALYSIS,
"Competitive Comparison": ResearchType.COMPETITIVE_COMPARISON,
"Market Landscape": ResearchType.MARKET_LANDSCAPE,
"Battle Card": ResearchType.BATTLE_CARD,
"Investment Thesis": ResearchType.INVESTMENT_THESIS,
"Custom Query": ResearchType.CUSTOM_QUERY,
}
RESEARCH_TYPE_META = {
"Company Analysis": {
"icon": "domain",
"desc": "Deep dive on a single company β products, positioning, SWOT",
},
"Competitive Comparison": {
"icon": "compare",
"desc": "Side-by-side comparison of 2-5 competitors",
},
"Market Landscape": {
"icon": "earth",
"desc": "Full market overview with players, trends, and entry analysis",
},
"Battle Card": {
"icon": "sword-cross",
"desc": "1-page sales enablement document",
},
"Investment Thesis": {
"icon": "cash-multiple",
"desc": "Due diligence report for investors",
},
"Custom Query": {
"icon": "help-circle",
"desc": "Free-form research question",
},
}
# Model options for power users who want to test different models
MODEL_OPTIONS = {
"Grok 3 (Free)": "x-ai/grok-3-fast:free",
"GPT-4.1 Mini": "openai/gpt-4.1-mini",
"Claude Sonnet 4": "anthropic/claude-sonnet-4",
"Gemini 2.5 Flash": "google/gemini-2.5-flash-preview-05-20",
}
# Progress messages - human-readable status updates
PROGRESS_MESSAGES = {
"starting": "Initializing research agents...",
"research": "Gathering intelligence from multiple sources...",
"analysis": "Analyzing competitive landscape and market position...",
"writing": "Synthesizing insights into actionable report...",
"complete": "Report ready",
}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Logging Infrastructure (for progress tracking)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ProgressTracker:
"""Tracks workflow progress for user-friendly status updates."""
def __init__(self) -> None:
self.current_stage = "starting"
self.messages: list[str] = []
def update(self, stage: str) -> None:
self.current_stage = stage
if stage in PROGRESS_MESSAGES:
self.messages.append(PROGRESS_MESSAGES[stage])
def get_status(self) -> str:
return PROGRESS_MESSAGES.get(self.current_stage, "Processing...")
class QueueHandler(logging.Handler):
"""Routes logs to progress tracker."""
def __init__(self, tracker: ProgressTracker) -> None:
super().__init__()
self.tracker = tracker
def emit(self, record: logging.LogRecord) -> None:
try:
msg = record.getMessage().lower()
if "research" in msg:
self.tracker.update("research")
elif "analysis" in msg:
self.tracker.update("analysis")
elif "writ" in msg:
self.tracker.update("writing")
except Exception:
pass
def attach_progress_tracker(tracker: ProgressTracker) -> QueueHandler:
"""Attach progress tracker to loggers."""
handler = QueueHandler(tracker)
logging.getLogger().addHandler(handler)
for name, logger_obj in logging.Logger.manager.loggerDict.items():
if name.startswith("src") and isinstance(logger_obj, logging.Logger):
logger_obj.addHandler(handler)
return handler
def detach_progress_tracker(handler: QueueHandler) -> None:
"""Remove progress tracker from loggers."""
logging.getLogger().removeHandler(handler)
for name, logger_obj in logging.Logger.manager.loggerDict.items():
if name.startswith("src") and isinstance(logger_obj, logging.Logger):
logger_obj.removeHandler(handler)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# UI Event Handlers
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def on_research_type_change(research_type: str) -> tuple[str, dict, dict]:
"""Handle research type selection β show appropriate form with MDI icon."""
meta = RESEARCH_TYPE_META.get(research_type, {})
icon = meta.get("icon", "help-circle")
desc = meta.get("desc", "")
icon_html = f'<span class="mdi mdi-{icon}" style="font-size:1.2rem;margin-right:0.5rem;"></span>'
description_html = f"{icon_html} **{research_type}**: {desc}"
is_company_analysis = research_type == "Company Analysis"
return (
description_html,
gr.update(visible=is_company_analysis),
gr.update(visible=not is_company_analysis),
)
async def run_analysis(
research_type_label: str,
company_name: str,
industry: str,
research_depth: str,
model_choice: str,
):
"""Execute market intelligence analysis with progress updates."""
if not company_name:
yield ("", "Please enter a company name", "β οΈ Missing input")
return
research_type = RESEARCH_TYPE_OPTIONS.get(
research_type_label, ResearchType.COMPANY_ANALYSIS
)
model = MODEL_OPTIONS.get(model_choice, "x-ai/grok-3-fast:free")
# Default budget from env (not user-configurable)
max_budget = float(os.getenv("MAX_BUDGET", "2.0"))
tracker = ProgressTracker()
handler = attach_progress_tracker(tracker)
tracker.update("starting")
try:
workflow = MarketIntelligenceWorkflow(
max_budget=max_budget,
model_name=model,
)
task = asyncio.create_task(
workflow.run(
company_name=company_name,
industry=industry if industry else None,
thread_id=f"ui-{datetime.now().timestamp()}",
research_depth=research_depth.lower(),
research_type=research_type,
)
)
# Stream progress updates
while not task.done():
status = tracker.get_status()
yield (status, "Generating report...", f"π {status}")
await asyncio.sleep(0.5)
result = await task
tracker.update("complete")
final_status = (
"β
Complete" if not result.get("errors") else "β Error occurred"
)
yield (
tracker.get_status(),
result.get("full_report", "No report generated"),
final_status,
)
except Exception as e:
logger.error(f"Analysis failed: {e}")
yield ("", f"Error: {e}", "β Failed")
finally:
detach_progress_tracker(handler)
def download_report(report_content: str) -> str | None:
"""Generate downloadable markdown file."""
if not report_content:
return None
with tempfile.NamedTemporaryFile(
mode="w", delete=False, suffix=".md", encoding="utf-8"
) as f:
f.write(report_content)
return f.name
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# UI Construction
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def create_ui() -> gr.Blocks:
"""Build the Gradio interface β clean, enterprise-grade."""
with gr.Blocks() as app:
# Header
gr.HTML(f"""
<link rel="stylesheet" href="{MDI_CDN}">
<div style="text-align: center; padding: 1.5rem 0; border-bottom: 1px solid #eee;">
<h1 style="margin: 0; font-weight: 600;">
<span class="mdi mdi-chart-timeline-variant"></span>
Market Intelligence
</h1>
<p style="margin: 0.5rem 0 0; color: #666;">
Enterprise-grade competitive research in minutes
</p>
</div>
""")
with gr.Row():
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Left Column: Configuration
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=1):
# F1: Research Type Selection
gr.HTML("""
<h3 style="margin-bottom: 0.5rem;">
<span class="mdi mdi-format-list-bulleted-type"></span>
What do you need?
</h3>
""")
research_type_selector = gr.Radio(
choices=list(RESEARCH_TYPE_OPTIONS.keys()),
value="Company Analysis",
label="Research Type",
show_label=False,
)
research_type_description = gr.Markdown(
value='<span class="mdi mdi-domain" style="font-size:1.2rem;margin-right:0.5rem;"></span> **Company Analysis**: Deep dive on a single company β products, positioning, SWOT'
)
# Dynamic Form: Company Analysis (F2)
with gr.Group(visible=True) as company_form:
company_input = gr.Textbox(
label="Company Name",
placeholder="e.g., Tesla, Notion, Stripe",
)
industry_input = gr.Textbox(
label="Industry (optional)",
placeholder="e.g., Electric Vehicles, SaaS",
)
research_depth = gr.Radio(
choices=["Basic", "Comprehensive"],
value="Comprehensive",
label="Research Depth",
info="Basic: faster, key insights. Comprehensive: deeper analysis.",
)
# Placeholder for F3-F7
with gr.Group(visible=False) as coming_soon:
gr.HTML("""
<div style="padding: 2rem; text-align: center; background: #f8f9fa; border-radius: 8px;">
<span class="mdi mdi-hammer-wrench" style="font-size: 2rem; color: #666;"></span>
<h3>Coming Soon</h3>
<p style="color: #666;">This research type is under development.</p>
</div>
""")
# Advanced Settings (for power users testing models)
with gr.Accordion("Advanced Settings", open=False):
model_choice = gr.Dropdown(
choices=list(MODEL_OPTIONS.keys()),
value="Grok 3 (Free)",
label="AI Model",
info="Test different models for quality comparison",
)
# Run button
run_btn = gr.Button(
"Generate Report",
variant="primary",
size="lg",
)
# Progress indicator
progress_status = gr.Textbox(
label="Status",
value="Ready",
interactive=False,
show_label=False,
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Right Column: Report Output
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=2):
gr.HTML("""
<h3 style="margin-bottom: 1rem;">
<span class="mdi mdi-file-document-outline"></span>
Report
</h3>
""")
# Single clean output area
report_display = gr.Markdown(
value="Your report will appear here after generation.",
elem_id="report-output",
)
# Download option
download_btn = gr.DownloadButton(
"Download Report (.md)",
visible=True,
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Event Wiring
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
research_type_selector.change(
fn=on_research_type_change,
inputs=[research_type_selector],
outputs=[research_type_description, company_form, coming_soon],
)
run_btn.click(
fn=run_analysis,
inputs=[
research_type_selector,
company_input,
industry_input,
research_depth,
model_choice,
],
outputs=[
progress_status,
report_display,
progress_status,
],
)
download_btn.click(
fn=download_report,
inputs=[report_display],
outputs=[download_btn],
)
return app
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Entry Point
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
is_deployment = os.getenv("SPACE_ID") or os.getenv("IS_DOCKER")
server_name = "0.0.0.0" if is_deployment else "127.0.0.1"
app = create_ui()
app.launch(server_name=server_name, server_port=7860, share=False, show_error=True)
|