Download src_2/ColdEmail/email_specific.py from Asad999/Texbase: direct link, hf CLI and curl.
- Browser
- Download file 32.9 kB
-
https://huggingface.co/spaces/Asad999/Texbase/resolve/main/src_2/ColdEmail/email_specific.py
- Command line
-
hf download hf://spaces/Asad999/Texbase/src_2/ColdEmail/email_specific.py
-
curl -L -o email_specific.py https://huggingface.co/spaces/Asad999/Texbase/resolve/main/src_2/ColdEmail/email_specific.py
32.9 kB
| #!/usr/bin/env python3 | |
| """ | |
| email_specific.py β TEXBASE Cold Email Pipeline (LangGraph Architecture) | |
| =========================================================================== | |
| Improvements: | |
| β’ Parallel processing of 2 companies simultaneously via asyncio | |
| β’ Structured JSON email extraction schema β no regex fishing | |
| β’ Deep research contact extraction uses an explicit Gemini parse step | |
| β’ Excel output (outreach_results.xlsx) written after every batch | |
| β’ Rollback on failure per company (independent) | |
| Flow per company (LangGraph StateGraph): | |
| fetch_company βββΊ profile_gemini βββΊ verify_emails βββΊ deep_research | |
| β | |
| save_to_db βββ draft_emails βββ scrape_importyeti | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import threading | |
| import sqlite3 | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| import requests | |
| import urllib.parse | |
| from dotenv import load_dotenv | |
| load_dotenv(os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'backend/.env')) | |
| import datetime | |
| from typing import TypedDict, Optional | |
| from langgraph.graph import StateGraph, END | |
| from google import genai | |
| from google.genai import types | |
| import openpyxl | |
| from openpyxl.styles import ( | |
| Font, PatternFill, Alignment, Border, Side | |
| ) | |
| from openpyxl.utils import get_column_letter | |
| sys.path.append(os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'USA_ImportYeti')) | |
| from scrape_importyeti_profiles import scrape_single_link, parse_data | |
| # βββ Paths & Keys βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DB_PATH = os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'USA_ImportYeti/importyeti_data.db') | |
| TRACKER_DB_PATH = os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'src_2/ColdEmail/outreach_tracker.db') | |
| DEEP_RES_PDF_DIR = os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'src_2/ColdEmail/deep_research_reports') | |
| EXCEL_OUTPUT = os.path.join(os.environ.get('WORKSPACE_ROOT', '.'), 'Database/excelSheet/outreach_results.xlsx') | |
| HUNTER_API_KEY = "" # not used β email finding removed | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY_3") | |
| BATCH_SIZE = 1 # companies to process in parallel | |
| # βββ Arooj Enterprises sender profile βββββββββββββββββββββββββββββββββββββββββ | |
| SENDER = { | |
| "company": "Arooj Enterprises", | |
| "est": "1993", | |
| "name": "Asad Irfan", | |
| "title": "Senior Marketing Manager", | |
| "website": "www.texbase.com", | |
| "capacity": "150k units/month", | |
| "certs": "ISO 14001, SEDEX, and OEKO-TEX", | |
| "advantage": "vertical integration and rigorous quality control", | |
| } | |
| # βββ Gemini Clients ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| client = genai.Client(api_key=GEMINI_API_KEY) | |
| google_search_tool = types.Tool(google_search=types.GoogleSearch()) | |
| FETCH_LOCK = threading.Lock() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PIPELINE STATE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class PipelineState(TypedDict, total=False): | |
| company_data: dict | |
| company_id: int | |
| company_name: str | |
| enriched_json: dict # Gemini profile (no emails) | |
| website: str | |
| executives: list # [{name, title}, ...] | |
| deep_text: str | |
| pdf_path: str | |
| result: str | |
| error: str | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TRACKER DATABASE HELPERS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def init_tracker_db() -> sqlite3.Connection: | |
| """Open (or create) the single outreach_companies table.""" | |
| os.makedirs(os.path.dirname(TRACKER_DB_PATH), exist_ok=True) | |
| conn = sqlite3.connect(TRACKER_DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| conn.executescript(""" | |
| CREATE TABLE IF NOT EXISTS outreach_companies ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| company_id INTEGER UNIQUE, | |
| company_name TEXT, | |
| website TEXT, | |
| address TEXT, | |
| total_shipments TEXT, | |
| top_suppliers TEXT, | |
| hs_codes TEXT, | |
| company_description TEXT, | |
| key_executives TEXT, -- JSON: [{name, title}, ...] | |
| deep_research_summary TEXT, | |
| deep_research_pdf_path TEXT, | |
| status TEXT DEFAULT 'under_review', | |
| run_date TEXT | |
| ); | |
| """) | |
| conn.commit() | |
| return conn | |
| def save_company_to_db(conn, state: dict) -> None: | |
| """Upsert one row per company into outreach_companies.""" | |
| cd = state.get("company_data", {}) | |
| company_id = state.get("company_id") | |
| company_name = state.get("company_name", "") | |
| enriched = cd.get("enriched_data", {}) | |
| profile = enriched.get("company_profile", {}) | |
| website = profile.get("website", "") | |
| description = profile.get("comprehensive_description", "") | |
| executives = json.dumps(state.get("executives", []), ensure_ascii=False) | |
| address = cd.get("address", "") | |
| total_ship = cd.get("total_shipments", "") | |
| top_sup = cd.get("top_suppliers", "") | |
| hs_codes = cd.get("scraped_profile", {}).get("hs_codes", "") | |
| deep_text = state.get("deep_text", "") | |
| pdf_path = state.get("pdf_path", "") | |
| conn.execute(""" | |
| INSERT INTO outreach_companies | |
| (company_id, company_name, website, address, total_shipments, | |
| top_suppliers, hs_codes, company_description, key_executives, | |
| deep_research_summary, deep_research_pdf_path, status, run_date) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'under_review', ?) | |
| ON CONFLICT(company_id) DO UPDATE SET | |
| company_name = excluded.company_name, | |
| website = excluded.website, | |
| address = excluded.address, | |
| total_shipments = excluded.total_shipments, | |
| top_suppliers = excluded.top_suppliers, | |
| hs_codes = excluded.hs_codes, | |
| company_description = excluded.company_description, | |
| key_executives = excluded.key_executives, | |
| deep_research_summary = excluded.deep_research_summary, | |
| deep_research_pdf_path = excluded.deep_research_pdf_path, | |
| run_date = excluded.run_date | |
| """, ( | |
| company_id, company_name, website, address, total_ship, | |
| top_sup, hs_codes, description, executives, | |
| deep_text, pdf_path, datetime.datetime.now().isoformat(), | |
| )) | |
| conn.commit() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # DEEP RESEARCH | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def clean_markdown_for_pdf(text: str) -> str: | |
| text = re.sub(r"#{1,6}\s*", "", text) | |
| text = re.sub(r"\*{1,3}([^*]+)\*{1,3}", r"\1", text) | |
| text = re.sub(r"__([^_]+)__", r"\1", text) | |
| text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) | |
| text = re.sub(r"`{1,3}[^`]*`{1,3}", "", text) | |
| text = re.sub(r"^[-*+]\s+", "β’ ", text, flags=re.MULTILINE) | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| return text.strip() | |
| def _save_deep_research_pdf(company_name: str, research_text: str) -> str: | |
| os.makedirs(DEEP_RES_PDF_DIR, exist_ok=True) | |
| safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", company_name)[:60] | |
| timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| try: | |
| from fpdf import FPDF # type: ignore | |
| pdf = FPDF() | |
| pdf.set_auto_page_break(auto=True, margin=15) | |
| pdf.add_page() | |
| pdf.set_font("Helvetica", style="B", size=16) | |
| pdf.cell(0, 10, txt=f"Deep Research: {company_name}", ln=True) | |
| pdf.set_font("Helvetica", size=9) | |
| pdf.cell(0, 6, txt=f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=True) | |
| pdf.ln(4) | |
| pdf.set_font("Helvetica", size=11) | |
| clean_text = clean_markdown_for_pdf(research_text) | |
| safe_text = clean_text.encode("latin-1", "replace").decode("latin-1") | |
| pdf.multi_cell(0, 6, txt=safe_text) | |
| pdf_path = os.path.join(DEEP_RES_PDF_DIR, f"{safe_name}_{timestamp}.pdf") | |
| pdf.output(pdf_path) | |
| print(f" [Deep Research] PDF saved β {pdf_path}") | |
| return pdf_path | |
| except ImportError: | |
| txt_path = os.path.join(DEEP_RES_PDF_DIR, f"{safe_name}_{timestamp}.txt") | |
| with open(txt_path, "w", encoding="utf-8") as f: | |
| f.write(f"Deep Research Report: {company_name}\n") | |
| f.write(f"Generated: {datetime.datetime.now()}\n\n") | |
| f.write(research_text) | |
| print(f" [Deep Research] Text report saved β {txt_path}") | |
| return txt_path | |
| def run_deep_research(company_data: dict) -> tuple: | |
| """Run deep research and return (research_text, pdf_path). Uses polling pattern.""" | |
| company_name = company_data.get("company_name", "Unknown") | |
| address = company_data.get("address", "") | |
| total_shipments = company_data.get("total_shipments", "") | |
| recent_shipment = company_data.get("recent_shipment", "") | |
| top_suppliers = company_data.get("top_suppliers", "") | |
| profile_url = company_data.get("profile_url", "") | |
| website = company_data.get("enriched_data", {}).get("company_profile", {}).get("website", "") | |
| print(f"\n[Deep Research] Starting for '{company_name}'β¦") | |
| query = f"""You are a corporate intelligence researcher. Using ALL available sources | |
| (company website, LinkedIn, ZoomInfo, Apollo, Crunchbase, news articles, press releases), | |
| research the following US-based import company and compile a detailed intelligence report. | |
| βββ COMPANY INTELLIGENCE BRIEF βββ | |
| Company Name : {company_name} | |
| Address : {address or 'See ImportYeti profile'} | |
| Total Shipments : {total_shipments} | |
| Most Recent PO : {recent_shipment} | |
| Known Suppliers : {top_suppliers} | |
| ImportYeti URL : {profile_url} | |
| Website : {website or 'Search to find it'} | |
| ββββββββββββββββββββββββββββββββββ | |
| Please research and provide: | |
| 1. EXECUTIVE TEAM β Full names, titles, and LinkedIn URLs for: | |
| - CEO / President / Owner | |
| - VP / Director of Purchasing or Sourcing | |
| - Merchandising Manager / Head of Product | |
| - Any other buying / procurement contacts | |
| 2. COMPANY PROFILE β Official website, main product categories imported, | |
| typical order sizes, primary countries of origin sourced from. | |
| 3. SUPPLIER CONTEXT β What do their known suppliers ({top_suppliers}) specialise in? | |
| 4. MARKET POSITION β Estimated revenue, key retail partners, brand positioning. | |
| Be thorough and precise. Do NOT include email addresses. | |
| """ | |
| MAX_POLL_SECONDS = 1200 # 20-minute absolute ceiling | |
| try: | |
| # ββ Step 1: Create the background interaction ββββββββββββββββββββββ | |
| interaction = client.interactions.create( | |
| input=query, | |
| agent="deep-research-preview-04-2026", | |
| background=True, | |
| ) | |
| print(f" [Deep Research] Interaction started: {interaction.id}") | |
| # ββ Step 2: Poll until completed or failed βββββββββββββββββββββββββ | |
| start_time = time.time() | |
| dot_count = 0 | |
| while True: | |
| elapsed = time.time() - start_time | |
| if elapsed > MAX_POLL_SECONDS: | |
| print(f" [Deep Research] β οΈ Max timeout ({MAX_POLL_SECONDS}s) reached.") | |
| return "", "" | |
| interaction = client.interactions.get(interaction.id) | |
| status = interaction.status | |
| if status == "completed": | |
| print(f"\n [Deep Research] β Research complete for '{company_name}'") | |
| break | |
| elif status == "failed": | |
| error = getattr(interaction, "error", "unknown error") | |
| print(f" [Deep Research] β Research failed: {error}") | |
| return "", "" | |
| else: | |
| # Still running β print a heartbeat dot every 10s | |
| dot_count += 1 | |
| print(f" [Deep Research] β³ [{elapsed:.0f}s] Status: {status}β¦", flush=True) | |
| time.sleep(10) | |
| # ββ Step 3: Extract text from outputs βββββββββββββββββββββββββββββ | |
| research_text = "" | |
| outputs = getattr(interaction, "outputs", None) or [] | |
| if outputs: | |
| research_text = outputs[-1].text or "" | |
| if not research_text: | |
| print(" [Deep Research] β οΈ No text in outputs. Interaction may be empty.") | |
| pdf_path = _save_deep_research_pdf(company_name, research_text) | |
| print(f" [Deep Research] Done β report saved.") | |
| return research_text, pdf_path | |
| except Exception as e: | |
| print(f" [Deep Research] Error: {e}") | |
| return "", "" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EXCEL OUTPUT (one row per company) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| EXCEL_COLUMNS = [ | |
| "Run Date", | |
| "Company ID", | |
| "Company Name", | |
| "Website", | |
| "Address", | |
| "Total Shipments", | |
| "Top Suppliers", | |
| "HS Codes", | |
| "Company Description", | |
| "Key Executives", # JSON: [{name, title}, ...] | |
| "Deep Research PDF", | |
| "Status", # default: under_review | |
| ] | |
| def write_excel(all_states: list[dict], output_path: str) -> None: | |
| """ | |
| Write / append one row per company to the Excel file. | |
| Deduplicates by company_id β if the company already exists, its row is skipped. | |
| """ | |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) | |
| if os.path.exists(output_path): | |
| wb = openpyxl.load_workbook(output_path) | |
| ws = wb.active | |
| else: | |
| wb = openpyxl.Workbook() | |
| ws = wb.active | |
| ws.title = "Outreach" | |
| _write_header(ws) | |
| # ββ Build dedup set from existing rows (col 2 = Company ID) ββββ | |
| existing_ids: set = set() | |
| for row_idx in range(2, ws.max_row + 1): | |
| cid = ws.cell(row=row_idx, column=2).value | |
| if cid is not None: | |
| existing_ids.add(str(cid).strip()) | |
| thin = Side(style="thin", color="CCCCCC") | |
| border = Border(left=thin, right=thin, top=thin, bottom=thin) | |
| added_count = 0 | |
| for state in all_states: | |
| company_id = state.get("company_id") | |
| if not company_id: | |
| continue | |
| cid_str = str(company_id).strip() | |
| if cid_str in existing_ids: | |
| print(f" [Excel] Skipping duplicate row for company_id={cid_str} ({state.get('company_name')})") | |
| continue | |
| existing_ids.add(cid_str) | |
| added_count += 1 | |
| cd = state.get("company_data", {}) | |
| company_name = state.get("company_name", "") | |
| enriched = cd.get("enriched_data", {}) | |
| profile = enriched.get("company_profile", {}) | |
| website = profile.get("website", "") | |
| description = profile.get("comprehensive_description", "") | |
| address = cd.get("address", "") | |
| total_ship = cd.get("total_shipments", "") | |
| top_sup = cd.get("top_suppliers", "") | |
| hs_codes = cd.get("scraped_profile", {}).get("hs_codes", "") | |
| executives = json.dumps(state.get("executives", []), ensure_ascii=False) | |
| pdf_path = state.get("pdf_path", "") | |
| run_date = datetime.date.today().isoformat() | |
| row_data = [ | |
| run_date, company_id, company_name, website, address, | |
| total_ship, top_sup, hs_codes, description, executives, | |
| pdf_path, "under_review", | |
| ] | |
| _append_row(ws, row_data, border) | |
| wb.save(output_path) | |
| print(f"\n[Excel] Saved {added_count} new entries β {output_path}") | |
| def _write_header(ws) -> None: | |
| header_fill = PatternFill("solid", start_color="1F4E79") | |
| header_font = Font(name="Arial", bold=True, color="FFFFFF", size=10) | |
| for col_idx, col_name in enumerate(EXCEL_COLUMNS, start=1): | |
| cell = ws.cell(row=1, column=col_idx, value=col_name) | |
| cell.fill = header_fill | |
| cell.font = header_font | |
| cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) | |
| widths = { | |
| 1: 12, # Run Date | |
| 2: 10, # Company ID | |
| 3: 30, # Company Name | |
| 4: 28, # Website | |
| 5: 32, # Address | |
| 6: 16, # Total Shipments | |
| 7: 36, # Top Suppliers | |
| 8: 22, # HS Codes | |
| 9: 60, # Company Description | |
| 10: 50, # Key Executives | |
| 11: 50, # Deep Research PDF | |
| 12: 16, # Status | |
| } | |
| for col_idx, width in widths.items(): | |
| ws.column_dimensions[get_column_letter(col_idx)].width = width | |
| ws.row_dimensions[1].height = 30 | |
| ws.freeze_panes = "A2" | |
| def _append_row(ws, row_data: list, border) -> None: | |
| row_idx = ws.max_row + 1 | |
| alt_fill = PatternFill("solid", start_color="EBF3FB") if row_idx % 2 == 0 else None | |
| for col_idx, value in enumerate(row_data, start=1): | |
| cell = ws.cell(row=row_idx, column=col_idx, value=value) | |
| cell.font = Font(name="Arial", size=9) | |
| cell.border = border | |
| cell.alignment = Alignment(vertical="top", wrap_text=(col_idx in (9, 10))) | |
| if alt_fill: | |
| cell.fill = alt_fill | |
| ws.row_dimensions[row_idx].height = 18 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LANGGRAPH NODES | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def node_fetch_company(state: PipelineState) -> PipelineState: | |
| print("\n" + "β"*65) | |
| print(" [Node 1/7] fetch_company") | |
| print("β"*65) | |
| with FETCH_LOCK: | |
| try: | |
| print(f" [DEBUG] Attempting to open DB: {os.path.abspath(DB_PATH)}") | |
| if not os.path.exists(DB_PATH): | |
| print(f" [ERROR] Database file does not exist at: {os.path.abspath(DB_PATH)}") | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| # ββ Check tracker DB to avoid processing duplicates ββββββ | |
| # We attach the tracker DB to do a single query check | |
| exclude_ids = [] | |
| if os.path.exists(TRACKER_DB_PATH): | |
| try: | |
| conn.execute("ATTACH DATABASE ? AS tracker", (TRACKER_DB_PATH,)) | |
| # Verify table exists before querying | |
| table_check = conn.execute( | |
| "SELECT name FROM tracker.sqlite_master WHERE type='table' AND name='outreach_companies'" | |
| ).fetchone() | |
| if table_check: | |
| exclude_query = "AND id NOT IN (SELECT company_id FROM tracker.outreach_companies)" | |
| else: | |
| exclude_query = "" | |
| except Exception as e: | |
| print(f" [WARN] Could not attach tracker DB: {e}") | |
| exclude_query = "" | |
| else: | |
| exclude_query = "" | |
| query = f""" | |
| SELECT * FROM companies | |
| WHERE (selection = 0 OR selection IS NULL) | |
| {exclude_query} | |
| ORDER BY CAST(REPLACE(total_shipments, ',', '') AS INTEGER) DESC | |
| LIMIT 1 | |
| """ | |
| row = conn.execute(query).fetchone() | |
| if not row: | |
| conn.close() | |
| return {**state, "error": "No companies left in queue.", "result": "Queue empty."} | |
| company_data = dict(row) | |
| company_id = company_data["id"] | |
| # Mark as selected immediately | |
| conn.execute("UPDATE companies SET selection = 1 WHERE id = ?", (company_id,)) | |
| conn.commit() | |
| conn.close() | |
| print(f" β Fetched: {company_data['company_name']} (id={company_id})") | |
| return { | |
| **state, | |
| "company_data": company_data, | |
| "company_id": company_id, | |
| "company_name": company_data["company_name"], | |
| "enriched_json": {}, | |
| "executives": [], | |
| "website": "", | |
| "deep_text": "", | |
| "pdf_path": "", | |
| "error": "", | |
| } | |
| except Exception as e: | |
| print(f" [ERROR] fetch_company node: {e}") | |
| return {**state, "error": str(e)} | |
| def node_profile_gemini(state: PipelineState) -> PipelineState: | |
| print("\n" + "β"*65) | |
| print(" [Node 2/4] profile_gemini") | |
| print("β"*65) | |
| company_data = state["company_data"] | |
| company_json_str = json.dumps(company_data, indent=2) | |
| profile_prompt = f"""**ROLE:** Corporate Intelligence Analyst. | |
| **GOAL:** Verify US presence, build a company profile, and identify key executives. | |
| Company data: | |
| ```json | |
| {company_json_str} | |
| ``` | |
| **TASKS:** | |
| 1. Verify real US commercial location and address. | |
| 2. Write a comprehensive company description (products imported, market segment, key retail partners, annual revenue estimate). | |
| 3. Identify the executive team: CEO/President/Owner, VP Purchasing, Merchandising Manager, and any other procurement contacts. | |
| - For each executive provide: Full Name, Job Title, and LinkedIn URL. | |
| - Do NOT include or predict any email addresses. | |
| Return ONLY raw JSON (no markdown fences): | |
| {{ | |
| "verification_status": {{"is_usa_based": true, "address_type": "...", "entity_match_confidence": "..."}}, | |
| "company_profile": {{ | |
| "official_name": "...", | |
| "website": "...", | |
| "main_phone": "...", | |
| "comprehensive_description": "..." | |
| }}, | |
| "executives": [ | |
| {{ | |
| "name": "Full Name", | |
| "title": "Job Title", | |
| "linkedin": "https://linkedin.com/in/..." | |
| }} | |
| ] | |
| }}""" | |
| enriched_json = {} | |
| try: | |
| resp = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=profile_prompt, | |
| config=types.GenerateContentConfig( | |
| tools=[google_search_tool], | |
| response_modalities=["TEXT"], | |
| ), | |
| ) | |
| raw = resp.text.strip() | |
| raw = re.sub(r"^```(?:json)?\s*", "", raw, flags=re.MULTILINE) | |
| raw = re.sub(r"```\s*$", "", raw, flags=re.MULTILINE).strip() | |
| try: | |
| enriched_json = json.loads(raw) | |
| except json.JSONDecodeError: | |
| print(" [WARN] Could not parse Gemini profile JSON.") | |
| except Exception as e: | |
| print(f" [Gemini] Profile error: {e}") | |
| company_data["enriched_data"] = enriched_json | |
| website = enriched_json.get("company_profile", {}).get("website", "") | |
| executives = enriched_json.get("executives", []) | |
| print(f" β {len(executives)} executive(s) identified") | |
| return {**state, "enriched_json": enriched_json, "company_data": company_data, | |
| "website": website, "executives": executives} | |
| def node_deep_research(state: PipelineState) -> PipelineState: | |
| print("\n" + "β"*65) | |
| print(" [Node 3/4] deep_research") | |
| print("β"*65) | |
| company_data = state["company_data"] | |
| research_text, pdf_path = run_deep_research(company_data) | |
| return {**state, "deep_text": research_text, "pdf_path": pdf_path} | |
| def node_scrape_importyeti(state: PipelineState) -> PipelineState: | |
| print("\n" + "β"*65) | |
| print(" [Node 2b/4] scrape_importyeti") | |
| print("β"*65) | |
| company_data = state["company_data"] | |
| profile_url = company_data.get("profile_url", "") | |
| if profile_url and isinstance(profile_url, str) and profile_url.startswith("http"): | |
| print(f" Fetching: {profile_url}") | |
| try: | |
| html = scrape_single_link(profile_url) | |
| if html: | |
| loc, codes = parse_data(html) | |
| company_data["scraped_profile"] = {"location_info": loc, "hs_codes": codes} | |
| print(f" β HS codes: {codes}") | |
| else: | |
| company_data["scraped_profile"] = {"error": "No HTML returned"} | |
| except Exception as e: | |
| company_data["scraped_profile"] = {"error": str(e)} | |
| else: | |
| print(" No profile URL β skipped.") | |
| company_data["scraped_profile"] = {} | |
| return {**state, "company_data": company_data} | |
| def node_save_to_db(state: PipelineState) -> PipelineState: | |
| print("\n" + "β"*65) | |
| print(" [Node 4/4] save_to_db") | |
| print("β"*65) | |
| company_name = state.get("company_name", "?") | |
| tracker = init_tracker_db() | |
| save_company_to_db(tracker, state) | |
| tracker.close() | |
| summary = f"Done β '{company_name}' | profile saved | status=under_review" | |
| print(f"\n{'β'*65}\n {summary}\n{'β'*65}") | |
| return {**state, "result": summary} | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONDITIONAL ROUTER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def route_after_fetch(state: PipelineState) -> str: | |
| if state.get("error") or not state.get("company_data"): | |
| return "end" | |
| return "continue" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BUILD LANGGRAPH | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_graph(): | |
| graph = StateGraph(PipelineState) | |
| graph.add_node("fetch_company", node_fetch_company) | |
| graph.add_node("profile_gemini", node_profile_gemini) | |
| graph.add_node("scrape_importyeti", node_scrape_importyeti) | |
| graph.add_node("deep_research", node_deep_research) | |
| graph.add_node("save_to_db", node_save_to_db) | |
| graph.set_entry_point("fetch_company") | |
| graph.add_conditional_edges( | |
| "fetch_company", | |
| route_after_fetch, | |
| {"continue": "profile_gemini", "end": END}, | |
| ) | |
| graph.add_edge("profile_gemini", "scrape_importyeti") | |
| graph.add_edge("scrape_importyeti", "deep_research") | |
| graph.add_edge("deep_research", "save_to_db") | |
| graph.add_edge("save_to_db", END) | |
| return graph.compile() | |
| pipeline = build_graph() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ROLLBACK HELPER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _rollback_company(company_id: int, reason: str) -> None: | |
| print(f"\n[ROLLBACK] company_id={company_id} β {reason}") | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.execute("UPDATE companies SET selection = 0 WHERE id = ?", (company_id,)) | |
| conn.commit() | |
| conn.close() | |
| print(f"[ROLLBACK] β selection reset to 0 for company_id={company_id}") | |
| except Exception as rb_err: | |
| print(f"[ROLLBACK ERROR] {rb_err}") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ASYNC PARALLEL RUNNER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def run_single_pipeline() -> dict: | |
| """Run one company through the pipeline in a thread pool (blocking I/O).""" | |
| loop = asyncio.get_event_loop() | |
| final_state = {} | |
| try: | |
| final_state = await loop.run_in_executor(None, lambda: pipeline.invoke({})) | |
| error_msg = final_state.get("error", "") | |
| if error_msg and final_state.get("company_id"): | |
| _rollback_company(final_state["company_id"], f"error: {error_msg}") | |
| except Exception as e: | |
| print(f"\n[CRITICAL ERROR] Pipeline crashed: {e}") | |
| if final_state and isinstance(final_state, dict) and final_state.get("company_id"): | |
| _rollback_company(final_state["company_id"], f"crash: {e}") | |
| return final_state if isinstance(final_state, dict) else {} | |
| async def run_batch(batch_size: int) -> list[dict]: | |
| """Run `batch_size` companies in parallel and return all final states.""" | |
| tasks = [run_single_pipeline() for _ in range(batch_size)] | |
| results = await asyncio.gather(*tasks, return_exceptions=False) | |
| return list(results) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CLI ENTRY POINT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| os.makedirs(DEEP_RES_PDF_DIR, exist_ok=True) | |
| print(f"\n{'β'*65}") | |
| print(f" TEXBASE Cold Email Pipeline β Batch of {BATCH_SIZE}") | |
| print(f"{'β'*65}") | |
| all_states = asyncio.run(run_batch(BATCH_SIZE)) | |
| # Filter out empty states (queue was empty for that slot) | |
| valid_states = [s for s in all_states if s.get("company_id")] | |
| if valid_states: | |
| write_excel(valid_states, EXCEL_OUTPUT) | |
| print(f"\n{'β'*65}") | |
| print(" BATCH SUMMARY") | |
| print(f"{'β'*65}") | |
| for s in valid_states: | |
| print(f" β’ {s.get('company_name', '?')} β " | |
| f"result: {s.get('result', s.get('error', ''))}") | |
| print(f"\n Excel output β {EXCEL_OUTPUT}") | |
| else: | |
| print("\n No companies processed (queue may be empty).") |