Shivam4096 commited on
Commit
2a29b09
Β·
verified Β·
1 Parent(s): f578f1f

Upload folder using huggingface_hub

Browse files
builders/build_excel.py CHANGED
@@ -628,7 +628,6 @@ def build_workbook(data: dict, output_path: str) -> None:
628
  _growth_sheet(wb, data)
629
  _peers_sheet(wb, data)
630
  _swot_sheet(wb, data)
631
- _narrative_sheet(wb, data)
632
  _sources_sheet(wb, data)
633
  wb.save(output_path)
634
 
 
628
  _growth_sheet(wb, data)
629
  _peers_sheet(wb, data)
630
  _swot_sheet(wb, data)
 
631
  _sources_sheet(wb, data)
632
  wb.save(output_path)
633
 
pipeline/fetcher.py CHANGED
@@ -1021,6 +1021,11 @@ def fetch_peer_snapshot(ticker: str) -> Optional[Dict]:
1021
  key_stats = t.key_stats.get(ticker, {})
1022
  fin_data = t.financial_data.get(ticker, {})
1023
  summary_det = t.summary_detail.get(ticker, {})
 
 
 
 
 
1024
  name = price_data.get("longName") or price_data.get("shortName", "")
1025
  if not name: return None
1026
  rev = None
@@ -1053,7 +1058,34 @@ def fetch_peer_snapshot(ticker: str) -> Optional[Dict]:
1053
  "ev_ebitda": _s(key_stats.get("enterpriseToEbitda")),
1054
  }
1055
  except Exception:
1056
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
 
1058
 
1059
  def fetch_valuation(ticker: str) -> Dict:
 
1021
  key_stats = t.key_stats.get(ticker, {})
1022
  fin_data = t.financial_data.get(ticker, {})
1023
  summary_det = t.summary_detail.get(ticker, {})
1024
+
1025
+ # Handle yahooquery invalid crumb string returns
1026
+ if isinstance(price_data, str) or isinstance(profile_data, str):
1027
+ raise ValueError("yahooquery returned string instead of dict (Invalid Crumb)")
1028
+
1029
  name = price_data.get("longName") or price_data.get("shortName", "")
1030
  if not name: return None
1031
  rev = None
 
1058
  "ev_ebitda": _s(key_stats.get("enterpriseToEbitda")),
1059
  }
1060
  except Exception:
1061
+ # Fallback to FMP if yahooquery fails and API key is present
1062
+ try:
1063
+ profile = (_fmp_get(f"/v3/profile/{ticker}") or [{}])[0]
1064
+ name = profile.get("companyName")
1065
+ if not name: return None
1066
+
1067
+ rtm = (_fmp_get(f"/v3/ratios-ttm/{ticker}") or [{}])[0]
1068
+ km = (_fmp_get(f"/v3/key-metrics-ttm/{ticker}") or [{}])[0]
1069
+
1070
+ return {
1071
+ "name": name, "ticker": ticker,
1072
+ "rationale": f"Industry peer ({profile.get('industry', '')})",
1073
+ "latest_year": "", "currency": profile.get("currency", "USD"),
1074
+ "units": "actuals", "revenue": None,
1075
+ "gross_margin": _s(rtm.get("grossProfitMarginTTM")),
1076
+ "operating_margin": _s(rtm.get("operatingProfitMarginTTM")),
1077
+ "net_margin": _s(rtm.get("netProfitMarginTTM")),
1078
+ "roe": _s(rtm.get("returnOnEquityTTM")),
1079
+ "roic": _s(rtm.get("returnOnCapitalEmployedTTM")),
1080
+ "debt_to_equity": _s(rtm.get("debtEquityRatioTTM")),
1081
+ "current_ratio": _s(rtm.get("currentRatioTTM")),
1082
+ "fcf_margin": None,
1083
+ "revenue_growth_3y_cagr": None,
1084
+ "pe_ttm": _s(rtm.get("peRatioTTM")),
1085
+ "ev_ebitda": _s(km.get("enterpriseValueOverEBITDATTM")),
1086
+ }
1087
+ except Exception:
1088
+ return None
1089
 
1090
 
1091
  def fetch_valuation(ticker: str) -> Dict:
pipeline/peer_selector.py CHANGED
@@ -67,60 +67,161 @@ def _fmp_peers(ticker: str) -> List[str]:
67
  return []
68
 
69
 
70
- def _hf_intelligent_peers(meta: CompanyMeta, log_fn=None) -> List[str]:
71
- """Use Hugging Face Serverless API to intelligently select granular peers."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def log(msg):
73
  if log_fn: log_fn(msg)
74
-
75
- hf_key = getattr(config, "HF_API_KEY", "")
76
- if not hf_key:
77
- return []
78
-
79
- log(" -> Hugging Face API key detected. Using AI for granular sector breakdown and peer selection...")
80
  desc = meta.description if meta.description else "No description available."
81
- try:
82
- from huggingface_hub import InferenceClient
83
- client = InferenceClient(api_key=hf_key)
84
-
85
- prompt = (
86
- f"You are a top-tier financial analyst. Given the following company, break down its exact sub-sector "
87
- f"and identify its 5 closest publicly traded peers.\n"
88
- f"Company Name: {meta.name}\n"
89
- f"Industry: {meta.industry}\n"
90
- f"Description: {desc}\n\n"
91
- f"Return ONLY a valid JSON array of their Yahoo Finance ticker symbols (e.g., [\"MSFT\", \"GOOGL\"]). "
92
- f"Do not include any other text, markdown, or explanation. Only the JSON array."
93
- )
94
-
95
- response = client.chat_completion(
96
- model="meta-llama/Meta-Llama-3-8B-Instruct",
97
- messages=[{"role": "user", "content": prompt}],
98
- max_tokens=100,
99
- temperature=0.1
100
- )
101
-
102
- if response and response.choices:
103
- text = response.choices[0].message.content
104
-
105
- # Extract JSON array using regex
106
- import re, json
107
- # Match anything between [ and ] inclusive, across multiple lines
108
- match = re.search(r'\[\s*(.*?)\s*\]', text, re.DOTALL)
109
- if match:
110
- arr_str = match.group(0)
111
- try:
112
- tickers = json.loads(arr_str)
113
- if isinstance(tickers, list) and all(isinstance(t, str) for t in tickers):
114
- log(f" [OK] HF AI successfully identified highly specific peers: {tickers}")
115
- return tickers
116
- except json.JSONDecodeError as e:
117
- log(f" ⚠ HF AI JSON parse error: {e}")
118
- log(" ⚠ HF AI returned invalid format, falling back...")
119
- else:
120
- log(f" ⚠ HF API Error. Falling back...")
121
- except Exception as e:
122
- log(f" ⚠ HF AI Request Failed: {e}. Falling back...")
123
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  return []
125
 
126
 
@@ -142,8 +243,8 @@ def select_peers(
142
 
143
  log(f"Selecting peers for {meta.name} ({meta.industry}) ...")
144
 
145
- # 1. Try Hugging Face Intelligent Peers
146
- peer_tickers = _hf_intelligent_peers(meta, log_fn)
147
 
148
  # 2. Try FMP peers endpoint if HF fails
149
  if not peer_tickers and FMP_API_KEY and meta.is_listed and ticker != "UNLISTED":
@@ -151,28 +252,43 @@ def select_peers(
151
  if peer_tickers:
152
  log(f" -> FMP suggested {len(peer_tickers)} peers")
153
 
154
- # 2. Fallback to hardcoded industry map
155
  if not peer_tickers:
156
- industry = meta.industry or meta.sector or ""
157
- # Check Indian peers first
158
- for key, tickers in INDIAN_PEERS.items():
159
- if key.lower() in industry.lower() or industry.lower() in key.lower():
160
- peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
161
- log(f" -> Using hardcoded Indian peers for '{key}'")
162
- break
163
- # Check global peers
164
- if not peer_tickers:
165
- for key, tickers in GLOBAL_PEERS.items():
166
  if key.lower() in industry.lower() or industry.lower() in key.lower():
167
  peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
168
- log(f" -> Using hardcoded global peers for '{key}'")
169
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  # Remove self
172
  peer_tickers = [t for t in peer_tickers if t.upper() != ticker.upper()][:max_peers]
173
 
174
  if not peer_tickers:
175
- log(" ⚠ No peers found. Peer comparison section will be skipped.")
176
  return []
177
 
178
  # 3. Fetch snapshots for each peer
 
67
  return []
68
 
69
 
70
+ def _parse_tickers_json(text: str) -> Optional[List[str]]:
71
+ """Parse JSON array of tickers from LLM response text."""
72
+ import re, json
73
+ # Match anything between [ and ] inclusive, across multiple lines
74
+ match = re.search(r'\[\s*(.*?)\s*\]', text, re.DOTALL)
75
+ if match:
76
+ arr_str = match.group(0)
77
+ try:
78
+ tickers = json.loads(arr_str)
79
+ if isinstance(tickers, list) and all(isinstance(t, str) for t in tickers):
80
+ return tickers
81
+ except Exception:
82
+ pass
83
+ return None
84
+
85
+
86
+ def _llm_intelligent_peers(meta: CompanyMeta, log_fn=None) -> List[str]:
87
+ """Use the first available LLM API (Gemini, Claude, OpenAI, Groq, or Hugging Face) to select peers."""
88
  def log(msg):
89
  if log_fn: log_fn(msg)
90
+
 
 
 
 
 
91
  desc = meta.description if meta.description else "No description available."
92
+ prompt = (
93
+ f"You are a top-tier financial analyst. Given the following company, break down its exact sub-sector "
94
+ f"and identify its 5 closest publicly traded peers.\n"
95
+ f"Company Name: {meta.name}\n"
96
+ f"Ticker: {meta.ticker}\n"
97
+ f"Industry: {meta.industry}\n"
98
+ f"Description: {desc}\n\n"
99
+ f"Return ONLY a valid JSON array of their Yahoo Finance ticker symbols (e.g., [\"MSFT\", \"GOOGL\"]). "
100
+ f"CRITICAL: The peers MUST be traded on the same exchange as the company. "
101
+ f"If the company's ticker has a suffix (e.g., .NS for India, .L for London), you MUST append the exact same suffix to all peer tickers.\n"
102
+ f"Do not include any other text, markdown, or explanation. Only the JSON array."
103
+ )
104
+
105
+ # 1. Try Gemini
106
+ gemini_key = getattr(config, "GEMINI_API_KEY", "")
107
+ if gemini_key:
108
+ log(" -> Gemini API key detected. Using Gemini for granular sector breakdown and peer selection...")
109
+ try:
110
+ gemini_model = getattr(config, "GEMINI_MODEL", "gemini-2.0-flash")
111
+ # Try new google-genai SDK
112
+ try:
113
+ from google import genai
114
+ from google.genai import types as gtypes
115
+ client = genai.Client(api_key=gemini_key)
116
+ response = client.models.generate_content(
117
+ model=gemini_model,
118
+ contents=prompt,
119
+ config=gtypes.GenerateContentConfig(temperature=0.1, max_output_tokens=100),
120
+ )
121
+ tickers = _parse_tickers_json(response.text)
122
+ if tickers:
123
+ log(f" [OK] Gemini successfully identified peers: {tickers}")
124
+ return tickers
125
+ except Exception:
126
+ pass
127
+
128
+ # Fallback to old SDK
129
+ import google.generativeai as genai_old
130
+ genai_old.configure(api_key=gemini_key)
131
+ model = genai_old.GenerativeModel(model_name=gemini_model)
132
+ response = model.generate_content(prompt)
133
+ tickers = _parse_tickers_json(response.text)
134
+ if tickers:
135
+ log(f" [OK] Gemini (old SDK) successfully identified peers: {tickers}")
136
+ return tickers
137
+ except Exception as e:
138
+ log(f" [Warning] Gemini peer selection failed: {e}")
139
+
140
+ # 2. Try Claude
141
+ claude_key = getattr(config, "CLAUDE_API_KEY", "")
142
+ if claude_key:
143
+ log(" -> Claude API key detected. Using Claude for granular sector breakdown and peer selection...")
144
+ try:
145
+ import anthropic
146
+ client = anthropic.Anthropic(api_key=claude_key)
147
+ claude_model = getattr(config, "CLAUDE_MODEL", "claude-3-5-haiku-20241022")
148
+ message = client.messages.create(
149
+ model=claude_model,
150
+ max_tokens=100,
151
+ temperature=0.1,
152
+ messages=[{"role": "user", "content": prompt}],
153
+ )
154
+ tickers = _parse_tickers_json(message.content[0].text)
155
+ if tickers:
156
+ log(f" [OK] Claude successfully identified peers: {tickers}")
157
+ return tickers
158
+ except Exception as e:
159
+ log(f" [Warning] Claude peer selection failed: {e}")
160
+
161
+ # 3. Try OpenAI
162
+ openai_key = getattr(config, "OPENAI_API_KEY", "")
163
+ if openai_key:
164
+ log(" -> OpenAI API key detected. Using OpenAI for granular sector breakdown and peer selection...")
165
+ try:
166
+ from openai import OpenAI
167
+ client = OpenAI(api_key=openai_key)
168
+ openai_model = getattr(config, "OPENAI_MODEL", "gpt-4o-mini")
169
+ resp = client.chat.completions.create(
170
+ model=openai_model,
171
+ messages=[{"role": "user", "content": prompt}],
172
+ temperature=0.1,
173
+ max_tokens=100,
174
+ )
175
+ tickers = _parse_tickers_json(resp.choices[0].message.content)
176
+ if tickers:
177
+ log(f" [OK] OpenAI successfully identified peers: {tickers}")
178
+ return tickers
179
+ except Exception as e:
180
+ log(f" [Warning] OpenAI peer selection failed: {e}")
181
+
182
+ # 4. Try Groq
183
+ groq_key = getattr(config, "GROQ_API_KEY", "")
184
+ if groq_key:
185
+ log(" -> Groq API key detected. Using Groq for granular sector breakdown and peer selection...")
186
+ try:
187
+ from openai import OpenAI
188
+ client = OpenAI(api_key=groq_key, base_url="https://api.groq.com/openai/v1")
189
+ groq_model = getattr(config, "GROQ_MODEL", "llama-3.3-70b-versatile")
190
+ resp = client.chat.completions.create(
191
+ model=groq_model,
192
+ messages=[{"role": "user", "content": prompt}],
193
+ temperature=0.1,
194
+ max_tokens=100,
195
+ )
196
+ tickers = _parse_tickers_json(resp.choices[0].message.content)
197
+ if tickers:
198
+ log(f" [OK] Groq successfully identified peers: {tickers}")
199
+ return tickers
200
+ except Exception as e:
201
+ log(f" [Warning] Groq peer selection failed: {e}")
202
+
203
+ # 5. Try Hugging Face (Free-tier model Qwen/Qwen2.5-7B-Instruct)
204
+ hf_key = getattr(config, "HF_API_KEY", "")
205
+ if hf_key:
206
+ log(" -> Hugging Face API key detected. Using HF Qwen/Qwen2.5-7B-Instruct for peer selection...")
207
+ try:
208
+ from huggingface_hub import InferenceClient
209
+ client = InferenceClient(api_key=hf_key)
210
+ response = client.chat_completion(
211
+ model="Qwen/Qwen2.5-7B-Instruct",
212
+ messages=[{"role": "user", "content": prompt}],
213
+ max_tokens=100,
214
+ temperature=0.1
215
+ )
216
+ if response and response.choices:
217
+ tickers = _parse_tickers_json(response.choices[0].message.content)
218
+ if tickers:
219
+ log(f" [OK] Hugging Face successfully identified peers: {tickers}")
220
+ return tickers
221
+ log(" [Warning] Hugging Face response parsing failed.")
222
+ except Exception as e:
223
+ log(f" [Warning] Hugging Face peer selection failed: {e}")
224
+
225
  return []
226
 
227
 
 
243
 
244
  log(f"Selecting peers for {meta.name} ({meta.industry}) ...")
245
 
246
+ # 1. Try LLM-based Intelligent Peers (Gemini, Claude, OpenAI, Groq, or HF)
247
+ peer_tickers = _llm_intelligent_peers(meta, log_fn)
248
 
249
  # 2. Try FMP peers endpoint if HF fails
250
  if not peer_tickers and FMP_API_KEY and meta.is_listed and ticker != "UNLISTED":
 
252
  if peer_tickers:
253
  log(f" -> FMP suggested {len(peer_tickers)} peers")
254
 
255
+ # 2. Fallback to hardcoded industry/sector map
256
  if not peer_tickers:
257
+ industry = meta.industry or ""
258
+ # Try specific industry match first
259
+ if industry:
260
+ for key, tickers in INDIAN_PEERS.items():
 
 
 
 
 
 
261
  if key.lower() in industry.lower() or industry.lower() in key.lower():
262
  peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
263
+ log(f" -> Using hardcoded Indian peers for industry '{key}'")
264
  break
265
+ if not peer_tickers:
266
+ for key, tickers in GLOBAL_PEERS.items():
267
+ if key.lower() in industry.lower() or industry.lower() in key.lower():
268
+ peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
269
+ log(f" -> Using hardcoded global peers for industry '{key}'")
270
+ break
271
+ # Try general sector match if industry didn't match anything
272
+ if not peer_tickers:
273
+ sector = meta.sector or ""
274
+ if sector:
275
+ for key, tickers in INDIAN_PEERS.items():
276
+ if key.lower() in sector.lower() or sector.lower() in key.lower():
277
+ peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
278
+ log(f" -> Using hardcoded Indian peers for sector '{key}'")
279
+ break
280
+ if not peer_tickers:
281
+ for key, tickers in GLOBAL_PEERS.items():
282
+ if key.lower() in sector.lower() or sector.lower() in key.lower():
283
+ peer_tickers = [t for t in tickers if t.upper() != ticker.upper()][:max_peers]
284
+ log(f" -> Using hardcoded global peers for sector '{key}'")
285
+ break
286
 
287
  # Remove self
288
  peer_tickers = [t for t in peer_tickers if t.upper() != ticker.upper()][:max_peers]
289
 
290
  if not peer_tickers:
291
+ log(" [Warning] No peers found. Peer comparison section will be skipped.")
292
  return []
293
 
294
  # 3. Fetch snapshots for each peer
pipeline/resolver.py CHANGED
@@ -153,13 +153,16 @@ INDIAN_TICKERS = {
153
  def _guess_ticker(query: str) -> Optional[str]:
154
  """Try to find an Indian NSE ticker for a given name/query."""
155
  q = query.lower().strip()
 
 
 
 
156
  # Exact match
 
 
157
  if q in INDIAN_TICKERS:
158
  return INDIAN_TICKERS[q]
159
- # Partial match
160
- for key, ticker in INDIAN_TICKERS.items():
161
- if key in q or q in key:
162
- return ticker
163
  return None
164
 
165
 
 
153
  def _guess_ticker(query: str) -> Optional[str]:
154
  """Try to find an Indian NSE ticker for a given name/query."""
155
  q = query.lower().strip()
156
+
157
+ # Clean up standard suffixes for safer matching
158
+ clean_q = q.replace(".ns", "").replace(".bo", "").replace(" ltd", "").replace(" limited", "").strip()
159
+
160
  # Exact match
161
+ if clean_q in INDIAN_TICKERS:
162
+ return INDIAN_TICKERS[clean_q]
163
  if q in INDIAN_TICKERS:
164
  return INDIAN_TICKERS[q]
165
+
 
 
 
166
  return None
167
 
168
 
supabase_webhook_guide.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ”Œ Supabase Audit Webhook Integration Guide
2
+
3
+ This guide explains how to configure Supabase Database Webhooks to send real-time alerts (e.g., to Slack, MS Teams, or Discord) whenever a user performs a financial statement analysis and a new entry is added to the `financial_analysis_audit` table.
4
+
5
+ ---
6
+
7
+ ## 1. Supabase Dashboard Configuration
8
+
9
+ Follow these steps to enable the database webhook directly in the Supabase Dashboard:
10
+
11
+ 1. **Navigate to Database Webhooks**:
12
+ - Go to your [Supabase Project Dashboard](https://supabase.com/dashboard).
13
+ - Select your project, click on **Database** in the left sidebar, and click on **Webhooks** (or go to **Integrations** -> **Webhooks**).
14
+ - Click the **Enable Webhooks** button if webhooks are not already enabled.
15
+
16
+ 2. **Create Webhook**:
17
+ - Click **Create Webhook**.
18
+ - **Name**: `send_audit_notification`
19
+ - **Schema**: `public`
20
+ - **Table**: `financial_analysis_audit`
21
+ - **Events**: Toggle on **`INSERT`** only (so it fires only when a new log is created).
22
+ - **Webhook Type**: Select **HTTP POST**.
23
+ - **HTTP URL**:
24
+ - If routing via **Zapier/Make**, enter your webhook trigger URL.
25
+ - If routing directly to Slack, enter your **Supabase Edge Function URL** (see Section 3 for direct Slack formatting).
26
+ - **Timeout**: `10` seconds (default).
27
+ - Click **Create Webhook**.
28
+
29
+ ---
30
+
31
+ ## 2. Event Payload Schema
32
+
33
+ When a new row is inserted, Supabase POSTs a JSON payload with the following schema:
34
+
35
+ ```json
36
+ {
37
+ "type": "INSERT",
38
+ "table": "financial_analysis_audit",
39
+ "schema": "public",
40
+ "record": {
41
+ "id": 42,
42
+ "timestamp": "2026-05-22T10:09:00Z",
43
+ "user_email": "analyst@company.com",
44
+ "company_ticker": "TCS.NS",
45
+ "company_name": "Tata Consultancy Services Limited",
46
+ "selected_formats": "xlsx, docx, pptx",
47
+ "ai_model_used": "groq, gemini",
48
+ "status": "completed"
49
+ },
50
+ "old_record": null
51
+ }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 3. Formatting for Slack / Teams (Supabase Edge Function)
57
+
58
+ Since Slack and Teams require specific JSON formats (e.g., `{"text": "..."}`), you cannot point the Supabase Database Webhook directly to Slack's webhook URL without a translator.
59
+
60
+ Below is the code for a lightweight **Supabase Edge Function** in TypeScript that acts as a translator.
61
+
62
+ ### Edge Function Code (`index.ts`)
63
+ ```typescript
64
+ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
65
+
66
+ const SLACK_WEBHOOK_URL = Deno.env.get("SLACK_WEBHOOK_URL") || "";
67
+
68
+ serve(async (req) => {
69
+ try {
70
+ const payload = await req.json();
71
+ const record = payload.record;
72
+
73
+ if (!record) {
74
+ return new Response(JSON.stringify({ error: "No record found" }), { status: 400 });
75
+ }
76
+
77
+ // Build a rich Slack block layout
78
+ const slackMessage = {
79
+ text: `πŸ“ˆ New Financial Analysis Run: ${record.company_name} (${record.company_ticker})`,
80
+ blocks: [
81
+ {
82
+ type: "header",
83
+ text: {
84
+ type: "plain_text",
85
+ text: "πŸ“ˆ Financial Statement Analysis Logged",
86
+ emoji: true
87
+ }
88
+ },
89
+ {
90
+ type: "section",
91
+ fields: [
92
+ {
93
+ type: "mrkdwn",
94
+ text: `*User:* \`${record.user_email}\``
95
+ },
96
+ {
97
+ type: "mrkdwn",
98
+ text: `*Company:* *${record.company_name}* (\`${record.company_ticker}\`)`
99
+ },
100
+ {
101
+ type: "mrkdwn",
102
+ text: `*Formats Generated:* \`${record.selected_formats}\``
103
+ },
104
+ {
105
+ type: "mrkdwn",
106
+ text: `*AI Engines Used:* \`${record.ai_model_used}\``
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ type: "context",
112
+ elements: [
113
+ {
114
+ type: "mrkdwn",
115
+ text: `*Status:* ${record.status === "completed" ? "βœ… Completed Successfully" : "❌ Failed"} | *Time:* \`${record.timestamp}\``
116
+ }
117
+ ]
118
+ }
119
+ ]
120
+ };
121
+
122
+ // Forward formatted payload to Slack
123
+ const response = await fetch(SLACK_WEBHOOK_URL, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: JSON.stringify(slackMessage),
127
+ });
128
+
129
+ return new Response(JSON.stringify({ success: true }), { status: 200 });
130
+
131
+ } catch (error) {
132
+ return new Response(JSON.stringify({ error: error.message }), { status: 500 });
133
+ }
134
+ })
135
+ ```
136
+
137
+ ### Steps to Deploy the Edge Function:
138
+ 1. Install Supabase CLI: `npm install -g supabase`
139
+ 2. Login: `supabase login`
140
+ 3. Initialize in your folder: `supabase init`
141
+ 4. Create Edge Function: `supabase functions new slack-notifier`
142
+ 5. Replace the generated code in `supabase/functions/slack-notifier/index.ts` with the TypeScript code above.
143
+ 6. Set the Slack Webhook secret:
144
+ ```bash
145
+ supabase secrets set SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
146
+ ```
147
+ 7. Deploy the function:
148
+ ```bash
149
+ supabase functions deploy slack-notifier --project-ref your-project-ref-id
150
+ ```
151
+ 8. Set the Webhook URL in Supabase Dashboard to point to your new deployed function:
152
+ `https://your-project-ref-id.supabase.co/functions/v1/slack-notifier`