Spaces:
Sleeping
Sleeping
| title: 'ResearchPilot AI ' | |
| sdk: docker | |
| emoji: π | |
| colorFrom: indigo | |
| colorTo: purple | |
| thumbnail: >- | |
| https://cdn-uploads.huggingface.co/production/uploads/64ea5e1dc68ddc867b5ec02f/r8wTLNQPQIfsQjgazt_I5.png | |
| short_description: ResearchPilot AI is an Autonomous Multi-Agent Research Syste | |
| <div align="center"> | |
| # π§ ResearchPilot AI | |
| ### Autonomous Multi-Agent Research System | |
| **Give it a topic β Get a complete, evidence-backed research report with charts, citations, and PDF export.** | |
| ResearchPilot AI runs a coordinated team of specialised AI agents β each with a focused job β orchestrated by LangGraph. It plans, routes, searches, analyses, fact-checks, visualises, writes, and self-grades, all from one query. | |
| [](https://python.org) | |
| [](https://langchain-ai.github.io/langgraph/) | |
| [](https://groq.com) | |
| [](https://streamlit.io) | |
| [](https://plotly.com) | |
| [](https://www.reportlab.com) | |
| [](LICENSE) | |
| [π Live Demo](#) Β· [π Report Sample](#) Β· [π¨βπ» Portfolio](https://vishal-lazrus-portfolio.vercel.app/) | |
| </div> | |
| --- | |
| ## π Table of Contents | |
| | # | Section | | |
| |---|---| | |
| | 1 | [What is ResearchPilot AI?](#-what-is-researchpilot-ai) | | |
| | 2 | [What Makes It Different](#-what-makes-it-different) | | |
| | 3 | [Architecture](#-architecture) | | |
| | 4 | [How It Works β Real Example](#-how-it-works--real-example) | | |
| | 5 | [Folder Structure](#-folder-structure) | | |
| | 6 | [The Five Agents](#-the-five-agents) | | |
| | 7 | [The Six Chart Types](#-the-six-chart-types) | | |
| | 8 | [Getting API Keys (Free)](#-getting-api-keys-free) | | |
| | 9 | [Installation & Setup](#-installation--setup) | | |
| | 10 | [Running the Project](#-running-the-project) | | |
| | 11 | [Deploy to Hugging Face Spaces](#-deploy-to-hugging-face-spaces) | | |
| | 12 | [Concepts Covered](#-concepts-covered) | | |
| | 13 | [Troubleshooting](#-troubleshooting) | | |
| | 14 | [Roadmap](#-roadmap) | | |
| | 15 | [Connect With Me](#-connect-with-me) | | |
| --- | |
| ## π€ What is ResearchPilot AI? | |
| ResearchPilot AI is a **multi-agent AI research system** built with LangGraph. You type one query β say, *"Impact of AI on healthcare market growth"* β and it autonomously runs an 8-node pipeline: | |
| ``` | |
| Query β Plan β Route β Research + Analyse β Visualise β Write β Grade β Deliver | |
| ``` | |
| The system produces: | |
| - β A **structured markdown report** (Executive Summary β Key Findings β Analysis β Expert Perspective β Conclusion β References) | |
| - π Up to **6 interactive Plotly charts** generated from real statistical data | |
| - π A **professional academic PDF** with embedded charts and APA citations | |
| - ποΈ A **CSV file** of the raw statistics for further analysis | |
| > This is not a chatbot wrapper. It implements the **planner β router β workers β critic** pattern used in production agentic AI systems. | |
| ### Who is this for? | |
| | π Students | πΌ Professionals | π§βπ» Developers | | |
| |---|---|---| | |
| | Research papers, literature reviews, assignment prep | Market research, competitive analysis, technology evaluation | Learn LangGraph, conditional routing, agentic design patterns | | |
| --- | |
| ## β¨ What Makes It Different | |
| | Feature | What it does | | |
| |---|---| | |
| | π **Dynamic Router Agent** | One LLM call decides *per-query* which of 5 specialist agents to activate. A history question never wastes time running a Statistics agent. A finance query gets Fact Checker. Nothing runs unless it adds value. | | |
| | π **Statistics β Charts pipeline** | The Statistics agent returns *structured JSON* (not prose) β metrics, time-series, regional breakdowns β which directly drives 6 Plotly chart types with no extra LLM calls. | | |
| | π **Persona-driven Domain Expert** | Adopts a credentialed persona per domain ("Dr. Sarah Chen, Harvard physician" for healthcare; "Marcus Reid, ex-Goldman analyst" for finance) β sharper than a generic "you are an expert" prompt. | | |
| | β **Dedicated Fact Checker** | Extracts specific claims, runs independent search evidence per claim, labels each: Verified / Partially True / Disputed / Unverifiable. | | |
| | π **Self-grading + auto-retry** | Quality Gate scores the report 0β10 against five weighted criteria. Below 7 β automatic retry with explicit feedback. Capped at 2 retries to prevent runaway cost. | | |
| | πΌοΈ **Cover runs after scoring** | The SVG cover card is generated *last* β after Quality Gate β so it always shows the real quality score, not zero. | | |
| | π **ReportLab academic PDF** | Flowable single-column layout, deep-blue academic headings, horizontal rule separators, charts embedded directly under their matching report section. | | |
| --- | |
| ## ποΈ Architecture | |
| > **Exact node execution order as wired in `graph/graph_builder.py`:** | |
| ``` | |
| βββββββββββββββββββββββββββββββββββ | |
| β USER QUERY β | |
| ββββββββββββββββββ¬βββββββββββββββββ | |
| β | |
| βΌ | |
| ββββββββββββββββββββ | |
| β π§ PLANNER β | |
| β β | |
| β β’ Detects domain β | |
| β β’ Builds 3 tasks β | |
| ββββββββββ¬ββββββββββ | |
| β | |
| βΌ | |
| ββββββββββββββββββββ | |
| β π ROUTER β | |
| β β | |
| β Single LLM call β | |
| β β active_agents β | |
| β list in state β | |
| ββββββββββ¬ββββββββββ | |
| β | |
| βΌ | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β βοΈ WORKERS NODE β | |
| β Only activated agents run β | |
| β β | |
| β ββββββββββββββ ββββββββββββββ ββββββββββββββββββββ β | |
| β β πResearch β β πStats β β πDomain Expert β β | |
| β β β β β β β β | |
| β β 3 Tavily β β Structured β β Persona prompt β β | |
| β β searches β β JSON β β β per domain β β | |
| β β β LLM β β chart data β β β β | |
| β βββββββ¬βββββββ βββββββ¬βββββββ ββββββββββ¬ββββββββββ β | |
| β β β β β | |
| β βββββββ΄βββββββ βββββββ΄βββββββ β β | |
| β β β Fact β β πRefs β β β | |
| β β Checker β β β β β | |
| β β Claim-by- β β APA format β β β | |
| β β claim β β citations β β β | |
| β βββββββ¬βββββββ βββββββ¬βββββββ β β | |
| β β β β β | |
| β βββββββββββββββββ΄βββββββ¬βββββββββββββ β | |
| β Virtual File System β | |
| β (shared dict living in state) β | |
| βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ | |
| β | |
| βΌ | |
| ββββββββββββββββββββ | |
| β π VISUALIZATION β | |
| β β | |
| β Statistics JSON β | |
| β β 6 Plotly chartsβ | |
| β (no LLM call) β | |
| ββββββββββ¬ββββββββββ | |
| β | |
| βΌ | |
| ββββββββββββββββββββ | |
| β βοΈ WRITER β | |
| β β | |
| β Reads all 5 VFS β | |
| β files β builds β | |
| β final report β | |
| ββββββββββ¬ββββββββββ | |
| β | |
| βΌ | |
| ββββββββββββββββββββ | |
| ββββββββββ€ π QUALITY GATE βββββββββββββββββββββββ | |
| β β β β | |
| β β LLM scores 0-10 β β | |
| β ββββββββββ¬ββββββββββ β | |
| β β β | |
| β score β₯ 7 β score < 7 (max 2 retries) β | |
| β ββββββββββ ββββββββββββββββββββββββΊ Writer | |
| β βΌ | |
| β ββββββββββββββββββββ | |
| β β πΌοΈ COVER β | |
| β β β | |
| β β SVG card with β | |
| β β real score baked β | |
| β β in (runs last) β | |
| β ββββββββββ¬ββββββββββ | |
| β β | |
| ββββββββββββ | |
| β | |
| βΌ | |
| STREAMLIT UI | |
| ββββββββββββββββββββββββββββββββββ | |
| β π¬ Research β π Data&Charts β | |
| β π Referencesβ π Report β | |
| β β β±οΈ Trace β | |
| ββββββββββββββββββββββββββββββββββ | |
| MD Β· CSV Β· PDF downloads | |
| ``` | |
| --- | |
| ## β‘ How It Works β Real Example | |
| **Query typed:** `"Impact of AI on healthcare market growth: adoption rates, investment trends and future outlook"` | |
| | Step | Node | What happens | | |
| |---|---|---| | |
| | 1 | **Planner** | Detects `topic_domain = "healthcare"`. Creates tasks: search clinical data, find market numbers, locate key organisations. | | |
| | 2 | **Router** | One LLM call β `["research", "statistics", "domain_expert", "fact_checker", "citation"]`. All 5 activate because healthcare needs everything. | | |
| | 3a | **Research** | Runs 3 Tavily searches: raw query + "latest developments 2025" + "key findings evidence". LLM synthesises into structured deep-research. Writes `research_deep.txt` to VFS. | | |
| | 3b | **Statistics** | Searches for "$45B market size", "29% CAGR" etc. Forces LLM to return exact JSON schema (metrics array + trends array + comparisons array). Writes `statistics.txt` + `statistics_data.csv`. | | |
| | 3c | **Domain Expert** | Persona: *"Dr. Sarah Chen, Harvard-trained physician"*. Reads `research_deep.txt`, adds clinical nuance. Writes `expert_analysis.txt`. | | |
| | 3d | **Fact Checker** | Extracts 6 specific claims. Verifies 4 individually via fresh Tavily searches. Labels β /β οΈ/β. Writes `fact_check.txt`. | | |
| | 3e | **References** | Formats all sources from `raw_search.txt` into APA JSON. Writes `references.txt`. Returns structured `citations` list. | | |
| | 4 | **Visualization** | No LLM call. Reads `structured_data` dict β builds 6 Plotly figures β stores as JSON strings in `chart_json`. | | |
| | 5 | **Writer** | Checks each VFS key. Builds context from whichever files exist. One large LLM call produces 9-section markdown report. | | |
| | 6 | **Quality Gate** | LLM scores the report: **7.5/10**. `should_retry()` returns `"done"`. Routes to Cover. | | |
| | 7 | **Cover** | Now has `quality_score = 7.5`. Generates SVG card with real score baked in. | | |
| | **End** | **UI** | 5 sections rendered. Download MD + CSV + PDF available. | | |
| --- | |
| ## π Folder Structure | |
| ``` | |
| ResearchPilot AI/ β root folder | |
| β | |
| βββ graph/ β LangGraph nodes (the "brain") | |
| β βββ __init__.py | |
| β βββ state.py β ResearchState TypedDict β shared memory | |
| β β every node reads from and writes to | |
| β βββ llm_factory.py β get_llm() β Groq primary, Gemini fallback | |
| β β every node imports this, never hardcodes | |
| β βββ planner.py β Node 1: detects domain, builds task list | |
| β βββ router.py β Node 2: LLM β active_agents list | |
| β βββ workers.py β Node 3: all 5 specialist agents | |
| β β conditionally called by if "agent" in active_agents | |
| β βββ visualization.py β Node 4: structured_data JSON β 6 Plotly charts | |
| β β (no LLM call β pure Python/Plotly) | |
| β βββ synthesizer.py β Node 5 (displayed as "Writer"): | |
| β β reads VFS β one LLM call β final report | |
| β βββ quality_gate.py β Node 6: scores report + should_retry() function | |
| β β drives the conditional edge / retry loop | |
| β βββ thumbnail.py β Node 7 (displayed as "Cover"): | |
| β β runs AFTER quality_gate so score exists | |
| β βββ graph_builder.py β Wires all 7 nodes + edges + conditional edge | |
| β into one compiled StateGraph object | |
| β | |
| βββ tools/ β Utilities used by nodes (not nodes themselves) | |
| β βββ __init__.py | |
| β βββ search_tool.py β web_search() wraps Tavily API | |
| β β returns plain string for LLM context | |
| β βββ file_system.py β vfs_write() / vfs_read() helpers | |
| β β copy-on-write to keep state immutable | |
| β βββ pdf_generator.py β ReportLab flowable PDF | |
| β strips duplicate References section, | |
| β embeds charts under matching headings | |
| β | |
| βββ ui/ | |
| β βββ app.py β Single-page Streamlit UI (~800 lines) | |
| β st.session_state caches results so | |
| β download clicks don't re-run the pipeline | |
| β | |
| βββ .streamlit/ | |
| β βββ config.toml β Dark theme + port 7860 for HF Spaces | |
| β | |
| βββ Dockerfile β Production Docker image for HF Spaces | |
| βββ main.py β Terminal test runner (no UI) | |
| βββ app.py β HF Spaces entry point | |
| βββ requirements.txt | |
| βββ .env.example β API key template | |
| βββ .gitignore β Excludes .env, __pycache__, *.pdf | |
| βββ README.md | |
| ``` | |
| --- | |
| ## π€ The Five Agents | |
| | Agent | File | Activates when | What it produces | | |
| |---|---|---|---| | |
| | **Research** | `workers.py` | Always | 3-angle Tavily search + LLM synthesis β `research_deep.txt` | | |
| | **Statistics** | `workers.py` | Topic has numbers/trends | Structured JSON (metrics + trends + comparisons) + CSV β `statistics.txt` | | |
| | **Domain Expert** | `workers.py` | Specialised topic | Persona-primed analysis β `expert_analysis.txt` | | |
| | **Fact Checker** | `workers.py` | Claims need verification | Per-claim Tavily verify β `fact_check.txt` | | |
| | **References** | `workers.py` | Citations add value | APA-formatted JSON list β `references.txt` | | |
| All agents share data through the **Virtual File System** β a `Dict[str, str]` inside LangGraph state. Zero disk I/O. | |
| --- | |
| ## π The Six Chart Types | |
| All generated from Statistics agent JSON with zero extra LLM calls: | |
| | # | Chart | Driven by | | |
| |---|---|---| | |
| | 1 | **Horizontal Bar** | `metrics` array | | |
| | 2 | **Multi-series Area / Trend** | `trends[].data_points` | | |
| | 3 | **Donut / Pie** | `comparisons` array | | |
| | 4 | **Gauge** | First 0β100 metric value | | |
| | 5 | **Bubble Scatter** | All `metrics` (size = relative value) | | |
| | 6 | **Year-on-Year Bar** | `trends[0].data_points` | | |
| --- | |
| ## π Getting API Keys (Free) | |
| ### Groq β Primary LLM β Required | |
| Fast open-source inference (14,400 free requests/day): | |
| 1. [console.groq.com](https://console.groq.com) β Sign up β API Keys β Create | |
| 2. Copy: `gsk_...` | |
| ### Tavily β Web Search β Required | |
| Real-time web search for agents: | |
| 1. [app.tavily.com](https://app.tavily.com) β Sign up β Dashboard | |
| 2. Copy: `tvly-...` | |
| 3. Free: 1,000 searches/month | |
| ### Google Gemini β Fallback LLM (optional) | |
| Only needed if no Groq key: | |
| 1. [aistudio.google.com](https://aistudio.google.com) β Get API Key | |
| 2. Copy: `AIzaSy...` | |
| ### LangSmith β Tracing (optional but recommended) | |
| See every node execution, LLM call, and retry as a visual timeline: | |
| 1. [smith.langchain.com](https://smith.langchain.com) β Settings β API Keys β Create | |
| 2. Copy: `lsv2_...` | |
| --- | |
| ## βοΈ Installation & Setup | |
| **Prerequisites:** Python 3.11+, Git | |
| ```bash | |
| # 1. Clone | |
| git clone https://github.com/vishal815/ResearchPilot-AI.git | |
| cd ResearchPilot-AI | |
| # 2. Virtual environment | |
| python -m venv .venv | |
| # 3. Activate | |
| # Windows: | |
| .venv\Scripts\activate | |
| # Mac/Linux: | |
| source .venv/bin/activate | |
| # 4. Install dependencies | |
| pip install -r requirements.txt | |
| # 5. Set up API keys | |
| cp .env.example .env | |
| # Now open .env and fill in your keys | |
| ``` | |
| Your `.env` file: | |
| ```env | |
| GROQ_API_KEY=gsk_your_key_here | |
| TAVILY_API_KEY=tvly_your_key_here | |
| # GOOGLE_API_KEY=AIzaSy_your_key_here (optional fallback) | |
| # LANGCHAIN_API_KEY=lsv2_your_key_here (optional tracing) | |
| # LANGCHAIN_TRACING_V2=true | |
| # LANGCHAIN_PROJECT=researchpilot-ai | |
| ``` | |
| --- | |
| ## βΆοΈ Running the Project | |
| ### Terminal mode (test the pipeline first) | |
| ```bash | |
| python main.py | |
| ``` | |
| Output: | |
| ``` | |
| ============================================================ | |
| ResearchPilot AI | |
| Query: Impact of AI on healthcare market growth... | |
| ============================================================ | |
| [NODE: PLANNER] Domain detected: healthcare | 3 tasks created | |
| [NODE: ROUTER] Activated: ['research', 'statistics', 'domain_expert', 'fact_checker', 'citation'] | |
| [NODE: WORKERS] Research done | Statistics: 8 metrics, 3 trends | Expert done | FactCheck done | Refs: 8 sources | |
| [NODE: VISUALIZATION] 6 charts generated | |
| [NODE: WRITER] Report generated | |
| [NODE: QUALITY_GATE] Score: 7.5/10 | |
| [NODE: COVER] SVG cover card generated | |
| ============================================================ | |
| Total time: 52.3s | Saved to output_report_v2.md | |
| ``` | |
| ### Streamlit Web UI | |
| ```bash | |
| streamlit run ui/app.py | |
| ``` | |
| Opens at `http://localhost:8501` | |
| --- | |
| ## π³ Deploy to Hugging Face Spaces | |
| The project ships with a ready `Dockerfile` that targets port 7860 (Hugging Face's required port). | |
| ### Step 1 β Push to GitHub | |
| ```bash | |
| git add . | |
| git commit -m "Initial ResearchPilot AI upload" | |
| git push origin main | |
| ``` | |
| ### Step 2 β Create a Hugging Face Space | |
| 1. Go to [huggingface.co/spaces](https://huggingface.co/spaces) | |
| 2. Click **Create new Space** | |
| 3. Set: | |
| - **Space name:** `ResearchPilot-AI` | |
| - **License:** MIT | |
| - **SDK:** **Docker** β important, not Streamlit | |
| - **Visibility:** Public | |
| ### Step 3 β Add API Keys as Secrets | |
| > β οΈ Never put real API keys in code or `Dockerfile`. Use HF Secrets β they're injected as environment variables at runtime, invisible in the repo. | |
| Go to your Space β **Settings** β **Repository secrets** β **New secret**: | |
| | Secret Name | Value | Required? | | |
| |---|---|---| | |
| | `GROQ_API_KEY` | Your Groq key (`gsk_...`) | β Required | | |
| | `TAVILY_API_KEY` | Your Tavily key (`tvly_...`) | β Required | | |
| | `LANGCHAIN_API_KEY` | Your LangSmith key (`lsv2_...`) | Optional | | |
| | `LANGCHAIN_TRACING_V2` | `true` | Optional (with LangSmith) | | |
| | `GOOGLE_API_KEY` | Your Gemini key | Optional fallback | | |
| **Why secrets, not `.env`?** HF Spaces clones your repo publicly β your `.env` must never be committed (it's in `.gitignore`). Secrets are stored encrypted in HF's vault and injected at container start time. | |
| Your `Dockerfile` already sets: | |
| ```dockerfile | |
| ENV LANGCHAIN_PROJECT="researchpilot-ai" | |
| ``` | |
| This constant is safe to hardcode because it's not sensitive β it's just the project name in LangSmith's dashboard. | |
| The `LANGCHAIN_TRACING_V2=true` env var activates LangSmith auto-tracing β set it as a secret alongside your `LANGCHAIN_API_KEY`. Once both are set, every run appears in your [LangSmith dashboard](https://smith.langchain.com) showing exactly which node ran, how long each LLM call took, and the full prompt/response for every agent call. | |
| ### Step 4 β Link GitHub Repo | |
| In your Space β **Files** β **Connect to GitHub repo** β select your repo β HF auto-builds on every push. | |
| Or push directly to the HF Space's git remote: | |
| ```bash | |
| git remote add hf https://huggingface.co/spaces/YOUR_HF_USERNAME/ResearchPilot-AI | |
| git push hf main | |
| ``` | |
| ### Step 5 β Wait for Build | |
| HF Spaces shows a build log. The first build takes ~3-5 minutes (downloads dependencies). After that, code-only pushes rebuild in ~60 seconds. | |
| --- | |
| ## π Concepts Covered | |
| | Concept | Where in code | One-line description | | |
| |---|---|---| | |
| | **Agentic AI** | All nodes | Agents decide and act, not just reply | | |
| | **LangGraph StateGraph** | `graph_builder.py` | Stateful multi-node orchestration with cycles | | |
| | **Conditional Edge / Routing** | `graph_builder.py` + `quality_gate.py` | `should_retry()` returns string β LangGraph picks next node | | |
| | **Shared State (TypedDict)** | `state.py` | One dict flows through every node β how agents "talk" | | |
| | **Tool Use** | `search_tool.py` | Agents calling Tavily external API | | |
| | **Virtual File System** | `file_system.py` | Dict-as-filesystem for zero-I/O context sharing | | |
| | **Persona Priming** | `workers.py` | Named, credentialed identity β sharper domain output | | |
| | **Structured Output Forcing** | `workers.py` | Force LLM to return exact JSON schema for deterministic chart rendering | | |
| | **LLM-as-Judge** | `quality_gate.py` | Separate LLM call evaluates the first LLM's output | | |
| | **Self-Correction Loop** | `quality_gate.py` | Cycle in graph capped by `retry_count` | | |
| | **RAG** | `workers.py` | Search first β synthesise over retrieved context | | |
| | **Flowable PDF** | `pdf_generator.py` | ReportLab auto-layout vs. manual x/y coordinate mess | | |
| | **Streamlit Session State** | `ui/app.py` | Cache results across reruns so downloads don't re-trigger agents | | |
| --- | |
| ## π§ Troubleshooting | |
| | Error | Cause | Fix | | |
| |---|---|---| | |
| | `ModuleNotFoundError` | venv not activated | `.venv\Scripts\activate` then `pip install -r requirements.txt` | | |
| | `ImportError: cannot import name 'web_search'` | Old `search_tool.py` | Confirm file has `def web_search(query, max_results=5):` | | |
| | Charts missing / only 1 chart | rgba color bug (old version) | Make sure you have the latest `visualization.py` | | |
| | Cover shows `Score: 0.0` | Old graph wiring (cover before quality_gate) | Make sure you have the latest `graph_builder.py` | | |
| | PDF has two References sections | LLM writes its own + structured list | Make sure you have latest `pdf_generator.py` with `_strip_references_section()` | | |
| | `kaleido` error in PDF | Wrong kaleido version | `pip install kaleido==0.2.1` (pin to 0.2.1 exactly) | | |
| | Page "resets" on download | Old `app.py` without session_state cache | Make sure you have latest `ui/app.py` | | |
| | `429` rate limit | Hit LLM free tier | Wait 60s and retry, or switch to Gemini fallback | | |
| --- | |
| ## π£οΈ Roadmap | |
| - [ ] AI-generated cover image via Pollinations AI (free, no key needed) | |
| - [ ] LangGraph checkpointing for session memory across multiple queries | |
| - [ ] Upload a PDF/document as additional research context | |
| - [ ] Model selector in UI (Groq / Gemini / OpenRouter) | |
| - [ ] Auto-generate PowerPoint slide deck from the report | |
| - [ ] LangSmith evaluation dashboard integration | |
| --- | |
| ## π License | |
| MIT License β free to use, modify, and distribute with attribution. | |
| --- | |
| ## π Acknowledgements | |
| [LangGraph](https://langchain-ai.github.io/langgraph/) Β· [Groq](https://groq.com) Β· [Google Gemini](https://aistudio.google.com) Β· [Tavily](https://tavily.com) Β· [Plotly](https://plotly.com) Β· [Streamlit](https://streamlit.io) Β· [ReportLab](https://www.reportlab.com) Β· [LangSmith](https://smith.langchain.com) | |
| --- | |
| ## π¨βπ» Connect With Me | |
| <div align="center"> | |
| [π Portfolio](https://vishal-lazrus-portfolio.vercel.app/) β’ | |
| [πΌ LinkedIn](https://www.linkedin.com/in/vishal-lazrus/) β’ | |
| [π GitHub](https://github.com/vishal815) | |
| **If this helped you learn something, give it a β on GitHub!** | |
| *Built by Vishal Lazrus during AI Internship at Infosys, June 2026* | |
| </div> |