| |
| """ |
| 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 |
|
|
| |
| 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 = "" |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY_3") |
|
|
| BATCH_SIZE = 1 |
|
|
| |
| 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", |
| } |
|
|
| |
| client = genai.Client(api_key=GEMINI_API_KEY) |
| google_search_tool = types.Tool(google_search=types.GoogleSearch()) |
|
|
| FETCH_LOCK = threading.Lock() |
|
|
|
|
| |
| |
| |
|
|
| class PipelineState(TypedDict, total=False): |
| company_data: dict |
| company_id: int |
| company_name: str |
| enriched_json: dict |
| website: str |
| executives: list |
| deep_text: str |
| pdf_path: str |
| result: str |
| error: str |
|
|
|
|
| |
| |
| |
|
|
| 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() |
|
|
|
|
|
|
| |
| |
| |
|
|
| 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 |
| 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 |
|
|
| try: |
| |
| interaction = client.interactions.create( |
| input=query, |
| agent="deep-research-preview-04-2026", |
| background=True, |
| ) |
| print(f" [Deep Research] Interaction started: {interaction.id}") |
|
|
| |
| 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: |
| |
| dot_count += 1 |
| print(f" [Deep Research] β³ [{elapsed:.0f}s] Status: {status}β¦", flush=True) |
| time.sleep(10) |
|
|
| |
| 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_COLUMNS = [ |
| "Run Date", |
| "Company ID", |
| "Company Name", |
| "Website", |
| "Address", |
| "Total Shipments", |
| "Top Suppliers", |
| "HS Codes", |
| "Company Description", |
| "Key Executives", |
| "Deep Research PDF", |
| "Status", |
| ] |
|
|
|
|
| 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) |
|
|
| |
| 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, |
| 2: 10, |
| 3: 30, |
| 4: 28, |
| 5: 32, |
| 6: 16, |
| 7: 36, |
| 8: 22, |
| 9: 60, |
| 10: 50, |
| 11: 50, |
| 12: 16, |
| } |
| 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 |
|
|
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| exclude_ids = [] |
| if os.path.exists(TRACKER_DB_PATH): |
| try: |
| conn.execute("ATTACH DATABASE ? AS tracker", (TRACKER_DB_PATH,)) |
| |
| 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"] |
|
|
| |
| 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} |
|
|
|
|
| |
| |
| |
|
|
| def route_after_fetch(state: PipelineState) -> str: |
| if state.get("error") or not state.get("company_data"): |
| return "end" |
| return "continue" |
|
|
|
|
| |
| |
| |
|
|
| 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() |
|
|
|
|
| |
| |
| |
|
|
| 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 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) |
|
|
|
|
| |
| |
| |
|
|
| 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)) |
|
|
| |
| 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).") |