Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 454 additions & 1 deletion README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Agent system for the LinkedIn Automation Platform."""
38 changes: 38 additions & 0 deletions agents/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Coordinator agent for orchestrating the multi-agent workflow."""

from __future__ import annotations

from typing import Any

from .sub_agents.browser import BrowserAgent
from .sub_agents.classifier import ClassifierAgent
from .sub_agents.enricher import EnricherAgent
from .sub_agents.scorer import ScorerAgent
from .sub_agents.scraper import ScraperAgent
from .sub_agents.searcher import SearcherAgent


class CoordinatorAgent:
"""High level agent responsible for coordinating sub-agents."""

def __init__(self) -> None:
self.scraper = ScraperAgent()
self.classifier = ClassifierAgent()
self.searcher = SearcherAgent()
self.enricher = EnricherAgent()
self.scorer = ScorerAgent()
self.browser = BrowserAgent()

async def start_workflow(self, startup_url: str) -> Any:
"""Execute the full investor discovery workflow.

The implementation is simplified and primarily demonstrates how the
various sub-agents are expected to interact.
"""

scraped = await self.scraper.scrape(startup_url)
classification = await self.classifier.classify(scraped.get("text", ""))
provider_results = await self.searcher.parallel_search(classification)
enriched = await self.enricher.enrich([])
scored = self.scorer.score(enriched)
return scored
1 change: 1 addition & 0 deletions agents/sub_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Specialised sub-agents used by the coordinator."""
11 changes: 11 additions & 0 deletions agents/sub_agents/browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Browser automation agent using Model Context Protocol (MCP)."""

from __future__ import annotations


class BrowserAgent:
async def navigate(self, url: str) -> str:
"""Navigate to a URL and return page content."""

# Placeholder for MCP browser automation
return ""
13 changes: 13 additions & 0 deletions agents/sub_agents/classifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Classifier agent for industry, stage and geography detection."""

from __future__ import annotations

from typing import Dict


class ClassifierAgent:
async def classify(self, text: str) -> Dict:
"""Return a lightweight classification for the provided text."""

# Placeholder for AI classification
return {}
13 changes: 13 additions & 0 deletions agents/sub_agents/enricher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Enricher agent that augments investor data using Apollo.io."""

from __future__ import annotations

from typing import Dict, List


class EnricherAgent:
async def enrich(self, investors: List[Dict]) -> List[Dict]:
"""Enrich investor records with contact information."""

# Placeholder for Apollo integration
return investors
13 changes: 13 additions & 0 deletions agents/sub_agents/scorer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Scorer agent implementing the Smart-Money Radar formula."""

from __future__ import annotations

from typing import Dict, List


class ScorerAgent:
def score(self, investors: List[Dict]) -> List[Dict]:
"""Apply the custom scoring algorithm to investors."""

# Placeholder scoring - returns investors unchanged
return investors
13 changes: 13 additions & 0 deletions agents/sub_agents/scraper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Scraper agent using Apify to extract website content."""

from __future__ import annotations

from typing import Dict


class ScraperAgent:
async def scrape(self, url: str) -> Dict[str, str]:
"""Scrape the startup homepage and return HTML and text."""

# Placeholder for Apify integration
return {"html": "", "text": ""}
34 changes: 34 additions & 0 deletions agents/sub_agents/searcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Searcher agent responsible for querying multiple AI providers."""

from __future__ import annotations

import asyncio
from typing import Dict, List


class SearcherAgent:
async def parallel_search(self, classification: Dict) -> Dict[str, Dict]:
"""Run searches across all available providers in parallel."""

search_prompt = "" # would be generated from the classification
tasks = {
"claude": self._search_claude(search_prompt),
"gemini": self._search_gemini(search_prompt),
"grok": self._search_grok(search_prompt),
}
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
return {name: result for name, result in zip(tasks.keys(), results)}

async def _search_claude(self, prompt: str) -> Dict:
return {}

async def _search_gemini(self, prompt: str) -> Dict:
return {}

async def _search_grok(self, prompt: str) -> Dict:
return {}

def _deduplicate_investors(self, investors: List[Dict]) -> List[Dict]:
"""Name normalization and similarity matching."""

return investors
159 changes: 159 additions & 0 deletions api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""FastAPI server for the LinkedIn Automation Platform.

This module exposes the public API used to orchestrate investor
workflows. The implementation intentionally keeps state in memory and
focuses on demonstrating the request/response contract described in the
project documentation.
"""

from __future__ import annotations

import asyncio
from typing import Dict, List, Optional
from uuid import uuid4

from fastapi import BackgroundTasks, FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

app = FastAPI(title="LinkedIn Automation Platform")

# ----------------------------------------------------------------------------
# In-memory storage used for the demo implementation. A real deployment would
# persist this information in a database and coordinate with the agent system.
# ----------------------------------------------------------------------------
workflows: Dict[str, Dict] = {}
progress_updates: Dict[str, int] = {}
budget_config: Dict[str, float] = {}

AVAILABLE_PROVIDERS = ["claude", "openai", "gemini", "grok"]
TEMPLATES = {
"claude": "investor_search_claude",
"openai": "investor_search_openai",
"gemini": "investor_search_gemini",
"grok": "investor_search_grok",
}


class WorkflowRequest(BaseModel):
"""Request body for starting a workflow."""

startup_url: str
budget_cap: Optional[float] = None
providers: List[str] = []
max_investors: Optional[int] = None


@app.post("/start_workflow")
async def start_workflow(
request: WorkflowRequest, background_tasks: BackgroundTasks
) -> Dict[str, str]:
"""Create a new investor discovery workflow.

The heavy lifting would normally be delegated to the coordinator agent
through a background task. Here we only register a stub workflow entry.
"""

workflow_id = f"wf_{uuid4().hex}"
workflows[workflow_id] = {
"id": workflow_id,
"status": "running",
"stage": "initialized",
"progress": 0,
"cost_used": 0.0,
"investors_found": 0,
}
progress_updates[workflow_id] = 0
# background_tasks.add_task(coordinator.start_workflow, workflow_id, request)
return {"id": workflow_id}


@app.get("/workflow/{workflow_id}/status")
async def workflow_status(workflow_id: str) -> Dict:
"""Return the status of a workflow."""

wf = workflows.get(workflow_id)
if wf is None:
raise HTTPException(status_code=404, detail="Workflow not found")
return wf


@app.post("/workflow/{workflow_id}/stop")
async def stop_workflow(workflow_id: str) -> Dict:
"""Stop a running workflow."""

wf = workflows.get(workflow_id)
if wf is None:
raise HTTPException(status_code=404, detail="Workflow not found")
wf["status"] = "stopped"
return wf


class ExpansionRequest(BaseModel):
"""Simple body for an expansion request."""

query: str


@app.post("/expansion/start")
async def expansion_start(request: ExpansionRequest) -> Dict[str, str]:
expansion_id = f"ex_{uuid4().hex}"
workflows[expansion_id] = {
"id": expansion_id,
"status": "running",
"stage": "expansion",
"progress": 0,
}
return {"id": expansion_id, "status": "started"}


@app.get("/expansion/{expansion_id}/status")
async def expansion_status(expansion_id: str) -> Dict[str, str]:
wf = workflows.get(expansion_id)
if wf is None:
raise HTTPException(status_code=404, detail="Expansion not found")
return wf


@app.get("/expansion/{expansion_id}/results")
async def expansion_results(expansion_id: str) -> Dict:
if expansion_id not in workflows:
raise HTTPException(status_code=404, detail="Expansion not found")
return {"id": expansion_id, "results": []}


@app.get("/progress/{job_id}")
async def get_progress(job_id: str) -> Dict[str, int]:
progress = progress_updates.get(job_id, 0)
return {"job_id": job_id, "progress": progress}


@app.websocket("/ws/progress/{job_id}")
async def websocket_progress(websocket: WebSocket, job_id: str) -> None:
await websocket.accept()
try:
while True:
progress = progress_updates.get(job_id, 0)
await websocket.send_json({"job_id": job_id, "progress": progress})
await asyncio.sleep(1)
except WebSocketDisconnect:
pass


@app.get("/config/providers")
async def config_providers() -> Dict[str, List[str]]:
return {"providers": AVAILABLE_PROVIDERS}


class BudgetConfig(BaseModel):
budget: float


@app.post("/config/budget")
async def set_budget(config: BudgetConfig) -> Dict[str, float]:
budget_config["budget"] = config.budget
return {"budget": config.budget}


@app.get("/config/templates")
async def get_templates() -> Dict[str, Dict]:
return {"templates": TEMPLATES}
66 changes: 66 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
-- Supabase/PostgreSQL schema for the LinkedIn Automation Platform

-- User Management
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
subscription_tier TEXT
);

-- Job Management
CREATE TABLE IF NOT EXISTS search_jobs (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
startup_url TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS analysis_results (
id UUID PRIMARY KEY,
job_id UUID REFERENCES search_jobs(id),
provider TEXT NOT NULL,
response_data JSONB,
score NUMERIC
);

-- Smart-Money Radar v4 Tables
CREATE TABLE IF NOT EXISTS prompt_runs (
id UUID PRIMARY KEY,
job_id UUID REFERENCES search_jobs(id),
provider TEXT NOT NULL,
prompt_template TEXT,
response JSONB,
cost NUMERIC
);

CREATE TABLE IF NOT EXISTS prompt_templates (
id UUID PRIMARY KEY,
provider TEXT NOT NULL,
template_type TEXT,
content TEXT,
is_active BOOLEAN DEFAULT TRUE
);

CREATE TABLE IF NOT EXISTS investor_watchlist (
id UUID PRIMARY KEY,
investor_name TEXT,
firm TEXT,
linkedin_url TEXT,
added_date DATE DEFAULT CURRENT_DATE
);

CREATE TABLE IF NOT EXISTS liquidity_events (
id UUID PRIMARY KEY,
investor_id UUID REFERENCES investor_watchlist(id),
event_type TEXT,
description TEXT,
detected_date DATE
);

-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_search_jobs_user ON search_jobs(user_id);
CREATE INDEX IF NOT EXISTS idx_prompt_runs_job ON prompt_runs(job_id);
CREATE INDEX IF NOT EXISTS idx_analysis_results_job ON analysis_results(job_id);

1 change: 1 addition & 0 deletions integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""External integration modules (Apify, Apollo, MCP)."""
12 changes: 12 additions & 0 deletions integrations/apify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Apify integration helpers."""

from __future__ import annotations

from typing import Dict


async def web_scrape(url: str) -> Dict[str, str]:
"""Sync scrape a startup homepage and return its HTML and text content."""

# Placeholder for Apify API calls
return {"html": "", "text": ""}
Loading