From 7c2fd5202ec5ed0de4a63b8a891598eeac28e627 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Thu, 1 May 2025 18:27:03 +0530 Subject: [PATCH 1/8] fix: incorrect params and commands in linkedin app readme --- docs/apps/linkdin/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/apps/linkdin/README.md b/docs/apps/linkdin/README.md index cce244ac..d14c5352 100644 --- a/docs/apps/linkdin/README.md +++ b/docs/apps/linkdin/README.md @@ -21,7 +21,7 @@ pip install crawl4ai openai sentence-transformers networkx pandas vis-network ri ### 1.2  Create / warm a LinkedIn browser profile ```bash -crwl profiler +crwl profiles ``` 1. The interactive shell shows **New profile** – hit **enter**. 2. Choose a name, e.g. `profile_linkedin_uc`. @@ -37,13 +37,13 @@ crwl profiler python c4ai_discover.py full \ --query "health insurance management" \ --geo 102713980 \ # Malaysia geoUrn - --title_filters "" \ # or "Product,Engineering" - --max_companies 10 \ # default set small for workshops - --max_people 20 \ # \^ same + --title-filters "" \ # or "Product,Engineering" + --max-companies 10 \ # default set small for workshops + --max-people 20 \ # \^ same --profile-name profile_linkedin_uc \ --outdir ./data \ --concurrency 2 \ - --log_level debug + --log-level debug ``` **Outputs** in `./data/`: * `companies.jsonl` – one JSON per company From baf7f6a6f52e9da50502b1da0f200dda02187896 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Fri, 2 May 2025 16:33:11 +0530 Subject: [PATCH 2/8] fix: typo in readme --- docs/apps/linkdin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apps/linkdin/README.md b/docs/apps/linkdin/README.md index d14c5352..7fe61bd7 100644 --- a/docs/apps/linkdin/README.md +++ b/docs/apps/linkdin/README.md @@ -121,6 +121,6 @@ The page fetches `data/company_graph.json` and the `org_chart_*.json` files auto --- ### TL;DR -`crwl profiler` → `c4ai_discover.py` → `c4ai_insights.py` → open `graph_view_template.html`. +`crwl profiles` → `c4ai_discover.py` → `c4ai_insights.py` → open `graph_view_template.html`. Live long and `import crawl4ai`. From 5cc58f9bb3ac049df3cb7a2ee31680b3aec30a41 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Fri, 2 May 2025 16:40:58 +0530 Subject: [PATCH 3/8] fix: 1. duplicate verbose flag 2.inconsistency in argument name --profile-name 3. duplicate initialisaiton of env_defaults --- docs/apps/linkdin/c4ai_discover.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/apps/linkdin/c4ai_discover.py b/docs/apps/linkdin/c4ai_discover.py index 82874568..dca2fa69 100644 --- a/docs/apps/linkdin/c4ai_discover.py +++ b/docs/apps/linkdin/c4ai_discover.py @@ -272,7 +272,7 @@ def build_arg_parser() -> argparse.ArgumentParser: parser.add_argument("--title-filters", default="Product,Engineering", help="comma list of job keywords") parser.add_argument("--max-companies", type=int, default=1000) parser.add_argument("--max-people", type=int, default=500) - parser.add_argument("--profile-path", default=str(pathlib.Path.home() / ".crawl4ai/profiles/profile_linkedin_uc")) + parser.add_argument("--profile-name", default=str(pathlib.Path.home() / ".crawl4ai/profiles/profile_linkedin_uc")) parser.add_argument("--outdir", default="./output") parser.add_argument("--concurrency", type=int, default=4) parser.add_argument("--log-level", default="info", choices=["debug", "info", "warn", "error"]) @@ -355,8 +355,7 @@ async def async_main(opts): user_agent_generator_config= { "platforms": "mobile", "os": "Android" - }, - verbose=False, + } ) crawler = AsyncWebCrawler(config=bc) @@ -366,7 +365,7 @@ async def async_main(opts): # crawler = await next_crawler().start() try: # Build LinkedIn search URL - search_url = f"https://www.linkedin.com/search/results/companies/?keywords={quote(opts.query)}&geoUrn={opts.geo}" + search_url = f'https://www.linkedin.com/search/results/companies/?keywords={quote(opts.query)}&companyHqGeo="{opts.geo}"' logging.info("Seed URL => %s", search_url) companies: List[Dict] = [] @@ -425,14 +424,13 @@ def main(): if cli_opts.debug: opts = detect_debug_defaults(force=True) else: - env_defaults = detect_debug_defaults() env_defaults = detect_debug_defaults() opts = env_defaults if env_defaults else cli_opts if not getattr(opts, "cmd", None): opts.cmd = "full" - exit_code = asyncio.run(async_main(opts)) + exit_code = asyncio.run(async_main(cli_opts)) sys.exit(exit_code) From 6650b2f34a1849d01e00ca1bcce5772ebaf7cc54 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Fri, 2 May 2025 16:51:15 +0530 Subject: [PATCH 4/8] fix: replace openAI with litellm to support multiple llm providers --- .gitignore | 4 ++- docs/apps/linkdin/c4ai_insights.py | 55 ++++++++++++++++-------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 1658a987..6a118cba 100644 --- a/.gitignore +++ b/.gitignore @@ -261,4 +261,6 @@ CLAUDE.md tests/**/test_site tests/**/reports -tests/**/benchmark_reports \ No newline at end of file +tests/**/benchmark_reports + +docs/**/data \ No newline at end of file diff --git a/docs/apps/linkdin/c4ai_insights.py b/docs/apps/linkdin/c4ai_insights.py index 8307c30d..94370258 100644 --- a/docs/apps/linkdin/c4ai_insights.py +++ b/docs/apps/linkdin/c4ai_insights.py @@ -43,7 +43,7 @@ import numpy as np import pandas as pd import hashlib -from openai import OpenAI # same SDK you pre-loaded +from litellm import completion #Support any LLM Provider # ─────────────────────────────────────────────────────────────────────────────── # Utils @@ -70,11 +70,12 @@ def dev_defaults() -> SimpleNamespace: out_dir="./insights_debug", embed_model="all-MiniLM-L6-v2", top_k=10, - openai_model="gpt-4.1", + llm_provider="openai/gpt-4.1", + llm_api_key=None, max_llm_tokens=8000, llm_temperature=1.0, - workers=4, # parallel processing - stub=False, # manual + workers=4, + stub=False ) # ─────────────────────────────────────────────────────────────────────────────── @@ -166,7 +167,7 @@ def build_company_graph(companies, embeds:np.ndarray, top_k:int) -> Dict[str,Any # ─────────────────────────────────────────────────────────────────────────────── # Org-chart via LLM # ─────────────────────────────────────────────────────────────────────────────── -async def infer_org_chart_llm(company, people, client:OpenAI, model_name:str, max_tokens:int, temperature:float, stub:bool): +async def infer_org_chart_llm(company, people, llm_provider:str, api_key:str, max_tokens:int, temperature:float, stub:bool): if stub: # Tiny fake org-chart when debugging offline chief = random.choice(people) @@ -202,15 +203,19 @@ Here is a JSON list of employees: Return JSON: {{ "nodes":[{{id,name,title,dept,yoe_total,yoe_current,seniority_score,decision_score,avatar_url,profile_url}}], "edges":[{{source,target,type,confidence}}] }} """} ] - resp = client.chat.completions.create( - model=model_name, + resp = completion( + model=llm_provider, messages=prompt, max_tokens=max_tokens, temperature=temperature, - response_format={"type":"json_object"} + response_format={"type":"json_object"}, + api_key=api_key ) chart = json.loads(resp.choices[0].message.content) - chart["meta"] = dict(model=model_name, generated_at=datetime.now(UTC).isoformat()) + chart["meta"] = dict( + model=llm_provider, + generated_at=datetime.now(UTC).isoformat() + ) return chart # ─────────────────────────────────────────────────────────────────────────────── @@ -270,15 +275,11 @@ async def run(opts): logging.info(f"[bold cyan]Loaded[/] {len(companies)} companies, {len(people)} people") logging.info("[bold]⇢[/] Embedding company descriptions…") - # embeds = embed_descriptions(companies, opts.embed_model, opts) + embeds = embed_descriptions(companies, opts.embed_model, opts) logging.info("[bold]⇢[/] Building similarity graph") - # company_graph = build_company_graph(companies, embeds, opts.top_k) - # dump_json(company_graph, out_dir/"company_graph.json") - - # OpenAI client (only built if not debugging) - stub = bool(opts.stub) - client = OpenAI() if not stub else None + company_graph = build_company_graph(companies, embeds, opts.top_k) + dump_json(company_graph, out_dir/"company_graph.json") # Filter companies that need processing to_process = [] @@ -311,14 +312,13 @@ async def run(opts): async def process_one(comp): handle = comp["handle"].strip("/").replace("/","_") persons = [p for p in people if p["company_handle"].strip("/") == comp["handle"].strip("/")] - chart = await infer_org_chart_llm( comp, persons, - client=client if client else OpenAI(api_key="sk-debug"), - model_name=opts.openai_model, + llm_provider=opts.llm_provider, + api_key=getattr(opts, 'llm_api_key', None), max_tokens=opts.max_llm_tokens, temperature=opts.llm_temperature, - stub=stub, + stub=opts.stub or False, ) chart["meta"]["company"] = comp["name"] @@ -354,18 +354,21 @@ def build_arg_parser(): p = argparse.ArgumentParser(description="Build graphs & visualisation from Stage-1 output") p.add_argument("--in", dest="in_dir", required=False, help="Stage-1 output dir", default=".") p.add_argument("--out", dest="out_dir", required=False, help="Destination dir", default=".") - p.add_argument("--embed_model", default="all-MiniLM-L6-v2") - p.add_argument("--top_k", type=int, default=10, help="Top-k neighbours per company") - p.add_argument("--openai_model", default="gpt-4.1") - p.add_argument("--max_llm_tokens", type=int, default=8024) - p.add_argument("--llm_temperature", type=float, default=1.0) + p.add_argument("--embed-model", default="all-MiniLM-L6-v2") + p.add_argument("--top-k", type=int, default=10, help="Top-k neighbours per company") + p.add_argument("--llm-provider", default="openai/gpt-4.1", + help="LLM model to use in format 'provider/model_name' (e.g., 'anthropic/claude-3')") + p.add_argument("--llm-api-key", help="API key for LLM provider (defaults to env vars)") + p.add_argument("--max-llm-tokens", type=int, default=8024) + p.add_argument("--llm-temperature", type=float, default=1.0) p.add_argument("--stub", action="store_true", help="Skip OpenAI call and generate tiny fake org charts") p.add_argument("--workers", type=int, default=4, help="Number of parallel workers for LLM inference") return p def main(): dbg = dev_defaults() - opts = dbg if True else build_arg_parser().parse_args() + # opts = dbg if True else build_arg_parser().parse_args() + opts = build_arg_parser().parse_args() asyncio.run(run(opts)) if __name__ == "__main__": From bd5a9ac632628f3e3b196ce86d3f5ed07f9414a0 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Fri, 2 May 2025 17:04:42 +0530 Subject: [PATCH 5/8] updated readme with arguments for litellm --- docs/apps/linkdin/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/apps/linkdin/README.md b/docs/apps/linkdin/README.md index 7fe61bd7..0441bf70 100644 --- a/docs/apps/linkdin/README.md +++ b/docs/apps/linkdin/README.md @@ -69,11 +69,12 @@ _See more: – t python c4ai_insights.py \ --in ./data \ --out ./data \ - --embed_model all-MiniLM-L6-v2 \ - --top_k 10 \ - --openai_model gpt-4.1 \ - --max_llm_tokens 8024 \ - --llm_temperature 1.0 \ + --embed_model all-MiniLM-L6-v2 \ + --llm-provider gemini/gemini-2.0-flash \ + --llm-api-key "" \ + --top-k 10 \ + --max-llm-tokens 8024 \ + --llm-temperature 1.0 \ --workers 4 ``` Emits next to the Stage‑1 files: From 87d4b0fff4d39dec9ab440c2e4f11b1ff0b4f6e3 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Fri, 2 May 2025 17:21:09 +0530 Subject: [PATCH 6/8] format bash scripts properly so copy & paste may work without issues --- docs/apps/linkdin/README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/apps/linkdin/README.md b/docs/apps/linkdin/README.md index 0441bf70..3b4ab504 100644 --- a/docs/apps/linkdin/README.md +++ b/docs/apps/linkdin/README.md @@ -34,15 +34,15 @@ crwl profiles ## 2  Discovery – scrape companies & people ```bash -python c4ai_discover.py full \ - --query "health insurance management" \ +python c4ai_discover.py full \ + --query "health insurance management" \ --geo 102713980 \ # Malaysia geoUrn --title-filters "" \ # or "Product,Engineering" --max-companies 10 \ # default set small for workshops --max-people 20 \ # \^ same - --profile-name profile_linkedin_uc \ - --outdir ./data \ - --concurrency 2 \ + --profile-name profile_linkedin_uc \ + --outdir ./data \ + --concurrency 2 \ --log-level debug ``` **Outputs** in `./data/`: @@ -66,15 +66,15 @@ _See more: – t ## 3  Insights – embeddings, org‑charts, decision makers ```bash -python c4ai_insights.py \ - --in ./data \ - --out ./data \ - --embed_model all-MiniLM-L6-v2 \ - --llm-provider gemini/gemini-2.0-flash \ - --llm-api-key "" \ - --top-k 10 \ - --max-llm-tokens 8024 \ - --llm-temperature 1.0 \ +python c4ai_insights.py \ + --in ./data \ + --out ./data \ + --embed-model all-MiniLM-L6-v2 \ + --llm-provider gemini/gemini-2.0-flash \ + --llm-api-key "" \ + --top-k 10 \ + --max-llm-tokens 8024 \ + --llm-temperature 1.0 \ --workers 4 ``` Emits next to the Stage‑1 files: From 38ebcbb304b806b81577e50befbc82d6a58f5d15 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Mon, 5 May 2025 10:34:38 +0530 Subject: [PATCH 7/8] fix: provide support for local llm by adding it to the arguments --- docs/apps/linkdin/c4ai_insights.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/apps/linkdin/c4ai_insights.py b/docs/apps/linkdin/c4ai_insights.py index 94370258..28b14cd8 100644 --- a/docs/apps/linkdin/c4ai_insights.py +++ b/docs/apps/linkdin/c4ai_insights.py @@ -74,8 +74,7 @@ def dev_defaults() -> SimpleNamespace: llm_api_key=None, max_llm_tokens=8000, llm_temperature=1.0, - workers=4, - stub=False + workers=4 ) # ─────────────────────────────────────────────────────────────────────────────── @@ -167,7 +166,7 @@ def build_company_graph(companies, embeds:np.ndarray, top_k:int) -> Dict[str,Any # ─────────────────────────────────────────────────────────────────────────────── # Org-chart via LLM # ─────────────────────────────────────────────────────────────────────────────── -async def infer_org_chart_llm(company, people, llm_provider:str, api_key:str, max_tokens:int, temperature:float, stub:bool): +async def infer_org_chart_llm(company, people, llm_provider:str, api_key:str, max_tokens:int, temperature:float, stub:bool=False, base_url:str=None): if stub: # Tiny fake org-chart when debugging offline chief = random.choice(people) @@ -209,7 +208,8 @@ Return JSON: {{ "nodes":[{{id,name,title,dept,yoe_total,yoe_current,seniority_sc max_tokens=max_tokens, temperature=temperature, response_format={"type":"json_object"}, - api_key=api_key + api_key=api_key, + base_url=base_url ) chart = json.loads(resp.choices[0].message.content) chart["meta"] = dict( @@ -315,10 +315,11 @@ async def run(opts): chart = await infer_org_chart_llm( comp, persons, llm_provider=opts.llm_provider, - api_key=getattr(opts, 'llm_api_key', None), + api_key=opts.llm_api_key or None, max_tokens=opts.max_llm_tokens, temperature=opts.llm_temperature, stub=opts.stub or False, + base_url=opts.llm_base_url or None ) chart["meta"]["company"] = comp["name"] @@ -359,6 +360,7 @@ def build_arg_parser(): p.add_argument("--llm-provider", default="openai/gpt-4.1", help="LLM model to use in format 'provider/model_name' (e.g., 'anthropic/claude-3')") p.add_argument("--llm-api-key", help="API key for LLM provider (defaults to env vars)") + p.add_argument("--llm-base-url", help="Base URL for LLM API endpoint") p.add_argument("--max-llm-tokens", type=int, default=8024) p.add_argument("--llm-temperature", type=float, default=1.0) p.add_argument("--stub", action="store_true", help="Skip OpenAI call and generate tiny fake org charts") From aaf05910ebef9f14a7a30673fa33c44bb1e94ffc Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Tue, 6 May 2025 15:53:55 +0530 Subject: [PATCH 8/8] fix: removed unnecessary imports and installs --- docs/apps/linkdin/README.md | 2 +- docs/apps/linkdin/c4ai_discover.py | 1 - docs/apps/linkdin/c4ai_insights.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/apps/linkdin/README.md b/docs/apps/linkdin/README.md index 3b4ab504..57f97815 100644 --- a/docs/apps/linkdin/README.md +++ b/docs/apps/linkdin/README.md @@ -16,7 +16,7 @@ prospect‑wizard/ ### 1.1  Install dependencies ```bash -pip install crawl4ai openai sentence-transformers networkx pandas vis-network rich +pip install crawl4ai litellm sentence-transformers pandas rich ``` ### 1.2  Create / warm a LinkedIn browser profile diff --git a/docs/apps/linkdin/c4ai_discover.py b/docs/apps/linkdin/c4ai_discover.py index dca2fa69..ac6d2783 100644 --- a/docs/apps/linkdin/c4ai_discover.py +++ b/docs/apps/linkdin/c4ai_discover.py @@ -43,7 +43,6 @@ from rich.console import Console from rich.logging import RichHandler from datetime import datetime, UTC -from itertools import cycle from textwrap import dedent from types import SimpleNamespace from typing import Dict, List, Optional diff --git a/docs/apps/linkdin/c4ai_insights.py b/docs/apps/linkdin/c4ai_insights.py index 28b14cd8..60348f43 100644 --- a/docs/apps/linkdin/c4ai_insights.py +++ b/docs/apps/linkdin/c4ai_insights.py @@ -20,7 +20,7 @@ from __future__ import annotations # Imports & Third-party # ─────────────────────────────────────────────────────────────────────────────── -import argparse, asyncio, json, os, sys, pathlib, random, time, csv +import argparse, asyncio, json, pathlib, random from datetime import datetime, UTC from types import SimpleNamespace from pathlib import Path @@ -30,7 +30,7 @@ from rich.console import Console from rich.logging import RichHandler from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn import logging -from jinja2 import Environment, FileSystemLoader, select_autoescape + BASE_DIR = pathlib.Path(__file__).resolve().parent