Shivam4096 commited on
Commit
eddeef9
Β·
verified Β·
1 Parent(s): d160fc8

Upload folder using huggingface_hub

Browse files
builders/build_docx.py CHANGED
@@ -347,6 +347,25 @@ def build_document(data: dict, output_path: str) -> None:
347
 
348
  _swot_section(doc, data)
349
  _trends_section(doc, data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  _ratio_section(doc, data)
351
  _dupont_section(doc, data)
352
  _peer_section(doc, data)
 
347
 
348
  _swot_section(doc, data)
349
  _trends_section(doc, data)
350
+
351
+ # Visualizations Section
352
+ try:
353
+ from builders.chart_builder import generate_all_charts
354
+ charts = generate_all_charts(data)
355
+ if any(charts.values()):
356
+ _add_heading(doc, "Financial Visualizations", 1)
357
+ if charts.get("revenue_net_income"):
358
+ _add_heading(doc, "Revenue & Net Income Trend", 2)
359
+ doc.add_picture(charts["revenue_net_income"], width=Inches(6.0))
360
+ if charts.get("margins"):
361
+ _add_heading(doc, "Profitability Margins", 2)
362
+ doc.add_picture(charts["margins"], width=Inches(6.0))
363
+ if charts.get("cash_flow"):
364
+ _add_heading(doc, "Cash Flow & CAPEX", 2)
365
+ doc.add_picture(charts["cash_flow"], width=Inches(6.0))
366
+ except Exception as e:
367
+ print(f"Warning: Could not generate DOCX charts: {e}", file=sys.stderr)
368
+
369
  _ratio_section(doc, data)
370
  _dupont_section(doc, data)
371
  _peer_section(doc, data)
builders/build_pptx.py CHANGED
@@ -382,7 +382,33 @@ def build_presentation(data: Dict, output_path: str):
382
  p_c.line_spacing = 1.2
383
 
384
  # ==========================================
385
- # SLIDE 6: Key Synthesis Insights
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  # ==========================================
387
  insights_list = data.get("key_insights", [])
388
  if insights_list:
 
382
  p_c.line_spacing = 1.2
383
 
384
  # ==========================================
385
+ # SLIDE 6: Financial Visualizations
386
+ # ==========================================
387
+ try:
388
+ from builders.chart_builder import generate_all_charts
389
+ charts = generate_all_charts(data)
390
+
391
+ for chart_key, title in [
392
+ ("revenue_net_income", "Revenue & Net Income Trend"),
393
+ ("margins", "Profitability Margins"),
394
+ ("cash_flow", "Cash Flow & CAPEX Overview")
395
+ ]:
396
+ if charts.get(chart_key):
397
+ slide_chart = prs.slides.add_slide(blank_layout)
398
+ _apply_theme_decorations(slide_chart, prs)
399
+ _add_slide_title(slide_chart, title)
400
+
401
+ # Add picture centrally
402
+ slide_chart.shapes.add_picture(
403
+ charts[chart_key],
404
+ Inches(1.5), Inches(1.5), width=Inches(10.0)
405
+ )
406
+ except Exception as e:
407
+ import sys
408
+ print(f"Warning: Could not generate PPTX charts: {e}", file=sys.stderr)
409
+
410
+ # ==========================================
411
+ # SLIDE 7: Key Synthesis Insights
412
  # ==========================================
413
  insights_list = data.get("key_insights", [])
414
  if insights_list:
builders/chart_builder.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import matplotlib
3
+ matplotlib.use('Agg') # Headless mode
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ import pandas as pd
7
+ import numpy as np
8
+
9
+ # Set seaborn theme for professional looks
10
+ sns.set_theme(style="whitegrid", palette="muted")
11
+ plt.rcParams['font.family'] = 'sans-serif'
12
+ plt.rcParams['font.sans-serif'] = ['Arial', 'Helvetica', 'DejaVu Sans']
13
+
14
+ def _create_bytes_img(fig):
15
+ buf = io.BytesIO()
16
+ fig.savefig(buf, format='png', bbox_inches='tight', dpi=150)
17
+ buf.seek(0)
18
+ plt.close(fig)
19
+ return buf
20
+
21
+ def generate_revenue_net_income_chart(data: dict) -> io.BytesIO:
22
+ years = data.get("years", [])
23
+ if not years:
24
+ return None
25
+
26
+ rev = data.get("income_statement", {}).get("revenue", [])
27
+ ni = data.get("income_statement", {}).get("net_income", [])
28
+
29
+ # Pad to match years
30
+ rev = (rev + [0]*len(years))[:len(years)]
31
+ ni = (ni + [0]*len(years))[:len(years)]
32
+
33
+ # Fill None with 0 for plotting
34
+ rev = [r if r is not None else 0 for r in rev]
35
+ ni = [n if n is not None else 0 for n in ni]
36
+
37
+ fig, ax1 = plt.subplots(figsize=(8, 4.5))
38
+
39
+ x = np.arange(len(years))
40
+ width = 0.35
41
+
42
+ ax1.bar(x - width/2, rev, width, label='Revenue', color='#1F4E78')
43
+ ax1.bar(x + width/2, ni, width, label='Net Income', color='#549E39')
44
+
45
+ ax1.set_ylabel(f"Amount ({data.get('currency', 'INR')} {data.get('units_label', 'Crores')})")
46
+ ax1.set_title("Revenue & Net Income Trend")
47
+ ax1.set_xticks(x)
48
+ ax1.set_xticklabels(years)
49
+ ax1.legend()
50
+
51
+ return _create_bytes_img(fig)
52
+
53
+ def generate_margins_chart(data: dict) -> io.BytesIO:
54
+ years = data.get("years", [])
55
+ if not years:
56
+ return None
57
+
58
+ ratios = data.get("ratios", {})
59
+ gm = ratios.get("gross_margin", [])
60
+ om = ratios.get("operating_margin", [])
61
+ nm = ratios.get("net_margin", [])
62
+
63
+ gm = [(g*100) if g is not None else 0 for g in (gm + [0]*len(years))[:len(years)]]
64
+ om = [(o*100) if o is not None else 0 for o in (om + [0]*len(years))[:len(years)]]
65
+ nm = [(n*100) if n is not None else 0 for n in (nm + [0]*len(years))[:len(years)]]
66
+
67
+ fig, ax = plt.subplots(figsize=(8, 4.5))
68
+ x = np.arange(len(years))
69
+
70
+ ax.plot(x, gm, marker='o', linewidth=2, label='Gross Margin %', color='#1F4E78')
71
+ ax.plot(x, om, marker='s', linewidth=2, label='Operating Margin %', color='#455F51')
72
+ ax.plot(x, nm, marker='^', linewidth=2, label='Net Margin %', color='#549E39')
73
+
74
+ ax.set_ylabel("Margin (%)")
75
+ ax.set_title("Profitability Margins Trend")
76
+ ax.set_xticks(x)
77
+ ax.set_xticklabels(years)
78
+
79
+ # Add value labels
80
+ for i in range(len(years)):
81
+ if nm[i] != 0:
82
+ ax.annotate(f"{nm[i]:.1f}%", (x[i], nm[i]), textcoords="offset points", xytext=(0,10), ha='center')
83
+
84
+ ax.legend()
85
+
86
+ return _create_bytes_img(fig)
87
+
88
+ def generate_cash_flow_chart(data: dict) -> io.BytesIO:
89
+ years = data.get("years", [])
90
+ if not years:
91
+ return None
92
+
93
+ cf = data.get("cash_flow", {})
94
+ ocf = cf.get("cash_from_operations", [])
95
+ capex = cf.get("capex", [])
96
+ fcf = cf.get("free_cash_flow", [])
97
+
98
+ ocf = [o if o is not None else 0 for o in (ocf + [0]*len(years))[:len(years)]]
99
+ # Capex is usually negative, let's plot absolute value for magnitude comparison
100
+ capex = [abs(c) if c is not None else 0 for c in (capex + [0]*len(years))[:len(years)]]
101
+ fcf = [f if f is not None else 0 for f in (fcf + [0]*len(years))[:len(years)]]
102
+
103
+ fig, ax = plt.subplots(figsize=(8, 4.5))
104
+ x = np.arange(len(years))
105
+ width = 0.25
106
+
107
+ ax.bar(x - width, ocf, width, label='Operating Cash Flow', color='#1F4E78')
108
+ ax.bar(x, capex, width, label='CAPEX (Abs)', color='#C00000')
109
+ ax.bar(x + width, fcf, width, label='Free Cash Flow', color='#549E39')
110
+
111
+ ax.set_ylabel(f"Amount ({data.get('currency', 'INR')} {data.get('units_label', 'Crores')})")
112
+ ax.set_title("Cash Flow & CAPEX Overview")
113
+ ax.set_xticks(x)
114
+ ax.set_xticklabels(years)
115
+ ax.legend()
116
+
117
+ return _create_bytes_img(fig)
118
+
119
+ def generate_all_charts(data: dict) -> dict:
120
+ """Returns a dict mapping chart names to BytesIO pngs."""
121
+ return {
122
+ "revenue_net_income": generate_revenue_net_income_chart(data),
123
+ "margins": generate_margins_chart(data),
124
+ "cash_flow": generate_cash_flow_chart(data)
125
+ }
config.py CHANGED
@@ -15,6 +15,7 @@ GEMINI_API_KEY: str = os.environ.get("GEMINI_API_KEY", "")
15
  CLAUDE_API_KEY: str = os.environ.get("CLAUDE_API_KEY", "")
16
  OPENAI_API_KEY: str = os.environ.get("OPENAI_API_KEY", "")
17
  GROQ_API_KEY: str = os.environ.get("GROQ_API_KEY", "")
 
18
 
19
  # ── Data settings ─────────────────────────────────────────────────────────────
20
  DEFAULT_YEARS: int = 5
@@ -35,6 +36,7 @@ GEMINI_MODEL: str = "gemini-2.0-flash"
35
  CLAUDE_MODEL: str = "claude-3-5-haiku-20241022"
36
  OPENAI_MODEL: str = "gpt-4o-mini"
37
  GROQ_MODEL: str = "llama-3.3-70b-versatile" # free, high-quality
 
38
 
39
  # ── App metadata ──────────────────────────────────────────────────────────────
40
  APP_NAME: str = "Financial Analyzer"
 
15
  CLAUDE_API_KEY: str = os.environ.get("CLAUDE_API_KEY", "")
16
  OPENAI_API_KEY: str = os.environ.get("OPENAI_API_KEY", "")
17
  GROQ_API_KEY: str = os.environ.get("GROQ_API_KEY", "")
18
+ NVIDIA_API_KEY: str = os.environ.get("NVIDIA_API_KEY", "")
19
 
20
  # ── Data settings ─────────────────────────────────────────────────────────────
21
  DEFAULT_YEARS: int = 5
 
36
  CLAUDE_MODEL: str = "claude-3-5-haiku-20241022"
37
  OPENAI_MODEL: str = "gpt-4o-mini"
38
  GROQ_MODEL: str = "llama-3.3-70b-versatile" # free, high-quality
39
+ NVIDIA_MODEL: str = "meta/llama-3.1-405b-instruct"
40
 
41
  # ── App metadata ──────────────────────────────────────────────────────────────
42
  APP_NAME: str = "Financial Analyzer"
pipeline/analyst.py CHANGED
@@ -13,7 +13,7 @@ from pathlib import Path
13
  from typing import Dict, List, Optional
14
 
15
  sys.path.insert(0, str(Path(__file__).parent.parent))
16
- from config import GEMINI_API_KEY, CLAUDE_API_KEY, GEMINI_MODEL, CLAUDE_MODEL, OPENAI_API_KEY, OPENAI_MODEL, GROQ_API_KEY, GROQ_MODEL
17
 
18
 
19
  # ── Formatting helpers ────────────────────────────────────────────────────────
@@ -317,12 +317,13 @@ def _gemini_generate(context: str, section: str, log_fn=None) -> str:
317
  import google.generativeai as genai_old
318
  genai_old.configure(api_key=GEMINI_API_KEY)
319
  model = genai_old.GenerativeModel(model_name=GEMINI_MODEL, system_instruction=SYSTEM_PROMPT)
320
- response = model.generate_content(f"FINANCIAL DATA:\n{context}\n\nTASK:\n{SECTION_PROMPTS[section]}")
321
  return response.text.strip()
322
  except Exception as e2:
323
- return f"[Gemini error for {section}: {e2}]"
 
324
 
325
- return f"[Gemini unavailable for {section}: API key missing]"
326
 
327
 
328
 
@@ -344,7 +345,8 @@ def _openai_generate(context: str, section: str, log_fn=None) -> str:
344
  )
345
  return resp.choices[0].message.content.strip()
346
  except Exception as e:
347
- return f"[ChatGPT error for {section}: {e}]"
 
348
 
349
 
350
  # ── Groq (free β€” Llama 3.3 70B) ──────────────────────────────────────────────
@@ -369,7 +371,34 @@ def _groq_generate(context: str, section: str, log_fn=None) -> str:
369
  )
370
  return resp.choices[0].message.content.strip()
371
  except Exception as e:
372
- return f"[Groq error for {section}: {e}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
 
375
  # ── Claude ────────────────────────────────────────────────────────────────────
@@ -387,7 +416,8 @@ def _claude_generate(context: str, section: str, log_fn=None) -> str:
387
  )
388
  return message.content[0].text.strip()
389
  except Exception as e:
390
- return f"[Claude error for {section}: {e}]"
 
391
 
392
 
393
  # ── Parse structured sections ─────────────────────────────────────────────────
@@ -462,37 +492,76 @@ def generate_narrative(
462
  use_claude = "claude" in models and bool(CLAUDE_API_KEY)
463
  use_openai = "openai" in models and bool(OPENAI_API_KEY)
464
  use_groq = "groq" in models and bool(GROQ_API_KEY)
 
465
 
466
- if not use_gemini and not use_claude and not use_openai and not use_groq:
467
  log("No AI models configured. Running data-only analysis (no narrative).")
468
  return _empty_narrative()
469
 
470
  context = _build_context(data, meta)
471
- active = [m for m, ok in [("Gemini",use_gemini),("Claude",use_claude),("ChatGPT",use_openai),("Groq (Llama)",use_groq)] if ok]
472
  log(f"Generating AI narrative ({', '.join(active)}) ...")
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  def gen(section: str) -> str:
475
- results = []
476
- if use_gemini:
477
- log(f" Gemini: {section} ...")
478
- results.append(("Gemini", _gemini_generate(context, section, log_fn)))
479
- if use_claude:
480
- log(f" Claude: {section} ...")
481
- results.append(("Claude", _claude_generate(context, section, log_fn)))
482
- if use_openai:
483
- log(f" ChatGPT: {section} ...")
484
- results.append(("ChatGPT", _openai_generate(context, section, log_fn)))
485
- if use_groq:
486
- log(f" Groq (Llama 3.3): {section} ...")
487
- results.append(("Groq/Llama", _groq_generate(context, section, log_fn)))
488
-
489
- if len(results) == 1:
490
- return results[0][1]
491
- # Multiple models β€” combine with attribution
492
- combined = ""
493
- for name, text in results:
494
- combined += f"[{name}]\n{text}\n\n"
495
- return combined.strip()
 
 
496
 
497
  # Generate all sections
498
  log(" Generating executive summary …")
 
13
  from typing import Dict, List, Optional
14
 
15
  sys.path.insert(0, str(Path(__file__).parent.parent))
16
+ from config import GEMINI_API_KEY, CLAUDE_API_KEY, GEMINI_MODEL, CLAUDE_MODEL, OPENAI_API_KEY, OPENAI_MODEL, GROQ_API_KEY, GROQ_MODEL, NVIDIA_API_KEY, NVIDIA_MODEL
17
 
18
 
19
  # ── Formatting helpers ────────────────────────────────────────────────────────
 
317
  import google.generativeai as genai_old
318
  genai_old.configure(api_key=GEMINI_API_KEY)
319
  model = genai_old.GenerativeModel(model_name=GEMINI_MODEL, system_instruction=SYSTEM_PROMPT)
320
+ response = model.generate_content(prompt)
321
  return response.text.strip()
322
  except Exception as e2:
323
+ if log_fn: log_fn(f"[Warning] Gemini failed for {section}: {str(e2)[:80]}")
324
+ return None
325
 
326
+ return None
327
 
328
 
329
 
 
345
  )
346
  return resp.choices[0].message.content.strip()
347
  except Exception as e:
348
+ if log_fn: log_fn(f"[Warning] ChatGPT failed for {section}: {str(e)[:80]}")
349
+ return None
350
 
351
 
352
  # ── Groq (free β€” Llama 3.3 70B) ──────────────────────────────────────────────
 
371
  )
372
  return resp.choices[0].message.content.strip()
373
  except Exception as e:
374
+ if log_fn: log_fn(f"[Warning] Groq failed for {section}: {str(e)[:80]}")
375
+ return None
376
+
377
+
378
+ # ── NVIDIA NIM (Llama 3.1 405B) ──────────────────────────────────────────────
379
+
380
+ def _nvidia_generate(context: str, section: str, log_fn=None) -> Optional[str]:
381
+ """Generate using NVIDIA NIM OpenAI-compatible API."""
382
+ try:
383
+ from openai import OpenAI
384
+ client = OpenAI(
385
+ api_key=NVIDIA_API_KEY,
386
+ base_url="https://integrate.api.nvidia.com/v1",
387
+ )
388
+ prompt = f"FINANCIAL DATA:\n{context}\n\nTASK:\n{SECTION_PROMPTS[section]}"
389
+ resp = client.chat.completions.create(
390
+ model=NVIDIA_MODEL,
391
+ messages=[
392
+ {"role": "system", "content": SYSTEM_PROMPT},
393
+ {"role": "user", "content": prompt},
394
+ ],
395
+ temperature=0.2,
396
+ max_tokens=1024,
397
+ )
398
+ return resp.choices[0].message.content.strip()
399
+ except Exception as e:
400
+ if log_fn: log_fn(f"[Warning] NVIDIA NIM failed for {section}: {str(e)[:80]}")
401
+ return None
402
 
403
 
404
  # ── Claude ────────────────────────────────────────────────────────────────────
 
416
  )
417
  return message.content[0].text.strip()
418
  except Exception as e:
419
+ if log_fn: log_fn(f"[Warning] Claude failed for {section}: {str(e)[:80]}")
420
+ return None
421
 
422
 
423
  # ── Parse structured sections ─────────────────────────────────────────────────
 
492
  use_claude = "claude" in models and bool(CLAUDE_API_KEY)
493
  use_openai = "openai" in models and bool(OPENAI_API_KEY)
494
  use_groq = "groq" in models and bool(GROQ_API_KEY)
495
+ use_nvidia = "nvidia" in models and bool(NVIDIA_API_KEY)
496
 
497
+ if not any([use_gemini, use_claude, use_openai, use_groq, use_nvidia]):
498
  log("No AI models configured. Running data-only analysis (no narrative).")
499
  return _empty_narrative()
500
 
501
  context = _build_context(data, meta)
502
+ active = [m for m, ok in [("Gemini",use_gemini),("Claude",use_claude),("ChatGPT",use_openai),("Groq",use_groq),("NVIDIA",use_nvidia)] if ok]
503
  log(f"Generating AI narrative ({', '.join(active)}) ...")
504
 
505
+ def _synthesize_drafts(section: str, drafts: Dict[str, str]) -> str:
506
+ """Use the best available model to merge drafts into a single consensus response."""
507
+ # Pick the synthesizer based on perceived quality/capability
508
+ synthesizer = None
509
+ if use_openai: synthesizer = ("ChatGPT", _openai_generate)
510
+ elif use_claude: synthesizer = ("Claude", _claude_generate)
511
+ elif use_nvidia: synthesizer = ("NVIDIA", _nvidia_generate)
512
+ elif use_gemini: synthesizer = ("Gemini", _gemini_generate)
513
+ elif use_groq: synthesizer = ("Groq", _groq_generate)
514
+
515
+ if not synthesizer:
516
+ return list(drafts.values())[0]
517
+
518
+ name, fn = synthesizer
519
+ log(f" > Synthesizing {len(drafts)} drafts using {name}...")
520
+
521
+ draft_text = "\n\n".join([f"--- Draft from {agent} ---\n{text}" for agent, text in drafts.items()])
522
+ prompt = SYNTHESIS_PROMPT.format(section=section) + "\n\n" + draft_text
523
+
524
+ # We reuse the context parameter to pass the prompt, and bypass the section wrapping by mocking it
525
+ # Actually, let's just create a custom direct call for synthesis to avoid section wrapping issues.
526
+ # But to reuse the functions easily, we can temporarily override the SECTION_PROMPTS.
527
+ original_prompt = SECTION_PROMPTS.get(section, "")
528
+ SECTION_PROMPTS[section] = prompt
529
+ try:
530
+ res = fn("", section) # Context empty because prompt contains everything
531
+ finally:
532
+ SECTION_PROMPTS[section] = original_prompt
533
+
534
+ if res: return res
535
+
536
+ # If synthesis fails, fallback to simple concatenation
537
+ return "\n\n".join([f"[{a}]\n{t}" for a, t in drafts.items()])
538
+
539
+ import concurrent.futures
540
+
541
  def gen(section: str) -> str:
542
+ tasks = {}
543
+ with concurrent.futures.ThreadPoolExecutor() as executor:
544
+ if use_gemini: tasks[executor.submit(_gemini_generate, context, section)] = "Gemini"
545
+ if use_claude: tasks[executor.submit(_claude_generate, context, section)] = "Claude"
546
+ if use_openai: tasks[executor.submit(_openai_generate, context, section)] = "ChatGPT"
547
+ if use_groq: tasks[executor.submit(_groq_generate, context, section)] = "Groq"
548
+ if use_nvidia: tasks[executor.submit(_nvidia_generate, context, section)] = "NVIDIA"
549
+
550
+ drafts = {}
551
+ for future in concurrent.futures.as_completed(tasks):
552
+ name = tasks[future]
553
+ try:
554
+ res = future.result()
555
+ if res: drafts[name] = res
556
+ except Exception as e:
557
+ if log_fn: log_fn(f"[Warning] {name} thread failed: {e}")
558
+
559
+ if len(drafts) == 0:
560
+ return f"[{section}: All AI models failed to generate a response]"
561
+ elif len(drafts) == 1:
562
+ return list(drafts.values())[0]
563
+ else:
564
+ return _synthesize_drafts(section, drafts)
565
 
566
  # Generate all sections
567
  log(" Generating executive summary …")
pipeline/key_validator.py CHANGED
@@ -133,9 +133,34 @@ def validate_groq(key: str) -> tuple:
133
  return False, f"Error: {msg[:80]}"
134
 
135
 
136
- def check_any_ai_model(gemini_key: str = "", claude_key: str = "", openai_key: str = "", groq_key: str = "") -> tuple:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  """Quickly check if at least one AI model is usable."""
138
  checks = [
 
139
  ("groq", groq_key, validate_groq),
140
  ("gemini", gemini_key, validate_gemini),
141
  ("claude", claude_key, validate_claude),
@@ -150,6 +175,7 @@ def check_any_ai_model(gemini_key: str = "", claude_key: str = "", openai_key: s
150
  return False, (
151
  "No working AI model key found.\n\n"
152
  "Free options (no credit card needed):\n"
 
153
  " β€’ Groq (Llama 3.3 70B) β†’ console.groq.com\n"
154
  " β€’ Gemini β†’ aistudio.google.com\n\n"
155
  "Paid options:\n"
 
133
  return False, f"Error: {msg[:80]}"
134
 
135
 
136
+ def validate_nvidia(key: str) -> tuple:
137
+ """Test NVIDIA NIM key with a minimal chat completion."""
138
+ if not key or key.strip() in ("", "your_nvidia_key_here"):
139
+ return False, "No key entered."
140
+ try:
141
+ from openai import OpenAI
142
+ client = OpenAI(
143
+ api_key=key.strip(),
144
+ base_url="https://integrate.api.nvidia.com/v1",
145
+ )
146
+ resp = client.chat.completions.create(
147
+ model="meta/llama-3.1-405b-instruct",
148
+ messages=[{"role": "user", "content": "Reply with the single word: OK"}],
149
+ max_tokens=5,
150
+ )
151
+ text = resp.choices[0].message.content.strip() if resp.choices else ""
152
+ return True, f"Valid βœ“ (NVIDIA NIM, response: '{text[:20]}')"
153
+ except Exception as e:
154
+ msg = str(e)
155
+ if "401" in msg or "invalid_api_key" in msg.lower() or "unauthorized" in msg.lower():
156
+ return False, "Invalid NVIDIA API key. Check and re-enter."
157
+ return False, f"Error: {msg[:80]}"
158
+
159
+
160
+ def check_any_ai_model(gemini_key: str = "", claude_key: str = "", openai_key: str = "", groq_key: str = "", nvidia_key: str = "") -> tuple:
161
  """Quickly check if at least one AI model is usable."""
162
  checks = [
163
+ ("nvidia", nvidia_key, validate_nvidia),
164
  ("groq", groq_key, validate_groq),
165
  ("gemini", gemini_key, validate_gemini),
166
  ("claude", claude_key, validate_claude),
 
175
  return False, (
176
  "No working AI model key found.\n\n"
177
  "Free options (no credit card needed):\n"
178
+ " β€’ NVIDIA NIM (Free credits) β†’ build.nvidia.com\n"
179
  " β€’ Groq (Llama 3.3 70B) β†’ console.groq.com\n"
180
  " β€’ Gemini β†’ aistudio.google.com\n\n"
181
  "Paid options:\n"
requirements.txt CHANGED
@@ -10,3 +10,5 @@ python-docx>=1.1.0
10
  pandas>=2.0.0
11
  Pillow>=10.0.0
12
  yfinance>=0.2.40
 
 
 
10
  pandas>=2.0.0
11
  Pillow>=10.0.0
12
  yfinance>=0.2.40
13
+ matplotlib
14
+ seaborn
web_app/app.py CHANGED
@@ -134,6 +134,18 @@ st.sidebar.markdown("""
134
  </div>
135
  """, unsafe_allow_html=True)
136
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  st.sidebar.markdown("""
138
  **Groq** *(Llama 3.3 70B β€” fast & free)*
139
  <a href='https://console.groq.com/keys' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key β†’ console.groq.com</a>
@@ -200,7 +212,36 @@ with col1:
200
  st.markdown("<div class='glass-card'>", unsafe_allow_html=True)
201
  st.subheader("Analysis Parameters")
202
 
203
- ticker_input = st.text_input("Company Name or Stock Ticker", placeholder="e.g. TCS, Reliance, AAPL, MSFT, INFY", help="Resolves directly via yfinance and FMP")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  col_years, col_refresh = st.columns([1, 1])
206
  with col_years:
@@ -219,8 +260,17 @@ with col1:
219
  _hint = "<span style='font-size:0.72rem;color:#9CA3AF;'>(add key in sidebar β†’)</span>"
220
 
221
  engines = []
 
 
 
 
 
 
 
 
 
222
  if st.checkbox(
223
- "πŸ†“ Groq β€” Llama 3.3 70B (recommended)",
224
  value=bool(groq_key),
225
  disabled=not groq_key,
226
  help="Free. Get key at console.groq.com"
@@ -284,24 +334,30 @@ if start_btn:
284
  config.CLAUDE_API_KEY = claude_key
285
  config.OPENAI_API_KEY = openai_key
286
  config.GROQ_API_KEY = groq_key
 
287
 
288
  pipeline.analyst.GEMINI_API_KEY = gemini_key
289
  pipeline.analyst.CLAUDE_API_KEY = claude_key
290
  pipeline.analyst.OPENAI_API_KEY = openai_key
291
  pipeline.analyst.GROQ_API_KEY = groq_key
 
292
 
293
  pipeline.key_validator.GEMINI_API_KEY = gemini_key
294
  pipeline.key_validator.CLAUDE_API_KEY = claude_key
295
  pipeline.key_validator.OPENAI_API_KEY = openai_key
 
 
296
 
297
  # Push into environment for any sub-process
298
  for k, v in [("GEMINI_API_KEY", gemini_key), ("CLAUDE_API_KEY", claude_key),
299
- ("OPENAI_API_KEY", openai_key), ("GROQ_API_KEY", groq_key)]:
 
300
  if v: os.environ[k] = v
301
 
302
  # Validate that selected engines have keys
303
  failed_validation = False
304
  engine_key_map = {
 
305
  "groq": (groq_key, "Groq"),
306
  "gemini": (gemini_key, "Gemini"),
307
  "claude": (claude_key, "Claude"),
@@ -406,30 +462,54 @@ if start_btn:
406
  # ── Generate download buttons in UI
407
  with download_container.container():
408
  st.success("Download ready!")
409
- col_btn1, col_btn2, col_btn3 = st.columns(3)
 
 
410
 
411
  if excel_bytes:
412
- col_btn1.download_button(
413
- label="πŸ“₯ Download Excel Workbook",
414
  data=excel_bytes,
415
- file_name=f"{ticker_safe}_Analysis_{timestamp_suffix}.xlsx",
416
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
 
417
  )
 
 
418
  if word_bytes:
419
- col_btn2.download_button(
420
- label="πŸ“₯ Download Word Report",
421
  data=word_bytes,
422
- file_name=f"{ticker_safe}_Report_{timestamp_suffix}.docx",
423
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
 
424
  )
 
 
425
  if pptx_bytes:
426
- col_btn3.download_button(
427
- label="πŸ“₯ Download PowerPoint Deck",
428
  data=pptx_bytes,
429
- file_name=f"{ticker_safe}_Presentation_{timestamp_suffix}.pptx",
430
- mime="application/vnd.openxmlformats-officedocument.presentationml.presentation"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  )
432
-
433
  # Write search activity log to audit table
434
  selected_formats = [f for f, ok in [("xlsx", chk_excel), ("docx", chk_word), ("pptx", chk_pptx)] if ok]
435
  save_audit_log(
 
134
  </div>
135
  """, unsafe_allow_html=True)
136
 
137
+ st.sidebar.markdown("""
138
+ **NVIDIA NIM** *(Llama 3.1 405B / Nemotron)*
139
+ <a href='https://build.nvidia.com/explore/discover' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key β†’ build.nvidia.com</a>
140
+ """, unsafe_allow_html=True)
141
+ nvidia_key = st.sidebar.text_input(
142
+ "NVIDIA API Key", type="password",
143
+ placeholder="nvapi-…",
144
+ value=os.environ.get("NVIDIA_API_KEY", ""),
145
+ help="Free credits available. Sign up at build.nvidia.com.",
146
+ key="inp_nvidia"
147
+ )
148
+
149
  st.sidebar.markdown("""
150
  **Groq** *(Llama 3.3 70B β€” fast & free)*
151
  <a href='https://console.groq.com/keys' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key β†’ console.groq.com</a>
 
212
  st.markdown("<div class='glass-card'>", unsafe_allow_html=True)
213
  st.subheader("Analysis Parameters")
214
 
215
+ # ── Ticker Search & Suggestion Logic ──
216
+ search_col1, search_col2 = st.columns([3, 1])
217
+ with search_col1:
218
+ search_query = st.text_input("Search Company Name or Ticker", placeholder="e.g. TCS, Reliance, AAPL, MSFT")
219
+ with search_col2:
220
+ st.markdown("<div style='margin-top:28px;'></div>", unsafe_allow_html=True)
221
+ search_clicked = st.button("πŸ” Find", use_container_width=True)
222
+
223
+ ticker_input = search_query # default if no suggestions
224
+
225
+ if search_clicked and search_query:
226
+ with st.spinner("Searching..."):
227
+ try:
228
+ import yfinance as yf
229
+ res = yf.Search(search_query, max_results=8)
230
+ quotes = [q for q in res.quotes if q.get("quoteType") in ("EQUITY", "MUTUALFUND", "ETF")]
231
+ st.session_state["ticker_suggestions"] = quotes
232
+ except Exception:
233
+ st.session_state["ticker_suggestions"] = []
234
+
235
+ if "ticker_suggestions" in st.session_state and st.session_state["ticker_suggestions"]:
236
+ options = {}
237
+ for q in st.session_state["ticker_suggestions"]:
238
+ name = q.get('longname') or q.get('shortname') or q.get('symbol')
239
+ label = f"{name} ({q['symbol']}) - {q.get('exchange', 'Unknown')}"
240
+ options[label] = q['symbol']
241
+
242
+ options[f"Use exact input: '{search_query}'"] = search_query
243
+ selected_label = st.selectbox("Confirm Company:", list(options.keys()))
244
+ ticker_input = options[selected_label]
245
 
246
  col_years, col_refresh = st.columns([1, 1])
247
  with col_years:
 
260
  _hint = "<span style='font-size:0.72rem;color:#9CA3AF;'>(add key in sidebar β†’)</span>"
261
 
262
  engines = []
263
+
264
+ if st.checkbox(
265
+ "πŸ†“ NVIDIA NIM β€” Llama 3.1 405B",
266
+ value=bool(nvidia_key),
267
+ disabled=not nvidia_key,
268
+ help="Free credits. Get key at build.nvidia.com"
269
+ ):
270
+ engines.append("nvidia")
271
+
272
  if st.checkbox(
273
+ "πŸ†“ Groq β€” Llama 3.3 70B",
274
  value=bool(groq_key),
275
  disabled=not groq_key,
276
  help="Free. Get key at console.groq.com"
 
334
  config.CLAUDE_API_KEY = claude_key
335
  config.OPENAI_API_KEY = openai_key
336
  config.GROQ_API_KEY = groq_key
337
+ config.NVIDIA_API_KEY = nvidia_key
338
 
339
  pipeline.analyst.GEMINI_API_KEY = gemini_key
340
  pipeline.analyst.CLAUDE_API_KEY = claude_key
341
  pipeline.analyst.OPENAI_API_KEY = openai_key
342
  pipeline.analyst.GROQ_API_KEY = groq_key
343
+ pipeline.analyst.NVIDIA_API_KEY = nvidia_key
344
 
345
  pipeline.key_validator.GEMINI_API_KEY = gemini_key
346
  pipeline.key_validator.CLAUDE_API_KEY = claude_key
347
  pipeline.key_validator.OPENAI_API_KEY = openai_key
348
+ pipeline.key_validator.GROQ_API_KEY = groq_key
349
+ pipeline.key_validator.NVIDIA_API_KEY = nvidia_key
350
 
351
  # Push into environment for any sub-process
352
  for k, v in [("GEMINI_API_KEY", gemini_key), ("CLAUDE_API_KEY", claude_key),
353
+ ("OPENAI_API_KEY", openai_key), ("GROQ_API_KEY", groq_key),
354
+ ("NVIDIA_API_KEY", nvidia_key)]:
355
  if v: os.environ[k] = v
356
 
357
  # Validate that selected engines have keys
358
  failed_validation = False
359
  engine_key_map = {
360
+ "nvidia": (nvidia_key, "NVIDIA"),
361
  "groq": (groq_key, "Groq"),
362
  "gemini": (gemini_key, "Gemini"),
363
  "claude": (claude_key, "Claude"),
 
462
  # ── Generate download buttons in UI
463
  with download_container.container():
464
  st.success("Download ready!")
465
+ dl_col1, dl_col2, dl_col3, dl_col4 = st.columns(4)
466
+
467
+ download_files = []
468
 
469
  if excel_bytes:
470
+ dl_col1.download_button(
471
+ label="πŸ“₯ Download Excel",
472
  data=excel_bytes,
473
+ file_name=f"{ticker_safe}_analysis.xlsx",
474
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
475
+ use_container_width=True
476
  )
477
+ download_files.append((f"{ticker_safe}_analysis.xlsx", excel_bytes))
478
+
479
  if word_bytes:
480
+ dl_col2.download_button(
481
+ label="πŸ“„ Download Word",
482
  data=word_bytes,
483
+ file_name=f"{ticker_safe}_report.docx",
484
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
485
+ use_container_width=True
486
  )
487
+ download_files.append((f"{ticker_safe}_report.docx", word_bytes))
488
+
489
  if pptx_bytes:
490
+ dl_col3.download_button(
491
+ label="πŸ“Š Download PPTX",
492
  data=pptx_bytes,
493
+ file_name=f"{ticker_safe}_deck.pptx",
494
+ mime="application/vnd.openxmlformats-officedocument.presentationml.presentation",
495
+ use_container_width=True
496
+ )
497
+ download_files.append((f"{ticker_safe}_deck.pptx", pptx_bytes))
498
+
499
+ if len(download_files) > 1:
500
+ import zipfile
501
+ import io
502
+ zip_buffer = io.BytesIO()
503
+ with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
504
+ for file_name, data in download_files:
505
+ zip_file.writestr(file_name, data)
506
+ dl_col4.download_button(
507
+ label="πŸ“¦ Download All (ZIP)",
508
+ data=zip_buffer.getvalue(),
509
+ file_name=f"{ticker_safe}_all_reports.zip",
510
+ mime="application/zip",
511
+ use_container_width=True
512
  )
 
513
  # Write search activity log to audit table
514
  selected_formats = [f for f, ok in [("xlsx", chk_excel), ("docx", chk_word), ("pptx", chk_pptx)] if ok]
515
  save_audit_log(
web_app/requirements.txt CHANGED
@@ -1,16 +1,18 @@
1
- streamlit>=1.30.0
2
- python-pptx>=0.6.21
3
- supabase==2.4.0
4
- requests>=2.31.0
5
- beautifulsoup4>=4.12.0
6
- lxml>=4.9.0
7
- python-dotenv>=1.0.0
8
- google-generativeai>=0.7.0
9
- google-genai>=0.8.0
10
- anthropic>=0.25.0
11
- openai>=1.0.0
12
- openpyxl>=3.1.0
13
- python-docx>=1.1.0
14
- pandas>=2.0.0
15
- Pillow>=10.0.0
16
- yfinance>=0.2.40
 
 
 
1
+ streamlit
2
+ python-pptx
3
+ supabase
4
+ requests
5
+ beautifulsoup4
6
+ lxml
7
+ python-dotenv
8
+ google-generativeai
9
+ google-genai
10
+ anthropic
11
+ openai
12
+ openpyxl
13
+ python-docx
14
+ pandas
15
+ Pillow
16
+ yfinance
17
+ matplotlib
18
+ seaborn