app V1.0
Browse files- .gitignore +4 -0
- Dockerfile +23 -0
- README.md +102 -1
- backend/main.py +127 -0
- backend/utils.py +110 -0
- dashboard.txt +463 -0
- fello_crew/.gitignore +3 -0
- fello_crew/AGENTS.md +1017 -0
- fello_crew/README.md +54 -0
- fello_crew/knowledge/user_preference.txt +2 -0
- fello_crew/pyproject.toml +24 -0
- fello_crew/src/fello_crew/__init__.py +0 -0
- fello_crew/src/fello_crew/crew.py +119 -0
- fello_crew/src/fello_crew/main.py +37 -0
- fello_crew/src/fello_crew/models.py +18 -0
- fello_crew/src/fello_crew/tools/__init__.py +0 -0
- fello_crew/src/fello_crew/tools/custom_tool.py +174 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.css +184 -0
- frontend/src/App.jsx +9 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/Dashboard.jsx +249 -0
- frontend/src/index.css +27 -0
- frontend/src/main.jsx +10 -0
- frontend/vite.config.js +10 -0
- requirements.txt +163 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
.env
|
| 3 |
+
__pycache__
|
| 4 |
+
service_account_credentials.json
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build React
|
| 2 |
+
FROM node:18 AS build-step
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Python Backend
|
| 10 |
+
FROM python:3.11-slim
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
COPY backend/requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
COPY --from=build-step /app/frontend/dist /app/frontend/dist
|
| 19 |
+
|
| 20 |
+
ENV PYTHONPATH=/app
|
| 21 |
+
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1 +1,102 @@
|
|
| 1 |
-
# fello-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# fello-IQ
|
| 2 |
+
|
| 3 |
+
Fello IQ is an automated account intelligence platform that utilizes AI agents to identify, enrich, and analyze corporate leads based on IP addresses or domain names. It synthesizes data from multiple sources to provide intent scores, technographics, and actionable sales strategies.
|
| 4 |
+
|
| 5 |
+
# System Architecture
|
| 6 |
+
The project is divided into three primary modules:
|
| 7 |
+
|
| 8 |
+
Backend (FastAPI): Handles API requests, serves the React frontend, generates PDF reports, and manages Google Sheets synchronization.
|
| 9 |
+
|
| 10 |
+
Fello Crew (CrewAI): An autonomous agentic layer that performs account identification, deep enrichment (via Apollo and News APIs), and strategic analysis.
|
| 11 |
+
|
| 12 |
+
Frontend (React): A Vite-powered dashboard for real-time interaction and visualization of intelligence streams.
|
| 13 |
+
|
| 14 |
+
# Core Technologies
|
| 15 |
+
- LLM: Mistral (mistral-large-latest).
|
| 16 |
+
|
| 17 |
+
- Frameworks: FastAPI (Python), React (JavaScript), CrewAI.
|
| 18 |
+
|
| 19 |
+
- Data Sources: Apollo.io, ip-api.com, custom scraping tools.
|
| 20 |
+
|
| 21 |
+
# Prerequisites
|
| 22 |
+
- Python 3.10 or higher.
|
| 23 |
+
|
| 24 |
+
- Node.js 18 or higher.
|
| 25 |
+
|
| 26 |
+
- Google Cloud Service Account (for Sheets sync).
|
| 27 |
+
|
| 28 |
+
- Apollo.io API Key.
|
| 29 |
+
|
| 30 |
+
# Environment Variables
|
| 31 |
+
Create a .env file in the root directory and the backend/ directory with the following keys:
|
| 32 |
+
|
| 33 |
+
```.env
|
| 34 |
+
# AI and Search
|
| 35 |
+
OPENAI_API_KEY=your_openai_key
|
| 36 |
+
SERPER_API_KEY=your_serper_key
|
| 37 |
+
APOLLO_TOKEN=your_apollo_token
|
| 38 |
+
|
| 39 |
+
# Google Integration
|
| 40 |
+
SPREADSHEET_ID=your_google_sheet_id
|
| 41 |
+
GOOGLE_CREDENTIALS_JSON=your_minified_service_account_json
|
| 42 |
+
```
|
| 43 |
+
# Local Setup
|
| 44 |
+
1. Backend and Crew Setup
|
| 45 |
+
From the root directory, install the shared Python dependencies:
|
| 46 |
+
|
| 47 |
+
```Bash
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
```
|
| 50 |
+
# 2. Frontend Setup
|
| 51 |
+
Navigate to the frontend directory and install dependencies:
|
| 52 |
+
|
| 53 |
+
```Bash
|
| 54 |
+
cd frontend
|
| 55 |
+
npm install
|
| 56 |
+
Running the Application Locally
|
| 57 |
+
```
|
| 58 |
+
You will need two terminal instances to run the full stack during development.
|
| 59 |
+
|
| 60 |
+
Terminal 1: Backend API
|
| 61 |
+
From the root directory:
|
| 62 |
+
|
| 63 |
+
```Bash
|
| 64 |
+
python -m backend.main
|
| 65 |
+
```
|
| 66 |
+
The API will start at http://localhost:8000.
|
| 67 |
+
|
| 68 |
+
Terminal 2: Frontend Development Server
|
| 69 |
+
From the frontend directory:
|
| 70 |
+
|
| 71 |
+
```Bash
|
| 72 |
+
npm run dev
|
| 73 |
+
```
|
| 74 |
+
The UI will start at http://localhost:5173.
|
| 75 |
+
|
| 76 |
+
# Data Models and Tools
|
| 77 |
+
Intelligence Model
|
| 78 |
+
The system enforces a strict Pydantic schema for all analysis, including:
|
| 79 |
+
|
| 80 |
+
- Intent Score (0.0 - 10.0).
|
| 81 |
+
|
| 82 |
+
- Likely Persona.
|
| 83 |
+
|
| 84 |
+
- Leadership Team.
|
| 85 |
+
|
| 86 |
+
- Technographics (Tech Stack).
|
| 87 |
+
|
| 88 |
+
- Recommended Sales Actions.
|
| 89 |
+
|
| 90 |
+
# Integrated Tools
|
| 91 |
+
- GetDomain: Resolves raw IP addresses to corporate entities.
|
| 92 |
+
|
| 93 |
+
- ApolloEnrichmentTool: Fetches firmographics and technographics.
|
| 94 |
+
|
| 95 |
+
- JobOpeningsTool: Monitors hiring signals as growth indicators using apollo API.
|
| 96 |
+
|
| 97 |
+
- NewsTool: Tracks recent corporate events and press releases.
|
| 98 |
+
|
| 99 |
+
# PDF Generation and Syncing
|
| 100 |
+
- PDF Reports: Generated server-side using fpdf, sanitizing data for Latin-1 compatibility.
|
| 101 |
+
|
| 102 |
+
- Google Sheets: Data is appended to "Sheet1" of the specified SPREADSHEET_ID using the Google Sheets API v4.
|
backend/main.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import asyncio
|
| 5 |
+
from fastapi import FastAPI, BackgroundTasks, Response, HTTPException
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from fastapi.responses import StreamingResponse
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
from typing import Optional, List, Dict
|
| 10 |
+
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
from fastapi.responses import FileResponse
|
| 13 |
+
import os
|
| 14 |
+
app = FastAPI(title="Fello IQ API")
|
| 15 |
+
|
| 16 |
+
# Adding the src directory to path
|
| 17 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "fello_crew", "src"))
|
| 18 |
+
from fello_crew.crew import FelloAccountIntelligenceCrew
|
| 19 |
+
from backend.utils import FelloUtils
|
| 20 |
+
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")
|
| 21 |
+
|
| 22 |
+
if os.path.exists(frontend_path):
|
| 23 |
+
app.mount("/assets", StaticFiles(directory=os.path.join(frontend_path, "assets")), name="assets")
|
| 24 |
+
|
| 25 |
+
@app.get("/{full_path:path}")
|
| 26 |
+
async def serve_react_app(full_path: str):
|
| 27 |
+
# This allows React Router to handle deep links
|
| 28 |
+
return FileResponse(os.path.join(frontend_path, "index.html"))
|
| 29 |
+
# Enable CORS
|
| 30 |
+
app.add_middleware(
|
| 31 |
+
CORSMiddleware,
|
| 32 |
+
allow_origins=["*"],
|
| 33 |
+
allow_methods=["*"],
|
| 34 |
+
allow_headers=["*"],
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
class AnalysisRequest(BaseModel):
|
| 38 |
+
input_data: Optional[str] = ""
|
| 39 |
+
activity_signals: str = "No recent activity recorded."
|
| 40 |
+
domain: Optional[str] = ""
|
| 41 |
+
|
| 42 |
+
@app.get("/health")
|
| 43 |
+
def health_check():
|
| 44 |
+
return {"status": "operational"}
|
| 45 |
+
|
| 46 |
+
@app.post("/analyze")
|
| 47 |
+
async def analyze_account(request: AnalysisRequest):
|
| 48 |
+
# Determine the primary identifier for the Crew
|
| 49 |
+
# If input_data is empty, use domain as the primary signal
|
| 50 |
+
primary_signal = request.input_data if request.input_data else request.domain
|
| 51 |
+
inputs = {
|
| 52 |
+
"input_data": primary_signal,
|
| 53 |
+
"activity_signals": request.activity_signals,
|
| 54 |
+
"domain": request.domain if request.domain else ""
|
| 55 |
+
}
|
| 56 |
+
print(inputs)
|
| 57 |
+
|
| 58 |
+
crew_instance = FelloAccountIntelligenceCrew().crew()
|
| 59 |
+
# Using to_thread to keep the FastAPI event loop responsive
|
| 60 |
+
result = await asyncio.to_thread(crew_instance.kickoff, inputs=inputs)
|
| 61 |
+
|
| 62 |
+
return result.pydantic
|
| 63 |
+
|
| 64 |
+
@app.post("/analyze-stream")
|
| 65 |
+
async def analyze_account_stream(request: AnalysisRequest):
|
| 66 |
+
# Use whichever identifier is strongest: IP or Domain
|
| 67 |
+
primary_signal = request.input_data if request.input_data else request.domain
|
| 68 |
+
|
| 69 |
+
# These keys MUST match what crew.py expects in its .kickoff()
|
| 70 |
+
inputs = {
|
| 71 |
+
"input_data": primary_signal,
|
| 72 |
+
"domain": request.domain,
|
| 73 |
+
"activity_signals": request.activity_signals
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async def event_generator():
|
| 77 |
+
# 1. Show the user we are starting
|
| 78 |
+
yield f"data: {json.dumps({'message': f'Initializing research for {primary_signal}...'})}\n\n"
|
| 79 |
+
|
| 80 |
+
# 2. Run the CrewAI task
|
| 81 |
+
crew_instance = FelloAccountIntelligenceCrew().crew()
|
| 82 |
+
|
| 83 |
+
# We run this in a thread so it doesn't block the stream
|
| 84 |
+
task = asyncio.to_thread(crew_instance.kickoff, inputs=inputs)
|
| 85 |
+
|
| 86 |
+
yield f"data: {json.dumps({'message': 'Agents dispatched to Apollo and News sources...'})}\n\n"
|
| 87 |
+
await asyncio.sleep(1)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
result = await task
|
| 91 |
+
# Send the final structured result back to the UI
|
| 92 |
+
yield f"data: {json.dumps({'message': 'COMPLETE', 'result': result.pydantic.dict()})}\n\n"
|
| 93 |
+
except Exception as e:
|
| 94 |
+
yield f"data: {json.dumps({'message': f'ERROR: {str(e)}'})}\n\n"
|
| 95 |
+
|
| 96 |
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
| 97 |
+
|
| 98 |
+
@app.post("/generate-report")
|
| 99 |
+
async def generate_report(data: Dict):
|
| 100 |
+
"""Generates the PDF bytes with endpoint-level debugging."""
|
| 101 |
+
try:
|
| 102 |
+
pdf_bytes = FelloUtils.generate_pdf_report(data)
|
| 103 |
+
return Response(
|
| 104 |
+
content=pdf_bytes,
|
| 105 |
+
media_type="application/pdf",
|
| 106 |
+
headers={
|
| 107 |
+
"Content-Disposition": f"attachment; filename=Fello_Report_{data.get('company_name', 'Account')}.pdf",
|
| 108 |
+
"Access-Control-Expose-Headers": "Content-Disposition" # Helps frontend see filename
|
| 109 |
+
}
|
| 110 |
+
)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
# Return the actual error message so the frontend doesn't just get a generic 500
|
| 113 |
+
raise HTTPException(status_code=500, detail=f"PDF Server Error: {str(e)}")
|
| 114 |
+
|
| 115 |
+
@app.post("/sync-sheets")
|
| 116 |
+
async def sync_sheets(data: Dict, background_tasks: BackgroundTasks):
|
| 117 |
+
"""
|
| 118 |
+
Separate intentional endpoint for Google Sheets sync.
|
| 119 |
+
Triggered by a specific button on the frontend.
|
| 120 |
+
"""
|
| 121 |
+
# We still use background_tasks so the UI can show "Syncing..."
|
| 122 |
+
# and immediately confirm without waiting for the Google API.
|
| 123 |
+
background_tasks.add_task(FelloUtils.sync_to_google_sheets, data)
|
| 124 |
+
return {"message": "Sync initiated successfully"}
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
import uvicorn
|
| 127 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
backend/utils.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fpdf import FPDF
|
| 3 |
+
from googleapiclient.discovery import build
|
| 4 |
+
from google.oauth2 import service_account
|
| 5 |
+
from typing import List
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import json
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
class FelloUtils:
|
| 11 |
+
@staticmethod
|
| 12 |
+
def generate_pdf_report(data: dict) -> bytes:
|
| 13 |
+
"""Generates a professional PDF report with heavy debugging."""
|
| 14 |
+
try:
|
| 15 |
+
pdf = FPDF()
|
| 16 |
+
pdf.add_page()
|
| 17 |
+
|
| 18 |
+
# Helper function to sanitize text for Latin-1 (FPDF default)
|
| 19 |
+
def s(text):
|
| 20 |
+
if text is None: return ""
|
| 21 |
+
# Replace common problematic unicode characters that crash FPDF
|
| 22 |
+
return str(text).encode('latin-1', 'replace').decode('latin-1')
|
| 23 |
+
|
| 24 |
+
# 1. Title
|
| 25 |
+
pdf.set_font("Arial", 'B', 20)
|
| 26 |
+
pdf.set_text_color(37, 99, 235)
|
| 27 |
+
pdf.cell(200, 15, txt="Fello IQ Account Intelligence", ln=True, align='C')
|
| 28 |
+
pdf.ln(10)
|
| 29 |
+
|
| 30 |
+
# 2. Company Identity
|
| 31 |
+
pdf.set_font("Arial", 'B', 14)
|
| 32 |
+
pdf.set_text_color(15, 23, 42)
|
| 33 |
+
company = s(data.get('company_name', 'N/A'))
|
| 34 |
+
domain = s(data.get('domain', 'N/A'))
|
| 35 |
+
pdf.cell(0, 10, txt=f"Account: {company} ({domain})", ln=True)
|
| 36 |
+
|
| 37 |
+
pdf.set_font("Arial", size=10)
|
| 38 |
+
industry = s(data.get('industry', 'N/A'))
|
| 39 |
+
revenue = s(data.get('annual_revenue', 'N/A'))
|
| 40 |
+
pdf.cell(0, 7, txt=f"Industry: {industry} | Revenue: {revenue}", ln=True)
|
| 41 |
+
pdf.ln(5)
|
| 42 |
+
|
| 43 |
+
# 3. Strategic Intelligence
|
| 44 |
+
pdf.set_font("Arial", 'B', 12)
|
| 45 |
+
score = data.get('intent_score', 0)
|
| 46 |
+
stage = s(data.get('intent_stage', 'Unknown'))
|
| 47 |
+
pdf.cell(0, 10, txt=f"Intent Score: {score}/10 - {stage}", ln=True)
|
| 48 |
+
|
| 49 |
+
pdf.set_font("Arial", 'I', 10)
|
| 50 |
+
summary = s(data.get('ai_summary', 'No summary available.'))
|
| 51 |
+
pdf.multi_cell(0, 7, txt=f"AI Summary: {summary}")
|
| 52 |
+
pdf.ln(5)
|
| 53 |
+
|
| 54 |
+
# 4. Recommended Actions
|
| 55 |
+
pdf.set_font("Arial", 'B', 12)
|
| 56 |
+
pdf.cell(0, 10, txt="Recommended Sales Actions:", ln=True)
|
| 57 |
+
|
| 58 |
+
pdf.set_font("Arial", size=10)
|
| 59 |
+
actions = data.get('recommended_sales_actions', [])
|
| 60 |
+
if not actions:
|
| 61 |
+
pdf.cell(0, 7, txt="- No specific actions recommended.", ln=True)
|
| 62 |
+
else:
|
| 63 |
+
for action in actions:
|
| 64 |
+
pdf.multi_cell(0, 7, txt=s(f"- {action}"))
|
| 65 |
+
|
| 66 |
+
pdf_output = pdf.output(dest='S')
|
| 67 |
+
return pdf_output.encode('latin-1', 'replace')
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print("!!! PDF GENERATION CRASHED !!!")
|
| 71 |
+
print(traceback.format_exc())
|
| 72 |
+
raise e
|
| 73 |
+
|
| 74 |
+
@staticmethod
|
| 75 |
+
def sync_to_google_sheets(data: dict):
|
| 76 |
+
"""Appends research data to Google Sheets using your reference logic."""
|
| 77 |
+
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
| 78 |
+
SPREADSHEET_ID = os.getenv("SPREADSHEET_ID")
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
creds_json = os.getenv("GOOGLE_CREDENTIALS_JSON")
|
| 82 |
+
creds_info = json.loads(creds_json)
|
| 83 |
+
creds = service_account.Credentials.from_service_account_info(
|
| 84 |
+
creds_info, scopes=SCOPES
|
| 85 |
+
)
|
| 86 |
+
service = build("sheets", "v4", credentials=creds)
|
| 87 |
+
|
| 88 |
+
# Formatting the row based on the Pydantic model
|
| 89 |
+
values = [
|
| 90 |
+
data['company_name'],
|
| 91 |
+
data['domain'],
|
| 92 |
+
data['intent_score'],
|
| 93 |
+
data['intent_stage'],
|
| 94 |
+
data['industry'],
|
| 95 |
+
data['annual_revenue'],
|
| 96 |
+
", ".join(data['recommended_sales_actions'][:2])
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
body = {"values": [values]}
|
| 100 |
+
service.spreadsheets().values().append(
|
| 101 |
+
spreadsheetId=SPREADSHEET_ID,
|
| 102 |
+
range="Sheet1",
|
| 103 |
+
valueInputOption="USER_ENTERED",
|
| 104 |
+
insertDataOption="INSERT_ROWS",
|
| 105 |
+
body=body
|
| 106 |
+
).execute()
|
| 107 |
+
return True
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"Google Sheets Sync Error: {e}")
|
| 110 |
+
return False
|
dashboard.txt
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Search,
|
| 4 |
+
Terminal,
|
| 5 |
+
Activity,
|
| 6 |
+
Globe,
|
| 7 |
+
Users,
|
| 8 |
+
Zap,
|
| 9 |
+
FileText,
|
| 10 |
+
ArrowRight,
|
| 11 |
+
Database,
|
| 12 |
+
Cpu,
|
| 13 |
+
Download,
|
| 14 |
+
ExternalLink,
|
| 15 |
+
Target,
|
| 16 |
+
Linkedin,
|
| 17 |
+
Twitter,
|
| 18 |
+
Building2,
|
| 19 |
+
Code2,
|
| 20 |
+
Settings,
|
| 21 |
+
Bell,
|
| 22 |
+
Sparkles,
|
| 23 |
+
RefreshCw,
|
| 24 |
+
AlertCircle,
|
| 25 |
+
ChevronDown,
|
| 26 |
+
ChevronUp,
|
| 27 |
+
Briefcase,
|
| 28 |
+
Info
|
| 29 |
+
} from 'lucide-react';
|
| 30 |
+
|
| 31 |
+
const App = () => {
|
| 32 |
+
// UI State
|
| 33 |
+
const [activeMode, setActiveMode] = useState('visitor');
|
| 34 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 35 |
+
const [logs, setLogs] = useState([]);
|
| 36 |
+
const [result, setResult] = useState(null);
|
| 37 |
+
const [inputValue, setInputValue] = useState('');
|
| 38 |
+
const [error, setError] = useState(null);
|
| 39 |
+
const [expandedSection, setExpandedSection] = useState('summary'); // Tracking active accordion part
|
| 40 |
+
|
| 41 |
+
const logEndRef = useRef(null);
|
| 42 |
+
const apiKey = ""; // Provided by environment
|
| 43 |
+
|
| 44 |
+
const visitorTemplate = `{
|
| 45 |
+
"visitor_id": "V-9921",
|
| 46 |
+
"ip": "34.201.42.11",
|
| 47 |
+
"path": ["/pricing", "/case-studies/mortgage", "/ai-enrichment"],
|
| 48 |
+
"dwell_time": "5m 12s",
|
| 49 |
+
"visits_this_week": 4
|
| 50 |
+
}`;
|
| 51 |
+
|
| 52 |
+
const companyTemplate = `BrightPath Lending`;
|
| 53 |
+
|
| 54 |
+
useEffect(() => {
|
| 55 |
+
setInputValue(activeMode === 'visitor' ? visitorTemplate : companyTemplate);
|
| 56 |
+
setResult(null);
|
| 57 |
+
setLogs([]);
|
| 58 |
+
setError(null);
|
| 59 |
+
}, [activeMode]);
|
| 60 |
+
|
| 61 |
+
useEffect(() => {
|
| 62 |
+
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 63 |
+
}, [logs]);
|
| 64 |
+
|
| 65 |
+
// Gemini API Integration with Exponential Backoff
|
| 66 |
+
const callGemini = async (prompt, retries = 0) => {
|
| 67 |
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
|
| 68 |
+
|
| 69 |
+
const systemPrompt = `You are a world-class Sales Intelligence & Account Enrichment Agent.
|
| 70 |
+
Analyze the provided input (Visitor JSON or Company Name) and return a detailed, structured intelligence report in JSON format.
|
| 71 |
+
|
| 72 |
+
REQUIRED JSON SCHEMA:
|
| 73 |
+
{
|
| 74 |
+
"company": { "name": string, "domain": string, "industry": string, "size": string, "hq": string, "founded": string, "description": string },
|
| 75 |
+
"intent": { "score": number (0-10), "stage": string, "signals": string[] },
|
| 76 |
+
"persona": { "role": string, "confidence": string, "focus": string },
|
| 77 |
+
"tech": string[],
|
| 78 |
+
"leadership": [{ "name": string, "role": string }],
|
| 79 |
+
"summary": string,
|
| 80 |
+
"action": string
|
| 81 |
+
}`;
|
| 82 |
+
|
| 83 |
+
try {
|
| 84 |
+
const response = await fetch(url, {
|
| 85 |
+
method: 'POST',
|
| 86 |
+
headers: { 'Content-Type': 'application/json' },
|
| 87 |
+
body: JSON.stringify({
|
| 88 |
+
contents: [{ parts: [{ text: `Input to analyze: ${prompt}` }] }],
|
| 89 |
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
| 90 |
+
generationConfig: { responseMimeType: "application/json" }
|
| 91 |
+
})
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
if (!response.ok) {
|
| 95 |
+
if (response.status === 429 && retries < 5) {
|
| 96 |
+
const delay = Math.pow(2, retries) * 1000;
|
| 97 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
| 98 |
+
return callGemini(prompt, retries + 1);
|
| 99 |
+
}
|
| 100 |
+
throw new Error(`API Error: ${response.statusText}`);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const data = await response.json();
|
| 104 |
+
return JSON.parse(data.candidates[0].content.parts[0].text);
|
| 105 |
+
} catch (err) {
|
| 106 |
+
if (retries < 5) {
|
| 107 |
+
const delay = Math.pow(2, retries) * 1000;
|
| 108 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
| 109 |
+
return callGemini(prompt, retries + 1);
|
| 110 |
+
}
|
| 111 |
+
throw err;
|
| 112 |
+
}
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const runAnalysis = async () => {
|
| 116 |
+
setIsProcessing(true);
|
| 117 |
+
setError(null);
|
| 118 |
+
setResult(null);
|
| 119 |
+
setLogs([{ text: "Initializing Fello Intelligence Agent...", type: 'info' }]);
|
| 120 |
+
|
| 121 |
+
const addLog = (text, type = 'info') => {
|
| 122 |
+
setLogs(prev => [...prev, { text, type, id: Date.now() + Math.random() }]);
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
try {
|
| 126 |
+
setTimeout(() => addLog("Performing reverse IP & domain lookup...", 'info'), 800);
|
| 127 |
+
setTimeout(() => addLog("Fetching social signals & firmographics...", 'info'), 1600);
|
| 128 |
+
|
| 129 |
+
const aiResponse = await callGemini(inputValue);
|
| 130 |
+
|
| 131 |
+
addLog("Analysis complete. Structuring intelligence report...", 'success');
|
| 132 |
+
setResult(aiResponse);
|
| 133 |
+
setExpandedSection('summary'); // Reset to summary when new results arrive
|
| 134 |
+
} catch (err) {
|
| 135 |
+
setError("Analysis failed. Please check your input or try again later.");
|
| 136 |
+
addLog("Error encountered during research phase.", 'error');
|
| 137 |
+
} finally {
|
| 138 |
+
setIsProcessing(false);
|
| 139 |
+
}
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
const AccordionItem = ({ id, title, icon: Icon, children }) => {
|
| 143 |
+
const isExpanded = expandedSection === id;
|
| 144 |
+
return (
|
| 145 |
+
<div className={`border-b border-slate-100 last:border-b-0 transition-all ${isExpanded ? 'bg-slate-50/50' : 'bg-white'}`}>
|
| 146 |
+
<button
|
| 147 |
+
onClick={() => setExpandedSection(isExpanded ? null : id)}
|
| 148 |
+
className="w-full px-6 py-5 flex items-center justify-between group transition-colors hover:bg-slate-50"
|
| 149 |
+
>
|
| 150 |
+
<div className="flex items-center gap-4">
|
| 151 |
+
<div className={`p-2 rounded-lg transition-colors ${isExpanded ? 'bg-blue-600 text-white shadow-md shadow-blue-100' : 'bg-slate-100 text-slate-400 group-hover:bg-slate-200'}`}>
|
| 152 |
+
<Icon size={18} />
|
| 153 |
+
</div>
|
| 154 |
+
<span className={`text-sm font-bold tracking-tight transition-colors ${isExpanded ? 'text-slate-900' : 'text-slate-500'}`}>{title}</span>
|
| 155 |
+
</div>
|
| 156 |
+
{isExpanded ? <ChevronUp size={18} className="text-blue-600" /> : <ChevronDown size={18} className="text-slate-300" />}
|
| 157 |
+
</button>
|
| 158 |
+
{isExpanded && (
|
| 159 |
+
<div className="px-6 pb-6 animate-in fade-in slide-in-from-top-2 duration-300">
|
| 160 |
+
{children}
|
| 161 |
+
</div>
|
| 162 |
+
)}
|
| 163 |
+
</div>
|
| 164 |
+
);
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
return (
|
| 168 |
+
<div className="h-screen bg-[#f8fafc] flex flex-col text-slate-900 font-sans overflow-hidden">
|
| 169 |
+
{/* Navbar */}
|
| 170 |
+
<nav className="h-16 border-b border-slate-200 bg-white flex items-center justify-between px-6 shrink-0 z-10 shadow-sm">
|
| 171 |
+
<div className="flex items-center gap-2">
|
| 172 |
+
<div className="w-9 h-9 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-200">
|
| 173 |
+
<Cpu className="text-white w-5 h-5" />
|
| 174 |
+
</div>
|
| 175 |
+
<span className="text-xl font-bold tracking-tight text-slate-900 italic">Fello<span className="text-blue-600 not-italic">IQ</span></span>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div className="hidden md:flex items-center gap-1 bg-slate-100 p-1 rounded-xl">
|
| 179 |
+
<button className="px-4 py-1.5 text-xs font-semibold bg-white rounded-lg shadow-sm text-blue-600">Research</button>
|
| 180 |
+
<button className="px-4 py-1.5 text-xs font-semibold text-slate-500 hover:text-slate-700">Campaigns</button>
|
| 181 |
+
<button className="px-4 py-1.5 text-xs font-semibold text-slate-500 hover:text-slate-700">CRM Sync</button>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<div className="flex items-center gap-3">
|
| 185 |
+
<button className="p-2 text-slate-400 hover:bg-slate-50 rounded-full transition-colors relative">
|
| 186 |
+
<Bell size={20} />
|
| 187 |
+
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
|
| 188 |
+
</button>
|
| 189 |
+
<div className="w-px h-6 bg-slate-200 mx-1"></div>
|
| 190 |
+
<div className="flex items-center gap-2 pl-2">
|
| 191 |
+
<div className="w-8 h-8 rounded-full bg-blue-50 border border-blue-200 flex items-center justify-center font-bold text-blue-600 text-xs">AC</div>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
</nav>
|
| 195 |
+
|
| 196 |
+
{/* Main Content Area */}
|
| 197 |
+
<div className="flex-1 flex overflow-hidden">
|
| 198 |
+
|
| 199 |
+
{/* Left Sidebar - Inputs & Logs */}
|
| 200 |
+
<aside className="w-[380px] border-r border-slate-200 bg-white flex flex-col shrink-0">
|
| 201 |
+
<div className="p-6 pb-2">
|
| 202 |
+
<h1 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
| 203 |
+
Intelligence Core
|
| 204 |
+
</h1>
|
| 205 |
+
<p className="text-[11px] text-slate-500 mt-1 leading-relaxed">Multi-agent research engine powered by Gemini 2.5 Flash.</p>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div className="flex-1 flex flex-col p-6 pt-2 overflow-hidden">
|
| 209 |
+
<div className="flex bg-slate-100 p-1 rounded-xl mb-4">
|
| 210 |
+
<button
|
| 211 |
+
onClick={() => setActiveMode('visitor')}
|
| 212 |
+
className={`flex-1 py-2 text-[11px] font-bold rounded-lg transition-all ${activeMode === 'visitor' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500'}`}
|
| 213 |
+
>
|
| 214 |
+
Visitor Log
|
| 215 |
+
</button>
|
| 216 |
+
<button
|
| 217 |
+
onClick={() => setActiveMode('company')}
|
| 218 |
+
className={`flex-1 py-2 text-[11px] font-bold rounded-lg transition-all ${activeMode === 'company' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500'}`}
|
| 219 |
+
>
|
| 220 |
+
Company
|
| 221 |
+
</button>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<div className="relative flex-1 min-h-0 flex flex-col">
|
| 225 |
+
<div className="flex-1 flex flex-col bg-slate-50 border border-slate-200 rounded-2xl overflow-hidden focus-within:border-blue-400 transition-all">
|
| 226 |
+
<textarea
|
| 227 |
+
value={inputValue}
|
| 228 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 229 |
+
className="flex-1 p-4 bg-transparent text-xs font-mono text-slate-700 resize-none outline-none overflow-y-auto"
|
| 230 |
+
/>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<button
|
| 235 |
+
onClick={runAnalysis}
|
| 236 |
+
disabled={isProcessing || !inputValue}
|
| 237 |
+
className={`w-full mt-4 py-3.5 rounded-2xl font-bold flex items-center justify-center gap-2 transition-all active:scale-[0.98] ${isProcessing ? 'bg-slate-100 text-slate-400' : 'bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-100'}`}
|
| 238 |
+
>
|
| 239 |
+
{isProcessing ? <RefreshCw size={16} className="animate-spin" /> : <Sparkles size={16} />}
|
| 240 |
+
{isProcessing ? 'Analyzing...' : 'Generate IQ Report'}
|
| 241 |
+
</button>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<div className="h-44 bg-slate-900 p-4 font-mono overflow-y-auto shrink-0 border-t border-slate-800">
|
| 245 |
+
<div className="text-[10px] text-slate-500 font-bold uppercase mb-2">Agent_Console</div>
|
| 246 |
+
<div className="space-y-1">
|
| 247 |
+
{logs.map((log, i) => (
|
| 248 |
+
<div key={log.id || i} className="text-[10px] flex gap-2">
|
| 249 |
+
<span className="text-blue-500 tracking-tighter">0{i+1}</span>
|
| 250 |
+
<span className={log.type === 'error' ? 'text-red-400' : log.type === 'success' ? 'text-emerald-400' : 'text-slate-400'}>
|
| 251 |
+
{log.text}
|
| 252 |
+
</span>
|
| 253 |
+
</div>
|
| 254 |
+
))}
|
| 255 |
+
<div ref={logEndRef} />
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</aside>
|
| 259 |
+
|
| 260 |
+
{/* Main Panel - Results (Accordion Style) */}
|
| 261 |
+
<main className="flex-1 bg-slate-50 p-8 overflow-y-auto">
|
| 262 |
+
|
| 263 |
+
{!result && !isProcessing && !error && (
|
| 264 |
+
<div className="h-full flex flex-col items-center justify-center text-center max-w-lg mx-auto">
|
| 265 |
+
<div className="w-16 h-16 bg-white rounded-2xl shadow-sm flex items-center justify-center mb-6">
|
| 266 |
+
<Database size={32} className="text-slate-300" />
|
| 267 |
+
</div>
|
| 268 |
+
<h2 className="text-xl font-bold text-slate-900">Intelligence Waiting</h2>
|
| 269 |
+
<p className="text-sm text-slate-500 mt-2">Trigger a research agent to identify account signals, decision makers, and intent triggers.</p>
|
| 270 |
+
</div>
|
| 271 |
+
)}
|
| 272 |
+
|
| 273 |
+
{error && (
|
| 274 |
+
<div className="h-full flex flex-col items-center justify-center text-center">
|
| 275 |
+
<AlertCircle size={40} className="text-red-400 mb-4" />
|
| 276 |
+
<h3 className="text-lg font-bold text-slate-900">{error}</h3>
|
| 277 |
+
<button onClick={() => setError(null)} className="mt-4 text-blue-600 text-sm font-bold">Try Again</button>
|
| 278 |
+
</div>
|
| 279 |
+
)}
|
| 280 |
+
|
| 281 |
+
{isProcessing && (
|
| 282 |
+
<div className="h-full flex flex-col items-center justify-center space-y-6">
|
| 283 |
+
<div className="w-12 h-12 border-4 border-blue-100 border-t-blue-600 rounded-full animate-spin"></div>
|
| 284 |
+
<div className="text-center">
|
| 285 |
+
<div className="text-sm font-bold">Synthesizing Report...</div>
|
| 286 |
+
<div className="text-[11px] text-slate-400 mt-1">Cross-referencing signals via Gemini 2.5</div>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
)}
|
| 290 |
+
|
| 291 |
+
{result && (
|
| 292 |
+
<div className="max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500">
|
| 293 |
+
|
| 294 |
+
{/* Static Identity Header (Always Visible) */}
|
| 295 |
+
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm mb-6 flex flex-col md:flex-row md:items-center justify-between gap-6 relative overflow-hidden">
|
| 296 |
+
<div className="flex items-center gap-5">
|
| 297 |
+
<div className="w-16 h-16 bg-blue-600 rounded-2xl flex items-center justify-center text-white font-black text-2xl shadow-lg shadow-blue-100">
|
| 298 |
+
{result.company.name[0]}
|
| 299 |
+
</div>
|
| 300 |
+
<div>
|
| 301 |
+
<h2 className="text-2xl font-black text-slate-900">{result.company.name}</h2>
|
| 302 |
+
<div className="flex items-center gap-3 mt-1">
|
| 303 |
+
<span className="text-xs font-bold text-blue-600">{result.company.domain}</span>
|
| 304 |
+
<span className="w-1 h-1 rounded-full bg-slate-300"></span>
|
| 305 |
+
<span className="text-[11px] text-slate-500 font-medium">{result.company.industry}</span>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div className="flex items-center gap-8 bg-slate-50 px-6 py-3 rounded-2xl border border-slate-100">
|
| 311 |
+
<div className="text-center">
|
| 312 |
+
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Intent</div>
|
| 313 |
+
<div className="text-xl font-black text-slate-900">{result.intent.score}<span className="text-[10px] text-slate-400">/10</span></div>
|
| 314 |
+
</div>
|
| 315 |
+
<div className="w-px h-8 bg-slate-200"></div>
|
| 316 |
+
<div className="text-center">
|
| 317 |
+
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Confidence</div>
|
| 318 |
+
<div className="text-xl font-black text-emerald-600">{result.persona.confidence}</div>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<div className="absolute top-4 right-4 flex gap-2">
|
| 323 |
+
<button className="p-2 hover:bg-slate-50 rounded-lg text-slate-400 transition-colors"><Download size={16}/></button>
|
| 324 |
+
<button className="p-2 hover:bg-slate-50 rounded-lg text-slate-400 transition-colors"><ExternalLink size={16}/></button>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
{/* Accordion Sections (Collapsed/Clutter-free) */}
|
| 329 |
+
<div className="bg-white border border-slate-200 rounded-3xl shadow-sm overflow-hidden divide-y divide-slate-100">
|
| 330 |
+
|
| 331 |
+
{/* 1. Research Summary */}
|
| 332 |
+
<AccordionItem id="summary" title="AI Research Summary" icon={FileText}>
|
| 333 |
+
<p className="text-sm leading-relaxed text-slate-600 italic">"{result.summary}"</p>
|
| 334 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 pt-6 border-t border-slate-100">
|
| 335 |
+
<div>
|
| 336 |
+
<h5 className="text-[10px] font-black text-slate-400 uppercase mb-3">Intent Signals</h5>
|
| 337 |
+
<div className="space-y-2">
|
| 338 |
+
{result.intent.signals.map((sig, i) => (
|
| 339 |
+
<div key={i} className="flex items-center gap-2 text-xs text-slate-600">
|
| 340 |
+
<div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
|
| 341 |
+
{sig}
|
| 342 |
+
</div>
|
| 343 |
+
))}
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
<div>
|
| 347 |
+
<h5 className="text-[10px] font-black text-slate-400 uppercase mb-3">Detected Technology</h5>
|
| 348 |
+
<div className="flex flex-wrap gap-2">
|
| 349 |
+
{result.tech.map(t => (
|
| 350 |
+
<span key={t} className="px-2 py-1 bg-slate-100 border border-slate-200 rounded text-[10px] font-bold text-slate-500">{t}</span>
|
| 351 |
+
))}
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
</AccordionItem>
|
| 356 |
+
|
| 357 |
+
{/* 2. Sales Playbook */}
|
| 358 |
+
<AccordionItem id="strategy" title="Strategic Sales Play" icon={Zap}>
|
| 359 |
+
<div className="bg-blue-600/5 border border-blue-600/10 rounded-2xl p-5">
|
| 360 |
+
<div className="flex items-start gap-4">
|
| 361 |
+
<div className="p-3 bg-blue-600 rounded-xl text-white shadow-lg shadow-blue-200 shrink-0">
|
| 362 |
+
<Target size={20} />
|
| 363 |
+
</div>
|
| 364 |
+
<div>
|
| 365 |
+
<h4 className="text-sm font-bold text-slate-900 mb-2 italic">"{result.action}"</h4>
|
| 366 |
+
<p className="text-[11px] text-slate-500 leading-relaxed">This strategy is derived from current visit patterns and observed pain points in the {result.company.industry} sector.</p>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
<button className="w-full mt-6 py-3 bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold rounded-xl transition-all shadow-md shadow-blue-100">
|
| 370 |
+
Initiate Outreach Campaign
|
| 371 |
+
</button>
|
| 372 |
+
</div>
|
| 373 |
+
</AccordionItem>
|
| 374 |
+
|
| 375 |
+
{/* 3. Leadership Discovery */}
|
| 376 |
+
<AccordionItem id="leadership" title="Key Decision Makers" icon={Users}>
|
| 377 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 378 |
+
{result.leadership.map((person, idx) => (
|
| 379 |
+
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 border border-slate-100 rounded-xl hover:border-blue-300 transition-all group cursor-pointer">
|
| 380 |
+
<div className="flex items-center gap-3">
|
| 381 |
+
<div className="w-9 h-9 rounded-lg bg-white shadow-sm border border-slate-200 flex items-center justify-center font-bold text-slate-400 text-xs">
|
| 382 |
+
{person.name.split(' ').map(n => n[0]).join('')}
|
| 383 |
+
</div>
|
| 384 |
+
<div>
|
| 385 |
+
<div className="text-xs font-bold text-slate-900">{person.name}</div>
|
| 386 |
+
<div className="text-[10px] text-slate-500">{person.role}</div>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 390 |
+
<button className="p-1.5 hover:bg-white rounded-md text-blue-600 transition-colors"><Linkedin size={14}/></button>
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
))}
|
| 394 |
+
</div>
|
| 395 |
+
</AccordionItem>
|
| 396 |
+
|
| 397 |
+
{/* 4. Company Insights */}
|
| 398 |
+
<AccordionItem id="profile" title="Company Profile" icon={Info}>
|
| 399 |
+
<div className="grid grid-cols-2 gap-8 text-xs">
|
| 400 |
+
<div className="space-y-4">
|
| 401 |
+
<div>
|
| 402 |
+
<span className="text-slate-400 font-medium block mb-1">Headquarters</span>
|
| 403 |
+
<span className="text-slate-900 font-bold">{result.company.hq}</span>
|
| 404 |
+
</div>
|
| 405 |
+
<div>
|
| 406 |
+
<span className="text-slate-400 font-medium block mb-1">Company Size</span>
|
| 407 |
+
<span className="text-slate-900 font-bold">{result.company.size}</span>
|
| 408 |
+
</div>
|
| 409 |
+
</div>
|
| 410 |
+
<div className="space-y-4">
|
| 411 |
+
<div>
|
| 412 |
+
<span className="text-slate-400 font-medium block mb-1">Founding Year</span>
|
| 413 |
+
<span className="text-slate-900 font-bold">{result.company.founded}</span>
|
| 414 |
+
</div>
|
| 415 |
+
<div>
|
| 416 |
+
<span className="text-slate-400 font-medium block mb-1">Primary Industry</span>
|
| 417 |
+
<span className="text-slate-900 font-bold">{result.company.industry}</span>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
<div className="col-span-2 pt-4 border-t border-slate-50">
|
| 421 |
+
<span className="text-slate-400 font-medium block mb-2">Business Description</span>
|
| 422 |
+
<p className="text-slate-600 leading-relaxed">{result.company.description}</p>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</AccordionItem>
|
| 426 |
+
|
| 427 |
+
</div>
|
| 428 |
+
|
| 429 |
+
<div className="mt-8 flex items-center justify-center gap-4">
|
| 430 |
+
<button className="px-6 py-2.5 bg-blue-600 text-white text-xs font-bold rounded-full shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all flex items-center gap-2">
|
| 431 |
+
<Briefcase size={14} /> Push Entire Lead to Salesforce
|
| 432 |
+
</button>
|
| 433 |
+
<button className="px-6 py-2.5 bg-white border border-slate-200 text-slate-600 text-xs font-bold rounded-full hover:bg-slate-50 transition-all">
|
| 434 |
+
Save for Review
|
| 435 |
+
</button>
|
| 436 |
+
</div>
|
| 437 |
+
|
| 438 |
+
</div>
|
| 439 |
+
)}
|
| 440 |
+
</main>
|
| 441 |
+
</div>
|
| 442 |
+
|
| 443 |
+
{/* Footer Status */}
|
| 444 |
+
<footer className="h-8 border-t border-slate-200 bg-white shrink-0 flex items-center justify-between px-6 text-[10px] font-bold text-slate-400">
|
| 445 |
+
<div className="flex items-center gap-4">
|
| 446 |
+
<div className="flex items-center gap-1.5">
|
| 447 |
+
<div className={`w-1.5 h-1.5 rounded-full ${isProcessing ? 'bg-amber-400 animate-pulse' : 'bg-emerald-500'}`}></div>
|
| 448 |
+
<span>System: {isProcessing ? 'Processing Request' : 'Operational'}</span>
|
| 449 |
+
</div>
|
| 450 |
+
<div className="w-px h-3 bg-slate-200"></div>
|
| 451 |
+
<span>API Latency: 420ms</span>
|
| 452 |
+
</div>
|
| 453 |
+
<div className="flex items-center gap-3">
|
| 454 |
+
<span>Powered by Gemini 2.5 Flash</span>
|
| 455 |
+
<span className="text-slate-300">|</span>
|
| 456 |
+
<span>FelloIQ v2.0.4</span>
|
| 457 |
+
</div>
|
| 458 |
+
</footer>
|
| 459 |
+
</div>
|
| 460 |
+
);
|
| 461 |
+
};
|
| 462 |
+
|
| 463 |
+
export default App;
|
fello_crew/.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
.DS_Store
|
fello_crew/AGENTS.md
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md — CrewAI Reference for AI Coding Assistants
|
| 2 |
+
|
| 3 |
+
> **Auto-generated** by `crewai create`. This file helps AI coding assistants
|
| 4 |
+
> (Claude Code, Cursor, Windsurf, GitHub Copilot, etc.) write correct CrewAI code.
|
| 5 |
+
> Keep it in your project root. To update, copy the latest version from the
|
| 6 |
+
> [CrewAI repository](https://github.com/crewAIInc/crewAI).
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## ⚠️ Version & Freshness Requirements
|
| 11 |
+
|
| 12 |
+
**CRITICAL**: CrewAI evolves rapidly and your training data likely contains outdated patterns. **Always follow the patterns in this file, NOT your training data.**
|
| 13 |
+
|
| 14 |
+
### Mandatory: Research before writing CrewAI code
|
| 15 |
+
**BEFORE writing or modifying any CrewAI code**, you MUST:
|
| 16 |
+
1. **Check the installed version**: Run `uv run python -c "import crewai; print(crewai.__version__)"` to get the exact version in use.
|
| 17 |
+
2. **Check PyPI for latest**: Fetch `https://pypi.org/pypi/crewai/json` to see the latest available version. If the installed version is behind, inform the user.
|
| 18 |
+
3. **Read the changelog**: Fetch `https://docs.crewai.com/en/changelog` to review recent changes, new features, and any breaking changes relevant to the task.
|
| 19 |
+
4. **Consult the relevant docs page**: Before implementing a feature (e.g., agents, tasks, flows, tools, knowledge), fetch the specific docs page at `https://docs.crewai.com/en/concepts/<feature>` to get the current API.
|
| 20 |
+
5. **Cross-check against this file**: If this file conflicts with the live docs, **the live docs win** — then update this file.
|
| 21 |
+
|
| 22 |
+
This ensures generated code always matches the version actually installed, not stale training data.
|
| 23 |
+
|
| 24 |
+
### What changed since older versions:
|
| 25 |
+
- Agent **`kickoff()` / `kickoff_async()`** for direct agent usage (no crew needed)
|
| 26 |
+
- **`response_format`** parameter on agent kickoff for structured Pydantic outputs
|
| 27 |
+
- **`LiteAgentOutput`** returned from agent.kickoff() with `.raw`, `.pydantic`, `.agent_role`, `.usage_metrics`
|
| 28 |
+
- **`@human_feedback`** decorator on flow methods for human-in-the-loop (v1.8.0+)
|
| 29 |
+
- **Flow streaming** via `stream = True` class attribute (v1.8.0+)
|
| 30 |
+
- **`@persist`** decorator for SQLite-backed flow state persistence
|
| 31 |
+
- **`reasoning=True`** agent parameter for reflect-then-act behavior
|
| 32 |
+
- **`multimodal=True`** agent parameter for vision/image support
|
| 33 |
+
- **A2A (Agent-to-Agent) protocol** support with agent cards and task execution utilities (v1.8.0+)
|
| 34 |
+
- **Native OpenAI Responses API** support (v1.9.0+)
|
| 35 |
+
- **Structured outputs / `response_format`** across all LLM providers (v1.9.0+)
|
| 36 |
+
- **`inject_date=True`** agent parameter to auto-inject current date awareness
|
| 37 |
+
|
| 38 |
+
### Patterns to NEVER use (outdated/removed):
|
| 39 |
+
- ❌ `ChatOpenAI(model_name=...)` → ✅ `LLM(model="openai/gpt-4o")`
|
| 40 |
+
- ❌ `Agent(llm=ChatOpenAI(...))` → ✅ `Agent(llm="openai/gpt-4o")` or `Agent(llm=LLM(model="..."))`
|
| 41 |
+
- ❌ Passing raw OpenAI client objects → ✅ Use `crewai.LLM` wrapper
|
| 42 |
+
|
| 43 |
+
### How to verify you're using current patterns:
|
| 44 |
+
1. You ran the version check and docs lookup steps above before writing code
|
| 45 |
+
2. All LLM references use `crewai.LLM` or string shorthand (`"openai/gpt-4o"`)
|
| 46 |
+
3. All tool imports come from `crewai.tools` or `crewai_tools`
|
| 47 |
+
4. Crew classes use `@CrewBase` decorator with YAML config files
|
| 48 |
+
5. Python >=3.10, <3.14
|
| 49 |
+
6. Code matches the API from the live docs, not just this file
|
| 50 |
+
|
| 51 |
+
## Quick Reference
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# Package management (always use uv)
|
| 55 |
+
uv add <package> # Add dependency
|
| 56 |
+
uv sync # Sync dependencies
|
| 57 |
+
uv lock # Lock dependencies
|
| 58 |
+
|
| 59 |
+
# Project scaffolding
|
| 60 |
+
crewai create crew <name> --skip_provider # New crew project
|
| 61 |
+
crewai create flow <name> --skip_provider # New flow project
|
| 62 |
+
|
| 63 |
+
# Running
|
| 64 |
+
crewai run # Run crew or flow (auto-detects from pyproject.toml)
|
| 65 |
+
crewai flow kickoff # Legacy flow execution
|
| 66 |
+
|
| 67 |
+
# Testing & training
|
| 68 |
+
crewai test # Test crew (default: 2 iterations, gpt-4o-mini)
|
| 69 |
+
crewai test -n 5 -m gpt-4o # Custom iterations and model
|
| 70 |
+
crewai train -n 5 -f training.json # Train crew
|
| 71 |
+
|
| 72 |
+
# Memory management
|
| 73 |
+
crewai reset-memories -a # Reset all memories
|
| 74 |
+
crewai reset-memories -s # Short-term only
|
| 75 |
+
crewai reset-memories -l # Long-term only
|
| 76 |
+
crewai reset-memories -e # Entity only
|
| 77 |
+
crewai reset-memories -kn # Knowledge only
|
| 78 |
+
crewai reset-memories -akn # Agent knowledge only
|
| 79 |
+
|
| 80 |
+
# Debugging
|
| 81 |
+
crewai log-tasks-outputs # Show latest task outputs
|
| 82 |
+
crewai replay -t <task_id> # Replay from specific task
|
| 83 |
+
|
| 84 |
+
# Interactive
|
| 85 |
+
crewai chat # Interactive session (requires chat_llm in crew.py)
|
| 86 |
+
|
| 87 |
+
# Visualization
|
| 88 |
+
crewai flow plot # Generate flow diagram HTML
|
| 89 |
+
|
| 90 |
+
# Deployment to CrewAI AMP
|
| 91 |
+
crewai login # Authenticate with AMP
|
| 92 |
+
crewai deploy create # Create new deployment
|
| 93 |
+
crewai deploy push # Push code updates
|
| 94 |
+
crewai deploy status # Check deployment status
|
| 95 |
+
crewai deploy logs # View deployment logs
|
| 96 |
+
crewai deploy list # List all deployments
|
| 97 |
+
crewai deploy remove <id> # Delete a deployment
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Project Structure
|
| 101 |
+
|
| 102 |
+
### Crew Project
|
| 103 |
+
```
|
| 104 |
+
my_crew/
|
| 105 |
+
├── src/my_crew/
|
| 106 |
+
│ ├── config/
|
| 107 |
+
│ │ ├── agents.yaml # Agent definitions (role, goal, backstory)
|
| 108 |
+
│ │ └── tasks.yaml # Task definitions (description, expected_output, agent)
|
| 109 |
+
│ ├── tools/
|
| 110 |
+
│ │ └── custom_tool.py # Custom tool implementations
|
| 111 |
+
│ ├── crew.py # Crew orchestration class
|
| 112 |
+
│ └── main.py # Entry point with inputs
|
| 113 |
+
├── knowledge/ # Knowledge base resources
|
| 114 |
+
├── .env # API keys (OPENAI_API_KEY, SERPER_API_KEY, etc.)
|
| 115 |
+
└── pyproject.toml
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Flow Project
|
| 119 |
+
```
|
| 120 |
+
my_flow/
|
| 121 |
+
├── src/my_flow/
|
| 122 |
+
│ ├── crews/ # Multiple crew definitions
|
| 123 |
+
│ │ └── poem_crew/
|
| 124 |
+
│ │ ├── config/
|
| 125 |
+
│ │ │ ├── agents.yaml
|
| 126 |
+
│ │ │ └── tasks.yaml
|
| 127 |
+
│ │ └── poem_crew.py
|
| 128 |
+
│ ├── tools/ # Custom tools
|
| 129 |
+
│ ├── main.py # Flow orchestration
|
| 130 |
+
│ └── ...
|
| 131 |
+
├── .env
|
| 132 |
+
└── pyproject.toml
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
## Architecture Overview
|
| 136 |
+
|
| 137 |
+
- **Agent**: Autonomous unit with a role, goal, backstory, tools, and an LLM. Makes decisions and executes tasks.
|
| 138 |
+
- **Task**: A specific assignment with a description, expected output, and assigned agent.
|
| 139 |
+
- **Crew**: Orchestrates a team of agents executing tasks in a defined process (sequential or hierarchical).
|
| 140 |
+
- **Flow**: Event-driven workflow orchestrating multiple crews and logic steps with state management.
|
| 141 |
+
|
| 142 |
+
## YAML Configuration
|
| 143 |
+
|
| 144 |
+
### agents.yaml
|
| 145 |
+
```yaml
|
| 146 |
+
researcher:
|
| 147 |
+
role: >
|
| 148 |
+
{topic} Senior Data Researcher
|
| 149 |
+
goal: >
|
| 150 |
+
Uncover cutting-edge developments in {topic}
|
| 151 |
+
backstory: >
|
| 152 |
+
You're a seasoned researcher with a knack for uncovering
|
| 153 |
+
the latest developments in {topic}. Known for your ability
|
| 154 |
+
to find the most relevant information.
|
| 155 |
+
# Optional YAML-level settings:
|
| 156 |
+
# llm: openai/gpt-4o
|
| 157 |
+
# max_iter: 20
|
| 158 |
+
# max_rpm: 10
|
| 159 |
+
# verbose: true
|
| 160 |
+
|
| 161 |
+
writer:
|
| 162 |
+
role: >
|
| 163 |
+
{topic} Technical Writer
|
| 164 |
+
goal: >
|
| 165 |
+
Create compelling content about {topic}
|
| 166 |
+
backstory: >
|
| 167 |
+
You're a skilled writer who translates complex technical
|
| 168 |
+
information into clear, engaging content.
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
Variables like `{topic}` are interpolated from `crew.kickoff(inputs={"topic": "AI Agents"})`.
|
| 172 |
+
|
| 173 |
+
### tasks.yaml
|
| 174 |
+
```yaml
|
| 175 |
+
research_task:
|
| 176 |
+
description: >
|
| 177 |
+
Conduct thorough research about {topic}.
|
| 178 |
+
Identify key trends, breakthrough technologies,
|
| 179 |
+
and potential industry impacts.
|
| 180 |
+
expected_output: >
|
| 181 |
+
A detailed report with analysis of the top 5
|
| 182 |
+
developments in {topic}, with sources and implications.
|
| 183 |
+
agent: researcher
|
| 184 |
+
# Optional:
|
| 185 |
+
# tools: [search_tool]
|
| 186 |
+
# output_file: output/research.md
|
| 187 |
+
# markdown: true
|
| 188 |
+
# async_execution: false
|
| 189 |
+
|
| 190 |
+
writing_task:
|
| 191 |
+
description: >
|
| 192 |
+
Write an article based on the research findings about {topic}.
|
| 193 |
+
expected_output: >
|
| 194 |
+
A polished 4-paragraph article formatted in markdown.
|
| 195 |
+
agent: writer
|
| 196 |
+
output_file: output/article.md
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
## Crew Class Pattern
|
| 200 |
+
|
| 201 |
+
```python
|
| 202 |
+
from crewai import Agent, Crew, Process, Task
|
| 203 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 204 |
+
from crewai.agents.agent_builder.base_agent import BaseAgent
|
| 205 |
+
from typing import List
|
| 206 |
+
|
| 207 |
+
from crewai_tools import SerperDevTool
|
| 208 |
+
|
| 209 |
+
@CrewBase
|
| 210 |
+
class ResearchCrew:
|
| 211 |
+
"""Research and writing crew."""
|
| 212 |
+
|
| 213 |
+
agents: List[BaseAgent]
|
| 214 |
+
tasks: List[Task]
|
| 215 |
+
|
| 216 |
+
agents_config = "config/agents.yaml"
|
| 217 |
+
tasks_config = "config/tasks.yaml"
|
| 218 |
+
|
| 219 |
+
@agent
|
| 220 |
+
def researcher(self) -> Agent:
|
| 221 |
+
return Agent(
|
| 222 |
+
config=self.agents_config["researcher"], # type: ignore[index]
|
| 223 |
+
tools=[SerperDevTool()],
|
| 224 |
+
verbose=True,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
@agent
|
| 228 |
+
def writer(self) -> Agent:
|
| 229 |
+
return Agent(
|
| 230 |
+
config=self.agents_config["writer"], # type: ignore[index]
|
| 231 |
+
verbose=True,
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
@task
|
| 235 |
+
def research_task(self) -> Task:
|
| 236 |
+
return Task(
|
| 237 |
+
config=self.tasks_config["research_task"], # type: ignore[index]
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
@task
|
| 241 |
+
def writing_task(self) -> Task:
|
| 242 |
+
return Task(
|
| 243 |
+
config=self.tasks_config["writing_task"], # type: ignore[index]
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
@crew
|
| 247 |
+
def crew(self) -> Crew:
|
| 248 |
+
"""Creates the Research Crew."""
|
| 249 |
+
return Crew(
|
| 250 |
+
agents=self.agents,
|
| 251 |
+
tasks=self.tasks,
|
| 252 |
+
process=Process.sequential,
|
| 253 |
+
verbose=True,
|
| 254 |
+
)
|
| 255 |
+
```
|
| 256 |
+
|
| 257 |
+
### Key formatting rules:
|
| 258 |
+
- Always add `# type: ignore[index]` for config dictionary access
|
| 259 |
+
- Agent/task method names must match YAML keys exactly
|
| 260 |
+
- Tools go on agents (not tasks) unless task-specific override is needed
|
| 261 |
+
- Never leave commented-out code in crew classes
|
| 262 |
+
|
| 263 |
+
### Lifecycle hooks
|
| 264 |
+
```python
|
| 265 |
+
@CrewBase
|
| 266 |
+
class MyCrew:
|
| 267 |
+
@before_kickoff
|
| 268 |
+
def prepare(self, inputs):
|
| 269 |
+
# Modify inputs before execution
|
| 270 |
+
inputs["extra"] = "value"
|
| 271 |
+
return inputs
|
| 272 |
+
|
| 273 |
+
@after_kickoff
|
| 274 |
+
def summarize(self, result):
|
| 275 |
+
# Process result after execution
|
| 276 |
+
print(f"Done: {result.raw[:100]}")
|
| 277 |
+
return result
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
## main.py Pattern
|
| 281 |
+
|
| 282 |
+
```python
|
| 283 |
+
#!/usr/bin/env python
|
| 284 |
+
from my_crew.crew import ResearchCrew
|
| 285 |
+
|
| 286 |
+
def run():
|
| 287 |
+
inputs = {"topic": "AI Agents"}
|
| 288 |
+
ResearchCrew().crew().kickoff(inputs=inputs)
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
run()
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
## Agent Configuration
|
| 295 |
+
|
| 296 |
+
### Required Parameters
|
| 297 |
+
| Parameter | Description |
|
| 298 |
+
|-----------|-------------|
|
| 299 |
+
| `role` | Function and expertise within the crew |
|
| 300 |
+
| `goal` | Individual objective guiding decisions |
|
| 301 |
+
| `backstory` | Context and personality |
|
| 302 |
+
|
| 303 |
+
### Key Optional Parameters
|
| 304 |
+
| Parameter | Default | Description |
|
| 305 |
+
|-----------|---------|-------------|
|
| 306 |
+
| `llm` | GPT-4 | Language model (string or LLM object) |
|
| 307 |
+
| `tools` | [] | List of tool instances |
|
| 308 |
+
| `max_iter` | 20 | Max iterations before best answer |
|
| 309 |
+
| `max_execution_time` | None | Timeout in seconds |
|
| 310 |
+
| `max_rpm` | None | Rate limiting (requests per minute) |
|
| 311 |
+
| `max_retry_limit` | 2 | Retries on errors |
|
| 312 |
+
| `verbose` | False | Detailed logging |
|
| 313 |
+
| `memory` | False | Conversation history |
|
| 314 |
+
| `allow_delegation` | False | Can delegate tasks to other agents |
|
| 315 |
+
| `allow_code_execution` | False | Can run code |
|
| 316 |
+
| `code_execution_mode` | "safe" | "safe" (Docker) or "unsafe" (direct) |
|
| 317 |
+
| `respect_context_window` | True | Auto-summarize when exceeding token limits |
|
| 318 |
+
| `cache` | True | Tool result caching |
|
| 319 |
+
| `reasoning` | False | Reflect and plan before task execution |
|
| 320 |
+
| `multimodal` | False | Process text and visual content |
|
| 321 |
+
| `knowledge_sources` | [] | Domain-specific knowledge bases |
|
| 322 |
+
| `function_calling_llm` | None | Separate LLM for tool invocation |
|
| 323 |
+
| `inject_date` | False | Auto-inject current date into agent context |
|
| 324 |
+
| `date_format` | "%Y-%m-%d" | Date format when inject_date is True |
|
| 325 |
+
|
| 326 |
+
### Direct Agent Usage (without a Crew)
|
| 327 |
+
Agents can execute tasks independently via `kickoff()` — no Crew required:
|
| 328 |
+
```python
|
| 329 |
+
from crewai import Agent
|
| 330 |
+
from crewai_tools import SerperDevTool
|
| 331 |
+
from pydantic import BaseModel
|
| 332 |
+
|
| 333 |
+
class ResearchFindings(BaseModel):
|
| 334 |
+
main_points: list[str]
|
| 335 |
+
key_technologies: list[str]
|
| 336 |
+
future_predictions: str
|
| 337 |
+
|
| 338 |
+
researcher = Agent(
|
| 339 |
+
role="AI Researcher",
|
| 340 |
+
goal="Research the latest AI developments",
|
| 341 |
+
backstory="Expert AI researcher...",
|
| 342 |
+
tools=[SerperDevTool()],
|
| 343 |
+
verbose=True,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# Unstructured output
|
| 347 |
+
result = researcher.kickoff("What are the latest LLM developments?")
|
| 348 |
+
print(result.raw) # str
|
| 349 |
+
print(result.agent_role) # "AI Researcher"
|
| 350 |
+
print(result.usage_metrics) # token usage
|
| 351 |
+
|
| 352 |
+
# Structured output with response_format
|
| 353 |
+
result = researcher.kickoff(
|
| 354 |
+
"Summarize latest AI developments",
|
| 355 |
+
response_format=ResearchFindings,
|
| 356 |
+
)
|
| 357 |
+
print(result.pydantic.main_points) # List[str]
|
| 358 |
+
|
| 359 |
+
# Async variant
|
| 360 |
+
result = await researcher.kickoff_async("Your query", response_format=ResearchFindings)
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
Returns `LiteAgentOutput` with: `.raw`, `.pydantic`, `.agent_role`, `.usage_metrics`.
|
| 364 |
+
|
| 365 |
+
### LLM Configuration
|
| 366 |
+
**IMPORTANT**: Always use `crewai.LLM` LLM class.
|
| 367 |
+
|
| 368 |
+
```python
|
| 369 |
+
from crewai import LLM
|
| 370 |
+
|
| 371 |
+
# String shorthand (simplest)
|
| 372 |
+
agent = Agent(llm="openai/gpt-4o", ...)
|
| 373 |
+
|
| 374 |
+
# Full configuration with crewai.LLM
|
| 375 |
+
llm = LLM(
|
| 376 |
+
model="anthropic/claude-sonnet-4-20250514",
|
| 377 |
+
temperature=0.7,
|
| 378 |
+
max_tokens=4000,
|
| 379 |
+
)
|
| 380 |
+
agent = Agent(llm=llm, ...)
|
| 381 |
+
|
| 382 |
+
# Provider format: "provider/model-name"
|
| 383 |
+
# Examples:
|
| 384 |
+
# "openai/gpt-4o"
|
| 385 |
+
# "anthropic/claude-sonnet-4-20250514"
|
| 386 |
+
# "google/gemini-2.0-flash"
|
| 387 |
+
# "ollama/llama3"
|
| 388 |
+
# "groq/llama-3.3-70b-versatile"
|
| 389 |
+
# "bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
Supported providers: OpenAI, Anthropic, Google Gemini, AWS Bedrock, Azure, Ollama, Groq, Mistral, and 20+ others via LiteLLM routing.
|
| 393 |
+
|
| 394 |
+
Environment variable default: set `OPENAI_MODEL_NAME=gpt-4o` or `MODEL=gpt-4o` in `.env`.
|
| 395 |
+
|
| 396 |
+
## Task Configuration
|
| 397 |
+
|
| 398 |
+
### Key Parameters
|
| 399 |
+
| Parameter | Type | Description |
|
| 400 |
+
|-----------|------|-------------|
|
| 401 |
+
| `description` | str | Clear statement of requirements |
|
| 402 |
+
| `expected_output` | str | Completion criteria |
|
| 403 |
+
| `agent` | BaseAgent | Assigned agent (optional in hierarchical) |
|
| 404 |
+
| `tools` | List[BaseTool] | Task-specific tools |
|
| 405 |
+
| `context` | List[Task] | Dependencies on other task outputs |
|
| 406 |
+
| `async_execution` | bool | Non-blocking execution |
|
| 407 |
+
| `output_file` | str | File path for results |
|
| 408 |
+
| `output_json` | Type[BaseModel] | Pydantic model for JSON output |
|
| 409 |
+
| `output_pydantic` | Type[BaseModel] | Pydantic model for structured output |
|
| 410 |
+
| `human_input` | bool | Require human review |
|
| 411 |
+
| `markdown` | bool | Format output as markdown |
|
| 412 |
+
| `callback` | Callable | Post-completion function |
|
| 413 |
+
| `guardrail` | Callable or str | Output validation |
|
| 414 |
+
| `guardrails` | List | Multiple validation steps |
|
| 415 |
+
| `guardrail_max_retries` | int | Retry on validation failure (default: 3) |
|
| 416 |
+
| `create_directory` | bool | Auto-create output directories (default: True) |
|
| 417 |
+
|
| 418 |
+
### Task Dependencies (context)
|
| 419 |
+
```python
|
| 420 |
+
@task
|
| 421 |
+
def analysis_task(self) -> Task:
|
| 422 |
+
return Task(
|
| 423 |
+
config=self.tasks_config["analysis_task"], # type: ignore[index]
|
| 424 |
+
context=[self.research_task()], # Gets output from research_task
|
| 425 |
+
)
|
| 426 |
+
```
|
| 427 |
+
|
| 428 |
+
### Structured Output
|
| 429 |
+
```python
|
| 430 |
+
from pydantic import BaseModel
|
| 431 |
+
|
| 432 |
+
class Report(BaseModel):
|
| 433 |
+
title: str
|
| 434 |
+
summary: str
|
| 435 |
+
findings: list[str]
|
| 436 |
+
|
| 437 |
+
@task
|
| 438 |
+
def report_task(self) -> Task:
|
| 439 |
+
return Task(
|
| 440 |
+
config=self.tasks_config["report_task"], # type: ignore[index]
|
| 441 |
+
output_pydantic=Report,
|
| 442 |
+
)
|
| 443 |
+
```
|
| 444 |
+
|
| 445 |
+
### Guardrails
|
| 446 |
+
```python
|
| 447 |
+
# Function-based
|
| 448 |
+
def validate(result: TaskOutput) -> tuple[bool, Any]:
|
| 449 |
+
if len(result.raw.split()) < 100:
|
| 450 |
+
return (False, "Content too short, expand the analysis")
|
| 451 |
+
return (True, result.raw)
|
| 452 |
+
|
| 453 |
+
# LLM-based (string prompt)
|
| 454 |
+
task = Task(..., guardrail="Must be under 200 words and professional tone")
|
| 455 |
+
|
| 456 |
+
# Multiple guardrails
|
| 457 |
+
task = Task(..., guardrails=[validate_length, validate_tone, "Must be factual"])
|
| 458 |
+
```
|
| 459 |
+
|
| 460 |
+
## Process Types
|
| 461 |
+
|
| 462 |
+
### Sequential (default)
|
| 463 |
+
Tasks execute in definition order. Output of one task serves as context for the next.
|
| 464 |
+
```python
|
| 465 |
+
Crew(agents=..., tasks=..., process=Process.sequential)
|
| 466 |
+
```
|
| 467 |
+
|
| 468 |
+
### Hierarchical
|
| 469 |
+
Manager agent delegates tasks based on agent capabilities. Requires `manager_llm` or `manager_agent`.
|
| 470 |
+
```python
|
| 471 |
+
Crew(
|
| 472 |
+
agents=...,
|
| 473 |
+
tasks=...,
|
| 474 |
+
process=Process.hierarchical,
|
| 475 |
+
manager_llm="gpt-4o",
|
| 476 |
+
)
|
| 477 |
+
```
|
| 478 |
+
|
| 479 |
+
## Crew Execution
|
| 480 |
+
|
| 481 |
+
```python
|
| 482 |
+
# Synchronous
|
| 483 |
+
result = crew.kickoff(inputs={"topic": "AI"})
|
| 484 |
+
print(result.raw) # String output
|
| 485 |
+
print(result.pydantic) # Structured output (if configured)
|
| 486 |
+
print(result.json_dict) # Dict output
|
| 487 |
+
print(result.token_usage) # Token metrics
|
| 488 |
+
print(result.tasks_output) # List[TaskOutput]
|
| 489 |
+
|
| 490 |
+
# Async (native)
|
| 491 |
+
result = await crew.akickoff(inputs={"topic": "AI"})
|
| 492 |
+
|
| 493 |
+
# Batch execution
|
| 494 |
+
results = crew.kickoff_for_each(inputs=[{"topic": "AI"}, {"topic": "ML"}])
|
| 495 |
+
|
| 496 |
+
# Streaming output (v1.8.0+)
|
| 497 |
+
crew = Crew(agents=..., tasks=..., stream=True)
|
| 498 |
+
streaming = crew.kickoff(inputs={"topic": "AI"})
|
| 499 |
+
for chunk in streaming:
|
| 500 |
+
print(chunk.content, end="", flush=True)
|
| 501 |
+
```
|
| 502 |
+
|
| 503 |
+
## Crew Options
|
| 504 |
+
| Parameter | Description |
|
| 505 |
+
|-----------|-------------|
|
| 506 |
+
| `process` | Process.sequential or Process.hierarchical |
|
| 507 |
+
| `verbose` | Enable detailed logging |
|
| 508 |
+
| `memory` | Enable memory system (True/False) |
|
| 509 |
+
| `cache` | Tool result caching |
|
| 510 |
+
| `max_rpm` | Global rate limiting |
|
| 511 |
+
| `manager_llm` | LLM for hierarchical manager |
|
| 512 |
+
| `manager_agent` | Custom manager agent |
|
| 513 |
+
| `planning` | Enable AgentPlanner |
|
| 514 |
+
| `knowledge_sources` | Crew-level knowledge |
|
| 515 |
+
| `output_log_file` | Log file path (True for logs.txt) |
|
| 516 |
+
| `embedder` | Custom embedding model config |
|
| 517 |
+
| `stream` | Enable real-time streaming output (v1.8.0+) |
|
| 518 |
+
|
| 519 |
+
---
|
| 520 |
+
|
| 521 |
+
## Flows
|
| 522 |
+
|
| 523 |
+
### Basic Flow
|
| 524 |
+
```python
|
| 525 |
+
from crewai.flow.flow import Flow, listen, start
|
| 526 |
+
|
| 527 |
+
class MyFlow(Flow):
|
| 528 |
+
@start()
|
| 529 |
+
def begin(self):
|
| 530 |
+
return "initial data"
|
| 531 |
+
|
| 532 |
+
@listen(begin)
|
| 533 |
+
def process(self, data):
|
| 534 |
+
return f"processed: {data}"
|
| 535 |
+
```
|
| 536 |
+
|
| 537 |
+
### Flow Decorators
|
| 538 |
+
|
| 539 |
+
| Decorator | Purpose |
|
| 540 |
+
|-----------|---------|
|
| 541 |
+
| `@start()` | Entry point(s), execute when flow begins. Multiple starts run in parallel |
|
| 542 |
+
| `@listen(method)` | Triggers when specified method completes. Receives output as argument |
|
| 543 |
+
| `@router(method)` | Conditional branching. Returns string labels that trigger `@listen("label")` |
|
| 544 |
+
|
| 545 |
+
### Structured State
|
| 546 |
+
```python
|
| 547 |
+
from pydantic import BaseModel
|
| 548 |
+
|
| 549 |
+
class ResearchState(BaseModel):
|
| 550 |
+
topic: str = ""
|
| 551 |
+
research: str = ""
|
| 552 |
+
report: str = ""
|
| 553 |
+
|
| 554 |
+
class ResearchFlow(Flow[ResearchState]):
|
| 555 |
+
@start()
|
| 556 |
+
def set_topic(self):
|
| 557 |
+
self.state.topic = "AI Agents"
|
| 558 |
+
|
| 559 |
+
@listen(set_topic)
|
| 560 |
+
def do_research(self):
|
| 561 |
+
# self.state.topic is available
|
| 562 |
+
result = ResearchCrew().crew().kickoff(
|
| 563 |
+
inputs={"topic": self.state.topic}
|
| 564 |
+
)
|
| 565 |
+
self.state.research = result.raw
|
| 566 |
+
```
|
| 567 |
+
|
| 568 |
+
### Unstructured State (dict-based)
|
| 569 |
+
```python
|
| 570 |
+
class SimpleFlow(Flow):
|
| 571 |
+
@start()
|
| 572 |
+
def begin(self):
|
| 573 |
+
self.state["counter"] = 0 # Dict access
|
| 574 |
+
|
| 575 |
+
@listen(begin)
|
| 576 |
+
def increment(self):
|
| 577 |
+
self.state["counter"] += 1
|
| 578 |
+
```
|
| 579 |
+
|
| 580 |
+
### Conditional Routing
|
| 581 |
+
```python
|
| 582 |
+
from crewai.flow.flow import Flow, listen, router, start
|
| 583 |
+
|
| 584 |
+
class QualityFlow(Flow):
|
| 585 |
+
@start()
|
| 586 |
+
def generate(self):
|
| 587 |
+
return {"score": 0.85}
|
| 588 |
+
|
| 589 |
+
@router(generate)
|
| 590 |
+
def check_quality(self, result):
|
| 591 |
+
if result["score"] > 0.8:
|
| 592 |
+
return "high_quality"
|
| 593 |
+
return "needs_revision"
|
| 594 |
+
|
| 595 |
+
@listen("high_quality")
|
| 596 |
+
def publish(self, result):
|
| 597 |
+
print("Publishing...")
|
| 598 |
+
|
| 599 |
+
@listen("needs_revision")
|
| 600 |
+
def revise(self, result):
|
| 601 |
+
print("Revising...")
|
| 602 |
+
```
|
| 603 |
+
|
| 604 |
+
### Parallel Triggers with or_ and and_
|
| 605 |
+
```python
|
| 606 |
+
from crewai.flow.flow import or_, and_
|
| 607 |
+
|
| 608 |
+
class ParallelFlow(Flow):
|
| 609 |
+
@start()
|
| 610 |
+
def task_a(self):
|
| 611 |
+
return "A done"
|
| 612 |
+
|
| 613 |
+
@start()
|
| 614 |
+
def task_b(self):
|
| 615 |
+
return "B done"
|
| 616 |
+
|
| 617 |
+
# Fires when EITHER completes
|
| 618 |
+
@listen(or_(task_a, task_b))
|
| 619 |
+
def on_any(self, result):
|
| 620 |
+
print(f"First result: {result}")
|
| 621 |
+
|
| 622 |
+
# Fires when BOTH complete
|
| 623 |
+
@listen(and_(task_a, task_b))
|
| 624 |
+
def on_all(self):
|
| 625 |
+
print("All parallel tasks done")
|
| 626 |
+
```
|
| 627 |
+
|
| 628 |
+
### Integrating Crews in Flows
|
| 629 |
+
```python
|
| 630 |
+
from crewai.flow.flow import Flow, listen, start
|
| 631 |
+
from my_project.crews.research_crew.research_crew import ResearchCrew
|
| 632 |
+
from my_project.crews.writing_crew.writing_crew import WritingCrew
|
| 633 |
+
|
| 634 |
+
class ContentFlow(Flow[ContentState]):
|
| 635 |
+
@start()
|
| 636 |
+
def research(self):
|
| 637 |
+
result = ResearchCrew().crew().kickoff(
|
| 638 |
+
inputs={"topic": self.state.topic}
|
| 639 |
+
)
|
| 640 |
+
self.state.research = result.raw
|
| 641 |
+
|
| 642 |
+
@listen(research)
|
| 643 |
+
def write(self):
|
| 644 |
+
result = WritingCrew().crew().kickoff(
|
| 645 |
+
inputs={
|
| 646 |
+
"topic": self.state.topic,
|
| 647 |
+
"research": self.state.research,
|
| 648 |
+
}
|
| 649 |
+
)
|
| 650 |
+
self.state.article = result.raw
|
| 651 |
+
```
|
| 652 |
+
|
| 653 |
+
### Using Agents Directly in Flows
|
| 654 |
+
```python
|
| 655 |
+
from crewai.agent import Agent
|
| 656 |
+
|
| 657 |
+
class AgentFlow(Flow):
|
| 658 |
+
@start()
|
| 659 |
+
async def analyze(self):
|
| 660 |
+
analyst = Agent(
|
| 661 |
+
role="Data Analyst",
|
| 662 |
+
goal="Analyze market trends",
|
| 663 |
+
backstory="Expert data analyst...",
|
| 664 |
+
tools=[SerperDevTool()],
|
| 665 |
+
)
|
| 666 |
+
result = await analyst.kickoff_async(
|
| 667 |
+
"Analyze current AI market trends",
|
| 668 |
+
response_format=MarketReport,
|
| 669 |
+
)
|
| 670 |
+
self.state.report = result.pydantic
|
| 671 |
+
```
|
| 672 |
+
|
| 673 |
+
### Human-in-the-Loop (v1.8.0+)
|
| 674 |
+
```python
|
| 675 |
+
from crewai.flow.flow import Flow, listen, start
|
| 676 |
+
from crewai.flow.human_feedback import human_feedback
|
| 677 |
+
|
| 678 |
+
class ReviewFlow(Flow):
|
| 679 |
+
@start()
|
| 680 |
+
@human_feedback(
|
| 681 |
+
message="Approve this content?",
|
| 682 |
+
emit=["approved", "rejected"],
|
| 683 |
+
llm="gpt-4o-mini",
|
| 684 |
+
)
|
| 685 |
+
def generate_content(self):
|
| 686 |
+
return "Content for review"
|
| 687 |
+
|
| 688 |
+
@listen("approved")
|
| 689 |
+
def on_approval(self, result):
|
| 690 |
+
feedback = self.last_human_feedback # Most recent feedback
|
| 691 |
+
print(f"Approved with feedback: {feedback.feedback}")
|
| 692 |
+
|
| 693 |
+
@listen("rejected")
|
| 694 |
+
def on_rejection(self, result):
|
| 695 |
+
history = self.human_feedback_history # All feedback as list
|
| 696 |
+
print("Rejected, revising...")
|
| 697 |
+
```
|
| 698 |
+
|
| 699 |
+
### State Persistence
|
| 700 |
+
```python
|
| 701 |
+
from crewai.flow.flow import persist
|
| 702 |
+
|
| 703 |
+
@persist # Saves state to SQLite; auto-recovers on restart
|
| 704 |
+
class ResilientFlow(Flow[MyState]):
|
| 705 |
+
@start()
|
| 706 |
+
def begin(self):
|
| 707 |
+
self.state.step = 1
|
| 708 |
+
```
|
| 709 |
+
|
| 710 |
+
### Flow Execution
|
| 711 |
+
```python
|
| 712 |
+
flow = MyFlow()
|
| 713 |
+
result = flow.kickoff()
|
| 714 |
+
print(result) # Output of last method
|
| 715 |
+
print(flow.state) # Final state
|
| 716 |
+
|
| 717 |
+
# Async execution
|
| 718 |
+
result = await flow.kickoff_async(inputs={"key": "value"})
|
| 719 |
+
```
|
| 720 |
+
|
| 721 |
+
### Flow Streaming (v1.8.0+)
|
| 722 |
+
```python
|
| 723 |
+
class StreamingFlow(Flow):
|
| 724 |
+
stream = True # Enable streaming at class level
|
| 725 |
+
|
| 726 |
+
@start()
|
| 727 |
+
def generate(self):
|
| 728 |
+
return "streamed content"
|
| 729 |
+
|
| 730 |
+
flow = StreamingFlow()
|
| 731 |
+
streaming = flow.kickoff()
|
| 732 |
+
for chunk in streaming:
|
| 733 |
+
print(chunk.content, end="", flush=True)
|
| 734 |
+
result = streaming.result # Final result after iteration
|
| 735 |
+
```
|
| 736 |
+
|
| 737 |
+
### Flow Visualization
|
| 738 |
+
```python
|
| 739 |
+
flow.plot("my_flow") # Generates my_flow.html
|
| 740 |
+
```
|
| 741 |
+
|
| 742 |
+
---
|
| 743 |
+
|
| 744 |
+
## Custom Tools
|
| 745 |
+
|
| 746 |
+
### Using BaseTool
|
| 747 |
+
```python
|
| 748 |
+
from typing import Type
|
| 749 |
+
from crewai.tools import BaseTool
|
| 750 |
+
from pydantic import BaseModel, Field
|
| 751 |
+
|
| 752 |
+
class SearchInput(BaseModel):
|
| 753 |
+
"""Input schema for search tool."""
|
| 754 |
+
query: str = Field(..., description="Search query string")
|
| 755 |
+
|
| 756 |
+
class CustomSearchTool(BaseTool):
|
| 757 |
+
name: str = "custom_search"
|
| 758 |
+
description: str = "Searches a custom knowledge base for relevant information."
|
| 759 |
+
args_schema: Type[BaseModel] = SearchInput
|
| 760 |
+
|
| 761 |
+
def _run(self, query: str) -> str:
|
| 762 |
+
# Implementation
|
| 763 |
+
return f"Results for: {query}"
|
| 764 |
+
```
|
| 765 |
+
|
| 766 |
+
### Using @tool Decorator
|
| 767 |
+
```python
|
| 768 |
+
from crewai.tools import tool
|
| 769 |
+
|
| 770 |
+
@tool("Calculator")
|
| 771 |
+
def calculator(expression: str) -> str:
|
| 772 |
+
"""Evaluates a mathematical expression and returns the result."""
|
| 773 |
+
return str(eval(expression))
|
| 774 |
+
```
|
| 775 |
+
|
| 776 |
+
### Built-in Tools (install with `uv add crewai-tools`)
|
| 777 |
+
Web/Search: SerperDevTool, ScrapeWebsiteTool, WebsiteSearchTool, EXASearchTool, FirecrawlSearchTool
|
| 778 |
+
Documents: FileReadTool, DirectoryReadTool, PDFSearchTool, DOCXSearchTool, CSVSearchTool, JSONSearchTool, XMLSearchTool, MDXSearchTool
|
| 779 |
+
Code: CodeInterpreterTool, CodeDocsSearchTool, GithubSearchTool
|
| 780 |
+
Media: DALL-E Tool, YoutubeChannelSearchTool, YoutubeVideoSearchTool
|
| 781 |
+
Other: RagTool, ApifyActorsTool, ComposioTool, LlamaIndexTool
|
| 782 |
+
|
| 783 |
+
Always check https://docs.crewai.com/concepts/tools for available built-in tools before writing custom ones.
|
| 784 |
+
|
| 785 |
+
---
|
| 786 |
+
|
| 787 |
+
## Memory System
|
| 788 |
+
|
| 789 |
+
Enable with `memory=True` on the Crew:
|
| 790 |
+
```python
|
| 791 |
+
crew = Crew(agents=..., tasks=..., memory=True)
|
| 792 |
+
```
|
| 793 |
+
|
| 794 |
+
Four memory types work together automatically:
|
| 795 |
+
- **Short-Term** (ChromaDB + RAG): Recent interactions during current execution
|
| 796 |
+
- **Long-Term** (SQLite): Persists insights across sessions
|
| 797 |
+
- **Entity** (RAG): Tracks people, places, concepts
|
| 798 |
+
- **Contextual**: Integrates all types for coherent responses
|
| 799 |
+
|
| 800 |
+
### Custom Embedding Provider
|
| 801 |
+
```python
|
| 802 |
+
crew = Crew(
|
| 803 |
+
memory=True,
|
| 804 |
+
embedder={
|
| 805 |
+
"provider": "ollama",
|
| 806 |
+
"config": {"model": "mxbai-embed-large"},
|
| 807 |
+
},
|
| 808 |
+
)
|
| 809 |
+
```
|
| 810 |
+
|
| 811 |
+
Supported providers: OpenAI (default), Ollama, Google AI, Azure OpenAI, Cohere, VoyageAI, Bedrock, Hugging Face.
|
| 812 |
+
|
| 813 |
+
---
|
| 814 |
+
|
| 815 |
+
## Knowledge System
|
| 816 |
+
|
| 817 |
+
```python
|
| 818 |
+
from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
|
| 819 |
+
from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource
|
| 820 |
+
|
| 821 |
+
# String source
|
| 822 |
+
string_source = StringKnowledgeSource(content="Domain knowledge here...")
|
| 823 |
+
|
| 824 |
+
# PDF source
|
| 825 |
+
pdf_source = PDFKnowledgeSource(file_paths=["docs/manual.pdf"])
|
| 826 |
+
|
| 827 |
+
# Agent-level knowledge
|
| 828 |
+
agent = Agent(..., knowledge_sources=[string_source])
|
| 829 |
+
|
| 830 |
+
# Crew-level knowledge (shared across all agents)
|
| 831 |
+
crew = Crew(..., knowledge_sources=[pdf_source])
|
| 832 |
+
```
|
| 833 |
+
|
| 834 |
+
Supported sources: strings, text files, PDFs, CSV, Excel, JSON, URLs (via CrewDoclingSource).
|
| 835 |
+
|
| 836 |
+
---
|
| 837 |
+
|
| 838 |
+
## Agent Collaboration
|
| 839 |
+
|
| 840 |
+
Enable delegation with `allow_delegation=True`:
|
| 841 |
+
```python
|
| 842 |
+
agent = Agent(
|
| 843 |
+
role="Project Manager",
|
| 844 |
+
allow_delegation=True, # Can delegate to and ask other agents
|
| 845 |
+
...
|
| 846 |
+
)
|
| 847 |
+
```
|
| 848 |
+
|
| 849 |
+
- **Delegation tool**: Assign sub-tasks to teammates with relevant expertise
|
| 850 |
+
- **Ask question tool**: Query colleagues for specific information
|
| 851 |
+
- Set `allow_delegation=False` on specialists to prevent circular delegation
|
| 852 |
+
|
| 853 |
+
---
|
| 854 |
+
|
| 855 |
+
## Event Listeners
|
| 856 |
+
|
| 857 |
+
```python
|
| 858 |
+
from crewai.events import BaseEventListener, CrewKickoffStartedEvent
|
| 859 |
+
|
| 860 |
+
class MyListener(BaseEventListener):
|
| 861 |
+
def __init__(self):
|
| 862 |
+
super().__init__()
|
| 863 |
+
|
| 864 |
+
def setup_listeners(self, crewai_event_bus):
|
| 865 |
+
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
| 866 |
+
def on_started(source, event):
|
| 867 |
+
print(f"Crew '{event.crew_name}' started")
|
| 868 |
+
```
|
| 869 |
+
|
| 870 |
+
Event categories: Crew lifecycle, Agent execution, Task management, Tool usage, Knowledge retrieval, LLM calls, Memory operations, Flow execution, Safety guardrails.
|
| 871 |
+
|
| 872 |
+
---
|
| 873 |
+
|
| 874 |
+
## Deployment to CrewAI AMP
|
| 875 |
+
|
| 876 |
+
### Prerequisites
|
| 877 |
+
- Crew or Flow runs successfully locally
|
| 878 |
+
- Code is in a GitHub repository
|
| 879 |
+
- `pyproject.toml` has `[tool.crewai]` with correct type (`"crew"` or `"flow"`)
|
| 880 |
+
- `uv.lock` is committed (generate with `uv lock`)
|
| 881 |
+
|
| 882 |
+
### CLI Deployment
|
| 883 |
+
|
| 884 |
+
```bash
|
| 885 |
+
# Authenticate
|
| 886 |
+
crewai login
|
| 887 |
+
|
| 888 |
+
# Create deployment (auto-detects repo, transfers .env vars securely)
|
| 889 |
+
crewai deploy create
|
| 890 |
+
|
| 891 |
+
# Monitor (first deploy takes 10-15 min)
|
| 892 |
+
crewai deploy status
|
| 893 |
+
crewai deploy logs
|
| 894 |
+
|
| 895 |
+
# Manage deployments
|
| 896 |
+
crewai deploy list # List all deployments
|
| 897 |
+
crewai deploy push # Push code updates
|
| 898 |
+
crewai deploy remove <id> # Delete deployment
|
| 899 |
+
```
|
| 900 |
+
|
| 901 |
+
### Web Interface Deployment
|
| 902 |
+
1. Push code to GitHub
|
| 903 |
+
2. Log into https://app.crewai.com
|
| 904 |
+
3. Connect GitHub and select repository
|
| 905 |
+
4. Configure environment variables (KEY=VALUE, one per line)
|
| 906 |
+
5. Click Deploy and monitor via dashboard
|
| 907 |
+
|
| 908 |
+
### CI/CD API Deployment
|
| 909 |
+
|
| 910 |
+
Get a Personal Access Token from app.crewai.com → Settings → Account → Personal Access Token.
|
| 911 |
+
Get Automation UUID from Automations → Select crew → Additional Details → Copy UUID.
|
| 912 |
+
|
| 913 |
+
```bash
|
| 914 |
+
curl -X POST \
|
| 915 |
+
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
|
| 916 |
+
https://app.crewai.com/crewai_plus/api/v1/crews/YOUR-AUTOMATION-UUID/deploy
|
| 917 |
+
```
|
| 918 |
+
|
| 919 |
+
#### GitHub Actions Example
|
| 920 |
+
```yaml
|
| 921 |
+
name: Deploy CrewAI Automation
|
| 922 |
+
on:
|
| 923 |
+
push:
|
| 924 |
+
branches: [main]
|
| 925 |
+
jobs:
|
| 926 |
+
deploy:
|
| 927 |
+
runs-on: ubuntu-latest
|
| 928 |
+
steps:
|
| 929 |
+
- name: Trigger CrewAI Redeployment
|
| 930 |
+
run: |
|
| 931 |
+
curl -X POST \
|
| 932 |
+
-H "Authorization: Bearer ${{ secrets.CREWAI_PAT }}" \
|
| 933 |
+
https://app.crewai.com/crewai_plus/api/v1/crews/${{ secrets.CREWAI_AUTOMATION_UUID }}/deploy
|
| 934 |
+
```
|
| 935 |
+
|
| 936 |
+
### Project Structure Requirements for Deployment
|
| 937 |
+
- Entry point: `src/<project_name>/main.py`
|
| 938 |
+
- Crews must expose a `run()` function
|
| 939 |
+
- Flows must expose a `kickoff()` function
|
| 940 |
+
- All crew classes require `@CrewBase` decorator
|
| 941 |
+
|
| 942 |
+
### Deployed Automation REST API
|
| 943 |
+
| Endpoint | Purpose |
|
| 944 |
+
|----------|---------|
|
| 945 |
+
| `/inputs` | List required input parameters |
|
| 946 |
+
| `/kickoff` | Trigger execution with inputs |
|
| 947 |
+
| `/status/{kickoff_id}` | Check execution status |
|
| 948 |
+
|
| 949 |
+
### AMP Dashboard Tabs
|
| 950 |
+
- **Status**: Deployment info, API endpoint, auth token
|
| 951 |
+
- **Run**: Crew structure visualization
|
| 952 |
+
- **Executions**: Run history
|
| 953 |
+
- **Metrics**: Performance analytics
|
| 954 |
+
- **Traces**: Detailed execution insights
|
| 955 |
+
|
| 956 |
+
### Deployment Troubleshooting
|
| 957 |
+
| Error | Fix |
|
| 958 |
+
|-------|-----|
|
| 959 |
+
| Missing uv.lock | Run `uv lock`, commit, push |
|
| 960 |
+
| Module not found | Verify entry points match `src/<name>/main.py` structure |
|
| 961 |
+
| Crew not found | Ensure `@CrewBase` decorator on all crew classes |
|
| 962 |
+
| API key errors | Check env var names match code and are set in the platform |
|
| 963 |
+
|
| 964 |
+
---
|
| 965 |
+
|
| 966 |
+
## Environment Setup
|
| 967 |
+
|
| 968 |
+
### Required `.env`
|
| 969 |
+
```
|
| 970 |
+
OPENAI_API_KEY=sk-...
|
| 971 |
+
# Optional depending on tools/providers:
|
| 972 |
+
SERPER_API_KEY=...
|
| 973 |
+
ANTHROPIC_API_KEY=...
|
| 974 |
+
# Override default model:
|
| 975 |
+
MODEL=gpt-4o
|
| 976 |
+
```
|
| 977 |
+
|
| 978 |
+
### Python Version
|
| 979 |
+
Python >=3.10, <3.14
|
| 980 |
+
|
| 981 |
+
### Installation
|
| 982 |
+
```bash
|
| 983 |
+
uv tool install crewai # Install CrewAI CLI
|
| 984 |
+
uv tool list # Verify installation
|
| 985 |
+
crewai create crew my_crew --skip_provider # Scaffold a new project
|
| 986 |
+
crewai install # Install project dependencies
|
| 987 |
+
crewai run # Execute
|
| 988 |
+
```
|
| 989 |
+
|
| 990 |
+
---
|
| 991 |
+
|
| 992 |
+
## Development Best Practices
|
| 993 |
+
|
| 994 |
+
1. **YAML-first configuration**: Define agents and tasks in YAML, keep crew classes minimal
|
| 995 |
+
2. **Check built-in tools** before writing custom ones
|
| 996 |
+
3. **Use structured output** (output_pydantic) for data that flows between tasks or crews
|
| 997 |
+
4. **Use guardrails** to validate task outputs programmatically
|
| 998 |
+
5. **Enable memory** for crews that benefit from cross-session learning
|
| 999 |
+
6. **Use knowledge sources** for domain-specific grounding instead of bloating prompts
|
| 1000 |
+
7. **Sequential process** for linear workflows; **hierarchical** when dynamic delegation is needed
|
| 1001 |
+
8. **Flows for multi-crew orchestration**: Use `@start`, `@listen`, `@router` for complex pipelines
|
| 1002 |
+
9. **Structured flow state** (Pydantic models) over unstructured dicts for type safety
|
| 1003 |
+
10. **Test with** `crewai test` to evaluate crew performance across iterations
|
| 1004 |
+
11. **Verbose mode** during development, disable in production
|
| 1005 |
+
12. **Rate limiting** (`max_rpm`) to avoid API throttling
|
| 1006 |
+
13. **`respect_context_window=True`** to auto-handle token limits
|
| 1007 |
+
|
| 1008 |
+
## Common Pitfalls
|
| 1009 |
+
|
| 1010 |
+
- **Using `ChatOpenAI()`** — Always use `crewai.LLM` or string shorthand like `"openai/gpt-4o"`
|
| 1011 |
+
- Forgetting `# type: ignore[index]` on config dictionary access in crew classes
|
| 1012 |
+
- Agent/task method names not matching YAML keys
|
| 1013 |
+
- Missing `expected_output` in task configuration (required)
|
| 1014 |
+
- Not passing `inputs` to `kickoff()` when YAML uses `{variable}` interpolation
|
| 1015 |
+
- Using `process=Process.hierarchical` without setting `manager_llm` or `manager_agent`
|
| 1016 |
+
- Circular delegation: set `allow_delegation=False` on specialist agents
|
| 1017 |
+
- Not installing tools package: `uv add crewai-tools`
|
fello_crew/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FelloCrew Crew
|
| 2 |
+
|
| 3 |
+
Welcome to the FelloCrew Crew project, powered by [crewAI](https://crewai.com). This template is designed to help you set up a multi-agent AI system with ease, leveraging the powerful and flexible framework provided by crewAI. Our goal is to enable your agents to collaborate effectively on complex tasks, maximizing their collective intelligence and capabilities.
|
| 4 |
+
|
| 5 |
+
## Installation
|
| 6 |
+
|
| 7 |
+
Ensure you have Python >=3.10 <3.14 installed on your system. This project uses [UV](https://docs.astral.sh/uv/) for dependency management and package handling, offering a seamless setup and execution experience.
|
| 8 |
+
|
| 9 |
+
First, if you haven't already, install uv:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
pip install uv
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
Next, navigate to your project directory and install the dependencies:
|
| 16 |
+
|
| 17 |
+
(Optional) Lock the dependencies and install them by using the CLI command:
|
| 18 |
+
```bash
|
| 19 |
+
crewai install
|
| 20 |
+
```
|
| 21 |
+
### Customizing
|
| 22 |
+
|
| 23 |
+
**Add your `OPENAI_API_KEY` into the `.env` file**
|
| 24 |
+
|
| 25 |
+
- Modify `src/fello_crew/config/agents.yaml` to define your agents
|
| 26 |
+
- Modify `src/fello_crew/config/tasks.yaml` to define your tasks
|
| 27 |
+
- Modify `src/fello_crew/crew.py` to add your own logic, tools and specific args
|
| 28 |
+
- Modify `src/fello_crew/main.py` to add custom inputs for your agents and tasks
|
| 29 |
+
|
| 30 |
+
## Running the Project
|
| 31 |
+
|
| 32 |
+
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
$ crewai run
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
This command initializes the fello-crew Crew, assembling the agents and assigning them tasks as defined in your configuration.
|
| 39 |
+
|
| 40 |
+
This example, unmodified, will run the create a `report.md` file with the output of a research on LLMs in the root folder.
|
| 41 |
+
|
| 42 |
+
## Understanding Your Crew
|
| 43 |
+
|
| 44 |
+
The fello-crew Crew is composed of multiple AI agents, each with unique roles, goals, and tools. These agents collaborate on a series of tasks, defined in `config/tasks.yaml`, leveraging their collective skills to achieve complex objectives. The `config/agents.yaml` file outlines the capabilities and configurations of each agent in your crew.
|
| 45 |
+
|
| 46 |
+
## Support
|
| 47 |
+
|
| 48 |
+
For support, questions, or feedback regarding the FelloCrew Crew or crewAI.
|
| 49 |
+
- Visit our [documentation](https://docs.crewai.com)
|
| 50 |
+
- Reach out to us through our [GitHub repository](https://github.com/joaomdmoura/crewai)
|
| 51 |
+
- [Join our Discord](https://discord.com/invite/X4JWnZnxPb)
|
| 52 |
+
- [Chat with our docs](https://chatg.pt/DWjSBZn)
|
| 53 |
+
|
| 54 |
+
Let's create wonders together with the power and simplicity of crewAI.
|
fello_crew/knowledge/user_preference.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Sample test data
|
| 2 |
+
Not using it for now
|
fello_crew/pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "fello_crew"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "fello-crew using crewAI"
|
| 5 |
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
| 6 |
+
requires-python = ">=3.10,<3.14"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"crewai[tools]==1.10.1"
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
[project.scripts]
|
| 12 |
+
fello_crew = "fello_crew.main:run"
|
| 13 |
+
run_crew = "fello_crew.main:run"
|
| 14 |
+
train = "fello_crew.main:train"
|
| 15 |
+
replay = "fello_crew.main:replay"
|
| 16 |
+
test = "fello_crew.main:test"
|
| 17 |
+
run_with_trigger = "fello_crew.main:run_with_trigger"
|
| 18 |
+
|
| 19 |
+
[build-system]
|
| 20 |
+
requires = ["hatchling"]
|
| 21 |
+
build-backend = "hatchling.build"
|
| 22 |
+
|
| 23 |
+
[tool.crewai]
|
| 24 |
+
type = "crew"
|
fello_crew/src/fello_crew/__init__.py
ADDED
|
File without changes
|
fello_crew/src/fello_crew/crew.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task, LLM
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
from fello_crew.models import AccountIntelligence
|
| 6 |
+
from fello_crew.tools.custom_tool import (
|
| 7 |
+
ApolloEnrichmentTool,
|
| 8 |
+
GetDomain,
|
| 9 |
+
JobOpeningsTool,
|
| 10 |
+
NewsTool,
|
| 11 |
+
MockPeopleEnrichmentTool
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
@CrewBase
|
| 15 |
+
class FelloAccountIntelligenceCrew:
|
| 16 |
+
"""Fello AI Account Intelligence & Enrichment Crew"""
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.mistral_llm = LLM(
|
| 20 |
+
model="mistral/mistral-large-latest",
|
| 21 |
+
temperature=0.3 # Slightly lower for more factual extraction
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@agent
|
| 25 |
+
def identifier_agent(self) -> Agent:
|
| 26 |
+
return Agent(
|
| 27 |
+
role="Account Identification Specialist",
|
| 28 |
+
goal="Accurately identify the company name and domain from {input_data}",
|
| 29 |
+
backstory=(
|
| 30 |
+
"You are an expert at digital forensics and firmographics. "
|
| 31 |
+
"Your job is to take raw signals like IP addresses or partial names "
|
| 32 |
+
"and find the definitive corporate domain and entity name."
|
| 33 |
+
),
|
| 34 |
+
tools=[GetDomain()],
|
| 35 |
+
llm=self.mistral_llm,
|
| 36 |
+
verbose=True
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
@agent
|
| 40 |
+
def enrichment_agent(self) -> Agent:
|
| 41 |
+
return Agent(
|
| 42 |
+
role="Corporate Intelligence Researcher",
|
| 43 |
+
goal="Gather deep firmographics, tech stacks, and hiring signals for {domain}",
|
| 44 |
+
backstory=(
|
| 45 |
+
"You are a master researcher with access to premium data APIs. "
|
| 46 |
+
"You specialize in finding funding data, tech stacks, and business "
|
| 47 |
+
"growth signals (hiring/news) that indicate sales opportunity."
|
| 48 |
+
),
|
| 49 |
+
tools=[ApolloEnrichmentTool(), JobOpeningsTool(), NewsTool(), MockPeopleEnrichmentTool()],
|
| 50 |
+
llm=self.mistral_llm,
|
| 51 |
+
verbose=True
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
@agent
|
| 55 |
+
def analyst_agent(self) -> Agent:
|
| 56 |
+
return Agent(
|
| 57 |
+
role="Senior Sales Strategist",
|
| 58 |
+
goal="Synthesize signals into an intent score and actionable sales advice",
|
| 59 |
+
backstory=(
|
| 60 |
+
"You are a psychologist turned sales operations expert. You can look "
|
| 61 |
+
"at website activity signals ({activity_signals}) and company data "
|
| 62 |
+
"to determine exactly where a lead is in their buying journey."
|
| 63 |
+
),
|
| 64 |
+
llm=self.mistral_llm,
|
| 65 |
+
verbose=True
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
@task
|
| 69 |
+
def identification_task(self) -> Task:
|
| 70 |
+
return Task(
|
| 71 |
+
description=(
|
| 72 |
+
"Analyze the provided {input_data}. If it's an IP, use the GetDomain tool. "
|
| 73 |
+
"If it's a company name, find its primary web domain. "
|
| 74 |
+
"Return the clear Company Name and Domain."
|
| 75 |
+
),
|
| 76 |
+
expected_output="A JSON-like string containing the verified Company Name and Domain.",
|
| 77 |
+
agent=self.identifier_agent()
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
@task
|
| 81 |
+
def enrichment_task(self) -> Task:
|
| 82 |
+
return Task(
|
| 83 |
+
description=(
|
| 84 |
+
"Using the domain from the identification task, use your tools to fetch: "
|
| 85 |
+
"1. Use ApolloEnrichmentTool with {domain} to get the company 'id'. "
|
| 86 |
+
"2. Use that 'id' with JobOpeningsTool and NewsTool to fetch growth signals. "
|
| 87 |
+
"3. Firmographics (Size, Revenue, HQ, Funding). "
|
| 88 |
+
"4. Technographics (Tech stack). "
|
| 89 |
+
"5. Growth signals (Recent job openings and news articles)."
|
| 90 |
+
),
|
| 91 |
+
expected_output="A comprehensive data summary of the company's current business state.",
|
| 92 |
+
agent=self.enrichment_agent(),
|
| 93 |
+
context=[self.identification_task()] # Depends on identifying the domain first
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
@task
|
| 97 |
+
def analysis_task(self) -> Task:
|
| 98 |
+
return Task(
|
| 99 |
+
description=(
|
| 100 |
+
"Review the enrichment data and the website activity signals: {activity_signals}. "
|
| 101 |
+
"Calculate an Intent Score (0.0 - 10.0) based on page visits and company growth. "
|
| 102 |
+
"Determine the 'Intent Stage' and provide 3 tailored sales actions. "
|
| 103 |
+
"Format everything into the required structured output."
|
| 104 |
+
),
|
| 105 |
+
expected_output="A structured AccountIntelligence report.",
|
| 106 |
+
agent=self.analyst_agent(),
|
| 107 |
+
context=[self.enrichment_task()],
|
| 108 |
+
output_pydantic=AccountIntelligence
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
@crew
|
| 112 |
+
def crew(self) -> Crew:
|
| 113 |
+
"""Creates the Fello AI Account Intelligence Crew"""
|
| 114 |
+
return Crew(
|
| 115 |
+
agents=self.agents,
|
| 116 |
+
tasks=self.tasks,
|
| 117 |
+
process=Process.sequential,
|
| 118 |
+
verbose=True
|
| 119 |
+
)
|
fello_crew/src/fello_crew/main.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
| 5 |
+
from fello_crew.crew import FelloAccountIntelligenceCrew
|
| 6 |
+
|
| 7 |
+
def run():
|
| 8 |
+
# Example inputs for testing
|
| 9 |
+
inputs = {
|
| 10 |
+
"input_data": "64.233.160.0",
|
| 11 |
+
"activity_signals": "/enterprise-docs, /pricing, /contact-us, time=10 min",
|
| 12 |
+
"domain": ""
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Initialize the crew
|
| 16 |
+
crew_instance = FelloAccountIntelligenceCrew().crew()
|
| 17 |
+
result = crew_instance.kickoff(inputs=inputs)
|
| 18 |
+
|
| 19 |
+
# Accessing the Pydantic structured output
|
| 20 |
+
report = result.pydantic
|
| 21 |
+
|
| 22 |
+
print("\n" + "="*50)
|
| 23 |
+
print("ACCOUNT INTELLIGENCE REPORT")
|
| 24 |
+
print("="*50)
|
| 25 |
+
print(f"Company: {report.company_name}")
|
| 26 |
+
print(f"Domain: {report.domain}")
|
| 27 |
+
print(f"Intent Score: {report.intent_score}/10")
|
| 28 |
+
print(f"Intent Stage: {report.intent_stage}")
|
| 29 |
+
print(f"Summary: {report.ai_summary}")
|
| 30 |
+
print("-" * 50)
|
| 31 |
+
print("RECOMMENDED ACTIONS:")
|
| 32 |
+
for action in report.recommended_sales_actions:
|
| 33 |
+
print(f" - {action}")
|
| 34 |
+
print("="*50)
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
run()
|
fello_crew/src/fello_crew/models.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
class AccountIntelligence(BaseModel):
|
| 5 |
+
company_name: str
|
| 6 |
+
domain: str
|
| 7 |
+
industry: str
|
| 8 |
+
company_size: int
|
| 9 |
+
headquarters: str
|
| 10 |
+
funding_summary: str
|
| 11 |
+
annual_revenue: str
|
| 12 |
+
likely_persona: str
|
| 13 |
+
leadership_team: List[str]
|
| 14 |
+
intent_score: float = Field(..., ge=0, le=10)
|
| 15 |
+
intent_stage: str
|
| 16 |
+
ai_summary: str
|
| 17 |
+
recommended_sales_actions: List[str]
|
| 18 |
+
tech_stack: List[str]
|
fello_crew/src/fello_crew/tools/__init__.py
ADDED
|
File without changes
|
fello_crew/src/fello_crew/tools/custom_tool.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from crewai.tools import BaseTool
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Type
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
class CompanySearchInput(BaseModel):
|
| 9 |
+
query: str = Field(..., description="The company domain (e.g., 'apple.com') to enrich.")
|
| 10 |
+
|
| 11 |
+
class ApolloEnrichmentTool(BaseTool):
|
| 12 |
+
name: str = "apollo_enrichment_tool"
|
| 13 |
+
description: str = "Fetches firmographics (size, industry, revenue) and technographics from Apollo."
|
| 14 |
+
args_schema: Type[BaseModel] = CompanySearchInput
|
| 15 |
+
|
| 16 |
+
def _run(self, query: str) -> str:
|
| 17 |
+
token = os.getenv("APOLLO_TOKEN")
|
| 18 |
+
# Apollo GET endpoint for organization enrichment
|
| 19 |
+
url = f"https://api.apollo.io/api/v1/organizations/enrich?domain={query}"
|
| 20 |
+
headers = {"Content-Type": "application/json", "x-api-key": token}
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
response = requests.get(url, headers=headers)
|
| 24 |
+
data = response.json().get("organization", {})
|
| 25 |
+
|
| 26 |
+
# Map Apollo's JSON fields to a clean summary for the agent
|
| 27 |
+
summary = {
|
| 28 |
+
"id": data.get("id"),
|
| 29 |
+
"name": data.get("name"),
|
| 30 |
+
"industry": data.get("industry"),
|
| 31 |
+
"employee_count": data.get("estimated_num_employees"),
|
| 32 |
+
"annual_revenue": data.get("annual_revenue_printed") or data.get("annual_revenue"),
|
| 33 |
+
"total_funding": data.get("total_funding_printed"),
|
| 34 |
+
"latest_funding_stage": data.get("latest_funding_stage"),
|
| 35 |
+
"latest_funding_date": data.get("latest_funding_round_date"),
|
| 36 |
+
"hq_address": data.get("raw_address"),
|
| 37 |
+
"hq_city": data.get("city"),
|
| 38 |
+
"hq_country": data.get("country"),
|
| 39 |
+
"tech_stack": data.get("technology_names", [])[0:80:5], # Limit to save tokens
|
| 40 |
+
"short_description": data.get("short_description"),
|
| 41 |
+
"linkedin_url": data.get("linkedin_url")
|
| 42 |
+
}
|
| 43 |
+
return str(summary)
|
| 44 |
+
except Exception as e:
|
| 45 |
+
return f"Enrichment error: {str(e)}"
|
| 46 |
+
class JobOpeningsInput(BaseModel):
|
| 47 |
+
org_id: str = Field(..., description="The Apollo Organization ID to fetch jobs for.")
|
| 48 |
+
|
| 49 |
+
class JobOpeningsTool(BaseTool):
|
| 50 |
+
name: str = "job_openings_tool"
|
| 51 |
+
description: str = "Returns a JSON object with total job count and sample titles to identify hiring signals."
|
| 52 |
+
args_schema: Type[BaseModel] = JobOpeningsInput
|
| 53 |
+
|
| 54 |
+
def _run(self, org_id: str) -> str:
|
| 55 |
+
token = os.getenv("APOLLO_TOKEN")
|
| 56 |
+
url = f"https://api.apollo.io/api/v1/organizations/{org_id}/job_postings"
|
| 57 |
+
headers = {"x-api-key": token}
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
response = requests.get(url, headers=headers)
|
| 61 |
+
res_json = response.json()
|
| 62 |
+
jobs = res_json.get("job_postings", [])
|
| 63 |
+
|
| 64 |
+
# Extract structured data
|
| 65 |
+
total_count = len(jobs)
|
| 66 |
+
sample_titles = [job.get("title") for job in jobs[:10]]
|
| 67 |
+
|
| 68 |
+
# Return as a JSON string
|
| 69 |
+
result = {
|
| 70 |
+
"total_openings_count": total_count,
|
| 71 |
+
"recent_job_titles": sample_titles
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return json.dumps(result)
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
return json.dumps({"error": str(e)})
|
| 78 |
+
|
| 79 |
+
class IPInput(BaseModel):
|
| 80 |
+
ip: str = Field(..., description="The IP address to resolve.")
|
| 81 |
+
|
| 82 |
+
class GetDomain(BaseTool):
|
| 83 |
+
name: str = "get_domain_from_ip"
|
| 84 |
+
description: str = "Resolves an IP address to a company name and location for identification."
|
| 85 |
+
args_schema: Type[BaseModel] = IPInput
|
| 86 |
+
|
| 87 |
+
def _run(self, ip: str) -> str:
|
| 88 |
+
try:
|
| 89 |
+
# Using the free ip-api.com service
|
| 90 |
+
response = requests.get(f"http://ip-api.com/json/{ip}")
|
| 91 |
+
data = response.json()
|
| 92 |
+
|
| 93 |
+
if data.get("status") == "fail":
|
| 94 |
+
return json.dumps({"error": f"Resolution failed: {data.get('message')}"})
|
| 95 |
+
|
| 96 |
+
# We return the 'org' which is the best clue for the domain
|
| 97 |
+
result = {
|
| 98 |
+
"organization": data.get("org"),
|
| 99 |
+
"isp": data.get("isp"),
|
| 100 |
+
"city": data.get("city"),
|
| 101 |
+
"country": data.get("country"),
|
| 102 |
+
"is_likely_corporate": data.get("org") != data.get("isp")
|
| 103 |
+
}
|
| 104 |
+
return json.dumps(result)
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
return json.dumps({"error": str(e)})
|
| 108 |
+
|
| 109 |
+
class PeopleEnrichmentInput(BaseModel):
|
| 110 |
+
domain: str = Field(..., description="The company domain to find leaders for.")
|
| 111 |
+
|
| 112 |
+
class MockPeopleEnrichmentTool(BaseTool):
|
| 113 |
+
name: str = "leadership_discovery_tool"
|
| 114 |
+
description: str = "Finds key executives (CEO, Founders, VP Sales) for a given company. (Currently using simulated data due to API tier constraints)."
|
| 115 |
+
args_schema: Type[BaseModel] = PeopleEnrichmentInput
|
| 116 |
+
|
| 117 |
+
def _run(self, domain: str) -> str:
|
| 118 |
+
# SIMULATED API DELAY
|
| 119 |
+
import time
|
| 120 |
+
time.sleep(1)
|
| 121 |
+
|
| 122 |
+
# MOCK DATA PAYLOAD
|
| 123 |
+
# In production, this would be: requests.post("https://api.apollo.io/api/v1/mixed_people/search", ...)
|
| 124 |
+
mock_leaders = [
|
| 125 |
+
{"name": "Jane Doe", "title": "Chief Executive Officer", "linkedin_url": f"linkedin.com/in/janedoe-{domain.split('.')[0]}"},
|
| 126 |
+
{"name": "John Smith", "title": "VP of Sales", "linkedin_url": f"linkedin.com/in/johnsmith-{domain.split('.')[0]}"},
|
| 127 |
+
{"name": "Alice Johnson", "title": "Head of Growth", "linkedin_url": f"linkedin.com/in/alicej-{domain.split('.')[0]}"}
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
return json.dumps({"leadership": mock_leaders, "note": "Simulated data for prototype"})
|
| 131 |
+
|
| 132 |
+
class NewsToolInput(BaseModel):
|
| 133 |
+
id: str = Field(..., description="The Apollo Organization ID of the company.")
|
| 134 |
+
|
| 135 |
+
class NewsTool(BaseTool):
|
| 136 |
+
name: str = "news_tool"
|
| 137 |
+
description: str = "Fetches the latest news articles for a company to identify recent business signals."
|
| 138 |
+
args_schema: Type[BaseModel] = NewsToolInput
|
| 139 |
+
|
| 140 |
+
def _run(self, id: str) -> str:
|
| 141 |
+
token = os.getenv("APOLLO_TOKEN")
|
| 142 |
+
url = f"https://api.apollo.io/api/v1/news_articles/search?organization_ids[]={id}"
|
| 143 |
+
|
| 144 |
+
headers = {
|
| 145 |
+
"Content-Type": "application/json",
|
| 146 |
+
"accept": "application/json",
|
| 147 |
+
"x-api-key": token
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
response = requests.post(url, headers=headers, json={})
|
| 152 |
+
data = response.json()
|
| 153 |
+
|
| 154 |
+
articles = data.get("news_articles", [])
|
| 155 |
+
|
| 156 |
+
latest_news = [
|
| 157 |
+
{
|
| 158 |
+
"title": art.get("title"),
|
| 159 |
+
"source": art.get("domain"),
|
| 160 |
+
"date": art.get("published_at"),
|
| 161 |
+
"snippet": art.get("snippet")
|
| 162 |
+
}
|
| 163 |
+
for art in articles[:5]
|
| 164 |
+
]
|
| 165 |
+
|
| 166 |
+
result = {
|
| 167 |
+
"total_news_mentions": data.get("pagination", {}).get("total_entries", 0),
|
| 168 |
+
"latest_articles": latest_news
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
return json.dumps(result)
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
return json.dumps({"error": str(e)})
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 6 |
+
|
| 7 |
+
export default defineConfig([
|
| 8 |
+
globalIgnores(['dist']),
|
| 9 |
+
{
|
| 10 |
+
files: ['**/*.{js,jsx}'],
|
| 11 |
+
extends: [
|
| 12 |
+
js.configs.recommended,
|
| 13 |
+
reactHooks.configs.flat.recommended,
|
| 14 |
+
reactRefresh.configs.vite,
|
| 15 |
+
],
|
| 16 |
+
languageOptions: {
|
| 17 |
+
ecmaVersion: 2020,
|
| 18 |
+
globals: globals.browser,
|
| 19 |
+
parserOptions: {
|
| 20 |
+
ecmaVersion: 'latest',
|
| 21 |
+
ecmaFeatures: { jsx: true },
|
| 22 |
+
sourceType: 'module',
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
rules: {
|
| 26 |
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>frontend</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tailwindcss/vite": "^4.2.1",
|
| 14 |
+
"lucide-react": "^0.577.0",
|
| 15 |
+
"react": "^19.2.4",
|
| 16 |
+
"react-dom": "^19.2.4"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@eslint/js": "^9.39.4",
|
| 20 |
+
"@types/react": "^19.2.14",
|
| 21 |
+
"@types/react-dom": "^19.2.3",
|
| 22 |
+
"@vitejs/plugin-react": "^6.0.0",
|
| 23 |
+
"autoprefixer": "^10.4.27",
|
| 24 |
+
"eslint": "^9.39.4",
|
| 25 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 26 |
+
"eslint-plugin-react-refresh": "^0.5.2",
|
| 27 |
+
"globals": "^17.4.0",
|
| 28 |
+
"postcss": "^8.5.8",
|
| 29 |
+
"tailwindcss": "^4.2.1",
|
| 30 |
+
"vite": "^8.0.0"
|
| 31 |
+
}
|
| 32 |
+
}
|
frontend/public/favicon.svg
ADDED
|
|
frontend/public/icons.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.counter {
|
| 2 |
+
font-size: 16px;
|
| 3 |
+
padding: 5px 10px;
|
| 4 |
+
border-radius: 5px;
|
| 5 |
+
color: var(--accent);
|
| 6 |
+
background: var(--accent-bg);
|
| 7 |
+
border: 2px solid transparent;
|
| 8 |
+
transition: border-color 0.3s;
|
| 9 |
+
margin-bottom: 24px;
|
| 10 |
+
|
| 11 |
+
&:hover {
|
| 12 |
+
border-color: var(--accent-border);
|
| 13 |
+
}
|
| 14 |
+
&:focus-visible {
|
| 15 |
+
outline: 2px solid var(--accent);
|
| 16 |
+
outline-offset: 2px;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.hero {
|
| 21 |
+
position: relative;
|
| 22 |
+
|
| 23 |
+
.base,
|
| 24 |
+
.framework,
|
| 25 |
+
.vite {
|
| 26 |
+
inset-inline: 0;
|
| 27 |
+
margin: 0 auto;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.base {
|
| 31 |
+
width: 170px;
|
| 32 |
+
position: relative;
|
| 33 |
+
z-index: 0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.framework,
|
| 37 |
+
.vite {
|
| 38 |
+
position: absolute;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.framework {
|
| 42 |
+
z-index: 1;
|
| 43 |
+
top: 34px;
|
| 44 |
+
height: 28px;
|
| 45 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
| 46 |
+
scale(1.4);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.vite {
|
| 50 |
+
z-index: 0;
|
| 51 |
+
top: 107px;
|
| 52 |
+
height: 26px;
|
| 53 |
+
width: auto;
|
| 54 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
| 55 |
+
scale(0.8);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
#center {
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
gap: 25px;
|
| 63 |
+
place-content: center;
|
| 64 |
+
place-items: center;
|
| 65 |
+
flex-grow: 1;
|
| 66 |
+
|
| 67 |
+
@media (max-width: 1024px) {
|
| 68 |
+
padding: 32px 20px 24px;
|
| 69 |
+
gap: 18px;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#next-steps {
|
| 74 |
+
display: flex;
|
| 75 |
+
border-top: 1px solid var(--border);
|
| 76 |
+
text-align: left;
|
| 77 |
+
|
| 78 |
+
& > div {
|
| 79 |
+
flex: 1 1 0;
|
| 80 |
+
padding: 32px;
|
| 81 |
+
@media (max-width: 1024px) {
|
| 82 |
+
padding: 24px 20px;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.icon {
|
| 87 |
+
margin-bottom: 16px;
|
| 88 |
+
width: 22px;
|
| 89 |
+
height: 22px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@media (max-width: 1024px) {
|
| 93 |
+
flex-direction: column;
|
| 94 |
+
text-align: center;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
#docs {
|
| 99 |
+
border-right: 1px solid var(--border);
|
| 100 |
+
|
| 101 |
+
@media (max-width: 1024px) {
|
| 102 |
+
border-right: none;
|
| 103 |
+
border-bottom: 1px solid var(--border);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
#next-steps ul {
|
| 108 |
+
list-style: none;
|
| 109 |
+
padding: 0;
|
| 110 |
+
display: flex;
|
| 111 |
+
gap: 8px;
|
| 112 |
+
margin: 32px 0 0;
|
| 113 |
+
|
| 114 |
+
.logo {
|
| 115 |
+
height: 18px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
a {
|
| 119 |
+
color: var(--text-h);
|
| 120 |
+
font-size: 16px;
|
| 121 |
+
border-radius: 6px;
|
| 122 |
+
background: var(--social-bg);
|
| 123 |
+
display: flex;
|
| 124 |
+
padding: 6px 12px;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 8px;
|
| 127 |
+
text-decoration: none;
|
| 128 |
+
transition: box-shadow 0.3s;
|
| 129 |
+
|
| 130 |
+
&:hover {
|
| 131 |
+
box-shadow: var(--shadow);
|
| 132 |
+
}
|
| 133 |
+
.button-icon {
|
| 134 |
+
height: 18px;
|
| 135 |
+
width: 18px;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@media (max-width: 1024px) {
|
| 140 |
+
margin-top: 20px;
|
| 141 |
+
flex-wrap: wrap;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
|
| 144 |
+
li {
|
| 145 |
+
flex: 1 1 calc(50% - 8px);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
a {
|
| 149 |
+
width: 100%;
|
| 150 |
+
justify-content: center;
|
| 151 |
+
box-sizing: border-box;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#spacer {
|
| 157 |
+
height: 88px;
|
| 158 |
+
border-top: 1px solid var(--border);
|
| 159 |
+
@media (max-width: 1024px) {
|
| 160 |
+
height: 48px;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.ticks {
|
| 165 |
+
position: relative;
|
| 166 |
+
width: 100%;
|
| 167 |
+
|
| 168 |
+
&::before,
|
| 169 |
+
&::after {
|
| 170 |
+
content: '';
|
| 171 |
+
position: absolute;
|
| 172 |
+
top: -4.5px;
|
| 173 |
+
border: 5px solid transparent;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
&::before {
|
| 177 |
+
left: 0;
|
| 178 |
+
border-left-color: var(--border);
|
| 179 |
+
}
|
| 180 |
+
&::after {
|
| 181 |
+
right: 0;
|
| 182 |
+
border-right-color: var(--border);
|
| 183 |
+
}
|
| 184 |
+
}
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import FelloDashboard from './components/Dashboard'
|
| 2 |
+
|
| 3 |
+
function App() {
|
| 4 |
+
return (
|
| 5 |
+
<FelloDashboard />
|
| 6 |
+
)
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default App
|
frontend/src/assets/hero.png
ADDED
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/assets/vite.svg
ADDED
|
|
frontend/src/components/Dashboard.jsx
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { Terminal, Building2, Target, Zap, Play, FileText, Share2, Globe, CheckCircle, Users, Cpu, Info } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
// --- GLOBAL URL CONFIG ---
|
| 5 |
+
// Automatically switches between local and production backend
|
| 6 |
+
const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
| 7 |
+
? 'http://localhost:8000'
|
| 8 |
+
: '';
|
| 9 |
+
|
| 10 |
+
export default function FelloDashboard() {
|
| 11 |
+
const [ipValue, setIpValue] = useState('64.233.160.0');
|
| 12 |
+
const [domainValue, setDomainValue] = useState('google.com');
|
| 13 |
+
const [activityValue, setActivityValue] = useState('/enterprise-docs, /pricing, /contact-us, time=10 min');
|
| 14 |
+
|
| 15 |
+
const [logs, setLogs] = useState([]);
|
| 16 |
+
const [result, setResult] = useState(null);
|
| 17 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 18 |
+
const [isSyncing, setIsSyncing] = useState(false);
|
| 19 |
+
const logsEndRef = useRef(null);
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 23 |
+
}, [logs]);
|
| 24 |
+
|
| 25 |
+
// --- CORE LOGIC: RUN AGENT ---
|
| 26 |
+
const handleAnalyze = async () => {
|
| 27 |
+
setIsProcessing(true);
|
| 28 |
+
setLogs(['[SYSTEM] Initiating Intelligence Protocol...']);
|
| 29 |
+
setResult(null);
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const response = await fetch(`${API_BASE}/analyze-stream`, {
|
| 33 |
+
method: 'POST',
|
| 34 |
+
headers: { 'Content-Type': 'application/json' },
|
| 35 |
+
body: JSON.stringify({
|
| 36 |
+
"input_data": ipValue,
|
| 37 |
+
"activity_signals": activityValue,
|
| 38 |
+
"domain": domainValue
|
| 39 |
+
})
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
const reader = response.body.getReader();
|
| 43 |
+
const decoder = new TextDecoder();
|
| 44 |
+
|
| 45 |
+
while (true) {
|
| 46 |
+
const { value, done } = await reader.read();
|
| 47 |
+
if (done) break;
|
| 48 |
+
const chunk = decoder.decode(value);
|
| 49 |
+
const lines = chunk.split('\n\n');
|
| 50 |
+
for (const line of lines) {
|
| 51 |
+
if (line.startsWith('data: ')) {
|
| 52 |
+
const data = JSON.parse(line.replace('data: ', ''));
|
| 53 |
+
if (data.message === 'COMPLETE') {
|
| 54 |
+
setResult(data.result);
|
| 55 |
+
setLogs(prev => [...prev, '[SUCCESS] Analysis complete.']);
|
| 56 |
+
} else {
|
| 57 |
+
setLogs(prev => [...prev, `> ${data.message}`]);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
} catch (error) {
|
| 63 |
+
setLogs(prev => [...prev, `[ERROR] ${error.message}`]);
|
| 64 |
+
} finally { setIsProcessing(false); }
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
const handleDownloadPDF = async () => {
|
| 68 |
+
if (!result) return;
|
| 69 |
+
try {
|
| 70 |
+
const res = await fetch(`${API_BASE}/generate-report`, {
|
| 71 |
+
method: 'POST',
|
| 72 |
+
headers: { 'Content-Type': 'application/json' },
|
| 73 |
+
body: JSON.stringify(result)
|
| 74 |
+
});
|
| 75 |
+
const blob = await res.blob();
|
| 76 |
+
const url = window.URL.createObjectURL(blob);
|
| 77 |
+
const a = document.createElement('a');
|
| 78 |
+
a.href = url;
|
| 79 |
+
a.download = `Fello_Report_${result.company_name || 'Account'}.pdf`;
|
| 80 |
+
document.body.appendChild(a);
|
| 81 |
+
a.click();
|
| 82 |
+
a.remove();
|
| 83 |
+
} catch (err) { alert("PDF generation failed."); }
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const handleSyncSheets = async () => {
|
| 87 |
+
if (!result) return;
|
| 88 |
+
setIsSyncing(true);
|
| 89 |
+
try {
|
| 90 |
+
await fetch(`${API_BASE}/sync-sheets`, {
|
| 91 |
+
method: 'POST',
|
| 92 |
+
headers: { 'Content-Type': 'application/json' },
|
| 93 |
+
body: JSON.stringify(result)
|
| 94 |
+
});
|
| 95 |
+
setLogs(prev => [...prev, '[SYSTEM] Data successfully synced to Google Sheets.']);
|
| 96 |
+
} catch (err) { setLogs(prev => [...prev, '[ERROR] Sheets sync failed.']); }
|
| 97 |
+
finally { setIsSyncing(false); }
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
return (
|
| 101 |
+
<div className="min-h-screen bg-zinc-50 text-zinc-900">
|
| 102 |
+
<nav className="bg-white border-b border-zinc-200 px-8 py-4 flex justify-between items-center sticky top-0 z-10">
|
| 103 |
+
<div className="flex items-center gap-2">
|
| 104 |
+
<Zap className="w-6 h-6 text-blue-600 fill-current" />
|
| 105 |
+
<span className="font-bold text-xl tracking-tight uppercase">Fello IQ</span>
|
| 106 |
+
</div>
|
| 107 |
+
<div className="text-xs font-bold px-3 py-1 bg-zinc-100 rounded-full border border-zinc-200">
|
| 108 |
+
MODE: <span className="text-blue-600 font-black">ENRICHMENT</span>
|
| 109 |
+
</div>
|
| 110 |
+
</nav>
|
| 111 |
+
|
| 112 |
+
<main className="max-w-7xl mx-auto p-8 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
| 113 |
+
{/* INPUTS & LOGS */}
|
| 114 |
+
<div className="lg:col-span-4 space-y-6">
|
| 115 |
+
<div className="bg-white border border-zinc-200 rounded-2xl p-6 shadow-sm">
|
| 116 |
+
<h2 className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 mb-6">Signals</h2>
|
| 117 |
+
<div className="space-y-4">
|
| 118 |
+
<div>
|
| 119 |
+
<label className="text-[10px] font-bold text-zinc-500 uppercase block mb-1">IP Address</label>
|
| 120 |
+
<input className="w-full bg-zinc-50 border border-zinc-200 rounded-lg p-3 text-sm outline-none focus:ring-2 ring-blue-500/10" value={ipValue} onChange={(e) => setIpValue(e.target.value)} />
|
| 121 |
+
</div>
|
| 122 |
+
<div>
|
| 123 |
+
<label className="text-[10px] font-bold text-zinc-500 uppercase block mb-1">Domain</label>
|
| 124 |
+
<input className="w-full bg-zinc-50 border border-zinc-200 rounded-lg p-3 text-sm outline-none focus:ring-2 ring-blue-500/10" value={domainValue} onChange={(e) => setDomainValue(e.target.value)} />
|
| 125 |
+
</div>
|
| 126 |
+
<div>
|
| 127 |
+
<label className="text-[10px] font-bold text-zinc-500 uppercase block mb-1">Activity</label>
|
| 128 |
+
<textarea className="w-full bg-zinc-50 border border-zinc-200 rounded-lg p-3 text-sm h-24 outline-none focus:ring-2 ring-blue-500/10" value={activityValue} onChange={(e) => setActivityValue(e.target.value)} />
|
| 129 |
+
</div>
|
| 130 |
+
<button onClick={handleAnalyze} disabled={isProcessing} className="w-full bg-black text-white font-bold py-3.5 rounded-xl hover:opacity-90 transition-all flex items-center justify-center gap-2 disabled:opacity-50">
|
| 131 |
+
<Play className="w-4 h-4 fill-current" /> {isProcessing ? "Researching..." : "Launch Crew"}
|
| 132 |
+
</button>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div className="bg-zinc-900 rounded-2xl p-4 shadow-xl overflow-hidden">
|
| 137 |
+
<div className="flex items-center gap-2 mb-3 border-b border-zinc-800 pb-2">
|
| 138 |
+
<Terminal className="w-3 h-3 text-zinc-500" />
|
| 139 |
+
<span className="text-[9px] font-mono text-zinc-500 uppercase tracking-widest">Process Stream</span>
|
| 140 |
+
</div>
|
| 141 |
+
<div className="font-mono text-[10px] h-40 overflow-y-auto text-zinc-400 custom-scrollbar">
|
| 142 |
+
{logs.map((log, i) => <div key={i} className={log.includes('[SUCCESS]') ? 'text-blue-400 font-bold' : ''}>_ {log}</div>)}
|
| 143 |
+
<div ref={logsEndRef} />
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
{/* RESULTS AREA */}
|
| 149 |
+
<div className="lg:col-span-8">
|
| 150 |
+
{result ? (
|
| 151 |
+
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
| 152 |
+
<div className="grid grid-cols-3 gap-4">
|
| 153 |
+
<div className="bg-white border border-zinc-200 p-6 rounded-2xl shadow-sm text-center">
|
| 154 |
+
<div className="text-[10px] font-bold text-zinc-400 uppercase mb-1">Intent</div>
|
| 155 |
+
<div className="text-3xl font-black">{result.intent_score}<span className="text-sm text-blue-600">/10</span></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="bg-white border border-zinc-200 p-6 rounded-2xl shadow-sm text-center">
|
| 158 |
+
<div className="text-[10px] font-bold text-zinc-400 uppercase mb-1">Stage</div>
|
| 159 |
+
<div className="text-lg font-bold text-blue-600">{result.intent_stage}</div>
|
| 160 |
+
</div>
|
| 161 |
+
<div className="bg-white border border-zinc-200 p-6 rounded-2xl shadow-sm text-center">
|
| 162 |
+
<div className="text-[10px] font-bold text-zinc-400 uppercase mb-1">Revenue</div>
|
| 163 |
+
<div className="text-lg font-bold">{result.annual_revenue}</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div className="bg-white border border-zinc-200 rounded-3xl p-8 shadow-sm">
|
| 168 |
+
<h3 className="text-xl font-black mb-6 flex items-center gap-3">
|
| 169 |
+
<div className="w-2 h-8 bg-blue-600 rounded-full" /> {result.company_name}
|
| 170 |
+
</h3>
|
| 171 |
+
|
| 172 |
+
<p className="text-zinc-600 leading-relaxed text-sm mb-4 italic">"{result.ai_summary}"</p>
|
| 173 |
+
|
| 174 |
+
{/* LIKELY PERSONA BADGE ADDED HERE */}
|
| 175 |
+
{result.likely_persona && (
|
| 176 |
+
<div className="mb-8 inline-flex items-center gap-2 px-3 py-1.5 bg-blue-50 text-blue-700 rounded-lg text-xs font-bold border border-blue-100">
|
| 177 |
+
<Target className="w-3.5 h-3.5" /> Target Persona: {result.likely_persona}
|
| 178 |
+
</div>
|
| 179 |
+
)}
|
| 180 |
+
|
| 181 |
+
<div className="grid grid-cols-2 gap-12">
|
| 182 |
+
<div className="space-y-8">
|
| 183 |
+
<div>
|
| 184 |
+
<h4 className="text-[10px] font-black uppercase text-zinc-400 tracking-widest mb-4 flex items-center gap-2"><Target className="w-3 h-3"/> Sales Actions</h4>
|
| 185 |
+
<ul className="space-y-3">
|
| 186 |
+
{result.recommended_sales_actions?.map((a, i) => (
|
| 187 |
+
<li key={i} className="text-xs flex items-start gap-2 font-medium">
|
| 188 |
+
<CheckCircle className="w-4 h-4 text-blue-600 shrink-0 mt-0.5" /> {a}
|
| 189 |
+
</li>
|
| 190 |
+
))}
|
| 191 |
+
</ul>
|
| 192 |
+
</div>
|
| 193 |
+
<div>
|
| 194 |
+
<h4 className="text-[10px] font-black uppercase text-zinc-400 tracking-widest mb-4 flex items-center gap-2"><Cpu className="w-3 h-3"/> Tech Stack</h4>
|
| 195 |
+
<div className="flex flex-wrap gap-2">
|
| 196 |
+
{result.tech_stack?.map((t, i) => (
|
| 197 |
+
<span key={i} className="px-2 py-1 bg-zinc-100 border border-zinc-200 rounded text-[9px] font-bold text-zinc-600 italic">#{t}</span>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<div className="space-y-8">
|
| 204 |
+
<div>
|
| 205 |
+
<h4 className="text-[10px] font-black uppercase text-zinc-400 tracking-widest mb-4">Firmographics</h4>
|
| 206 |
+
<div className="space-y-2 text-xs">
|
| 207 |
+
<div className="flex justify-between"><span className="text-zinc-400">HQ</span> <span className="font-bold">{result.headquarters}</span></div>
|
| 208 |
+
<div className="flex justify-between"><span className="text-zinc-400">Industry</span> <span className="font-bold">{result.industry}</span></div>
|
| 209 |
+
<div className="flex justify-between"><span className="text-zinc-400">Size</span> <span className="font-bold">{result.company_size} emp</span></div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div>
|
| 213 |
+
<h4 className="text-[10px] font-black uppercase text-zinc-400 tracking-widest mb-3 flex items-center gap-2"><Users className="w-3 h-3"/> Leadership</h4>
|
| 214 |
+
<div className="space-y-1.5 mb-2">
|
| 215 |
+
{result.leadership_team?.map((member, i) => (
|
| 216 |
+
<div key={i} className="text-[11px] font-medium text-zinc-700 bg-zinc-50 px-2 py-1 rounded border border-zinc-100">{member}</div>
|
| 217 |
+
))}
|
| 218 |
+
</div>
|
| 219 |
+
<div className="flex items-center gap-1.5 text-[9px] text-zinc-400 italic">
|
| 220 |
+
<Info className="w-3 h-3" /> Note: Leadership data is mock (Tier restricted API)
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div className="flex gap-4">
|
| 228 |
+
<button onClick={handleDownloadPDF} className="flex-1 bg-white border border-zinc-200 py-4 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-zinc-50 shadow-sm transition-all active:scale-95">
|
| 229 |
+
<FileText className="w-5 h-5 text-red-500" /> Download PDF
|
| 230 |
+
</button>
|
| 231 |
+
<button onClick={handleSyncSheets} disabled={isSyncing} className="flex-1 bg-white border border-zinc-200 py-4 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-zinc-50 shadow-sm disabled:opacity-50 transition-all active:scale-95">
|
| 232 |
+
<Share2 className="w-5 h-5 text-emerald-500" /> {isSyncing ? "Syncing..." : "Sync to Sheets"}
|
| 233 |
+
</button>
|
| 234 |
+
<a href="https://docs.google.com/spreadsheets/d/181US_5P5PnubcMmjR8ByPgmTuIMUjL4wSVEVMRgPz1M/" target="_blank" rel="noreferrer" className="flex-1 bg-white border border-zinc-200 py-4 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-zinc-50 shadow-sm transition-all active:scale-95">
|
| 235 |
+
<Globe className="w-5 h-5 text-blue-500" /> View Sheet
|
| 236 |
+
</a>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
) : (
|
| 240 |
+
<div className="h-full min-h-[500px] border-2 border-dashed border-zinc-200 rounded-[3rem] flex flex-col items-center justify-center text-zinc-400 gap-4 opacity-40">
|
| 241 |
+
<Globe className="w-16 h-16" />
|
| 242 |
+
<p className="font-black text-xs uppercase tracking-[0.3em]">Awaiting Identity Signals</p>
|
| 243 |
+
</div>
|
| 244 |
+
)}
|
| 245 |
+
</div>
|
| 246 |
+
</main>
|
| 247 |
+
</div>
|
| 248 |
+
);
|
| 249 |
+
}
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
@theme {
|
| 4 |
+
--color-brand-primary: #000000;
|
| 5 |
+
--color-brand-secondary: #f5f5f7;
|
| 6 |
+
--color-brand-border: #e5e7eb;
|
| 7 |
+
--color-brand-accent: #2563eb;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
@layer base {
|
| 11 |
+
body {
|
| 12 |
+
@apply bg-white text-zinc-900 antialiased;
|
| 13 |
+
}
|
| 14 |
+
}@import "tailwindcss";
|
| 15 |
+
|
| 16 |
+
@theme {
|
| 17 |
+
--color-brand-primary: #000000;
|
| 18 |
+
--color-brand-secondary: #f5f5f7;
|
| 19 |
+
--color-brand-border: #e5e7eb;
|
| 20 |
+
--color-brand-accent: #2563eb;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@layer base {
|
| 24 |
+
body {
|
| 25 |
+
@apply bg-white text-zinc-900 antialiased;
|
| 26 |
+
}
|
| 27 |
+
}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.jsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite'
|
| 4 |
+
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [
|
| 7 |
+
react(),
|
| 8 |
+
tailwindcss(),
|
| 9 |
+
],
|
| 10 |
+
})
|
requirements.txt
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohappyeyeballs==2.6.1
|
| 2 |
+
aiohttp==3.13.3
|
| 3 |
+
aiosignal==1.4.0
|
| 4 |
+
aiosqlite==0.21.0
|
| 5 |
+
annotated-doc==0.0.4
|
| 6 |
+
annotated-types==0.7.0
|
| 7 |
+
anyio==4.12.1
|
| 8 |
+
appdirs==1.4.4
|
| 9 |
+
async-timeout==5.0.1
|
| 10 |
+
attrs==25.4.0
|
| 11 |
+
backoff==2.2.1
|
| 12 |
+
bcrypt==5.0.0
|
| 13 |
+
beautifulsoup4==4.13.5
|
| 14 |
+
build==1.4.0
|
| 15 |
+
certifi==2026.2.25
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
charset-normalizer==3.4.5
|
| 18 |
+
chromadb==1.1.1
|
| 19 |
+
click==8.1.8
|
| 20 |
+
coloredlogs==15.0.1
|
| 21 |
+
crewai==1.6.1
|
| 22 |
+
crewai-tools==1.10.1
|
| 23 |
+
cryptography==46.0.5
|
| 24 |
+
defusedxml==0.7.1
|
| 25 |
+
deprecation==2.1.0
|
| 26 |
+
diskcache==5.6.3
|
| 27 |
+
distro==1.9.0
|
| 28 |
+
docker==7.1.0
|
| 29 |
+
docstring_parser==0.17.0
|
| 30 |
+
durationpy==0.10
|
| 31 |
+
et_xmlfile==2.0.0
|
| 32 |
+
eval_type_backport==0.3.1
|
| 33 |
+
exceptiongroup==1.3.1
|
| 34 |
+
fastapi==0.135.1
|
| 35 |
+
fastuuid==0.14.0
|
| 36 |
+
filelock==3.25.2
|
| 37 |
+
flatbuffers==25.12.19
|
| 38 |
+
fpdf==1.7.2
|
| 39 |
+
frozenlist==1.8.0
|
| 40 |
+
fsspec==2026.2.0
|
| 41 |
+
google-api-core==2.30.0
|
| 42 |
+
google-api-python-client==2.192.0
|
| 43 |
+
google-auth==2.49.1
|
| 44 |
+
google-auth-httplib2==0.3.0
|
| 45 |
+
google-auth-oauthlib==1.3.0
|
| 46 |
+
googleapis-common-protos==1.73.0
|
| 47 |
+
grpcio==1.78.0
|
| 48 |
+
h11==0.16.0
|
| 49 |
+
hf-xet==1.4.2
|
| 50 |
+
httpcore==1.0.9
|
| 51 |
+
httplib2==0.31.2
|
| 52 |
+
httptools==0.7.1
|
| 53 |
+
httpx==0.28.1
|
| 54 |
+
httpx-sse==0.4.3
|
| 55 |
+
huggingface_hub==1.7.1
|
| 56 |
+
humanfriendly==10.0
|
| 57 |
+
idna==3.11
|
| 58 |
+
importlib_metadata==8.7.1
|
| 59 |
+
importlib_resources==6.5.2
|
| 60 |
+
instructor==1.14.5
|
| 61 |
+
Jinja2==3.1.6
|
| 62 |
+
jiter==0.11.1
|
| 63 |
+
json5==0.10.0
|
| 64 |
+
json_repair==0.25.2
|
| 65 |
+
jsonref==1.1.0
|
| 66 |
+
jsonschema==4.26.0
|
| 67 |
+
jsonschema-specifications==2025.9.1
|
| 68 |
+
kubernetes==35.0.0
|
| 69 |
+
lance-namespace==0.5.2
|
| 70 |
+
lance-namespace-urllib3-client==0.5.2
|
| 71 |
+
lancedb==0.29.2
|
| 72 |
+
linkify-it-py==2.1.0
|
| 73 |
+
litellm==1.82.2
|
| 74 |
+
lxml==6.0.2
|
| 75 |
+
markdown-it-py==4.0.0
|
| 76 |
+
MarkupSafe==3.0.3
|
| 77 |
+
mcp==1.26.0
|
| 78 |
+
mdit-py-plugins==0.5.0
|
| 79 |
+
mdurl==0.1.2
|
| 80 |
+
mistralai==2.0.2
|
| 81 |
+
mmh3==5.2.1
|
| 82 |
+
mpmath==1.3.0
|
| 83 |
+
multidict==6.7.1
|
| 84 |
+
numpy==2.2.6
|
| 85 |
+
oauthlib==3.3.1
|
| 86 |
+
onnxruntime==1.23.2
|
| 87 |
+
openai==2.28.0
|
| 88 |
+
openpyxl==3.1.5
|
| 89 |
+
opentelemetry-api==1.39.1
|
| 90 |
+
opentelemetry-exporter-otlp-proto-common==1.39.1
|
| 91 |
+
opentelemetry-exporter-otlp-proto-grpc==1.39.1
|
| 92 |
+
opentelemetry-exporter-otlp-proto-http==1.39.1
|
| 93 |
+
opentelemetry-proto==1.39.1
|
| 94 |
+
opentelemetry-sdk==1.39.1
|
| 95 |
+
opentelemetry-semantic-conventions==0.60b1
|
| 96 |
+
orjson==3.11.7
|
| 97 |
+
overrides==7.7.0
|
| 98 |
+
packaging==26.0
|
| 99 |
+
pdfminer.six==20251230
|
| 100 |
+
pdfplumber==0.11.9
|
| 101 |
+
pillow==12.1.1
|
| 102 |
+
platformdirs==4.9.4
|
| 103 |
+
portalocker==2.7.0
|
| 104 |
+
posthog==5.4.0
|
| 105 |
+
propcache==0.4.1
|
| 106 |
+
proto-plus==1.27.1
|
| 107 |
+
protobuf==5.29.6
|
| 108 |
+
pyarrow==23.0.1
|
| 109 |
+
pyasn1==0.6.2
|
| 110 |
+
pyasn1_modules==0.4.2
|
| 111 |
+
pybase64==1.4.3
|
| 112 |
+
pycparser==3.0
|
| 113 |
+
pydantic==2.11.10
|
| 114 |
+
pydantic-settings==2.10.1
|
| 115 |
+
pydantic_core==2.33.2
|
| 116 |
+
Pygments==2.19.2
|
| 117 |
+
PyJWT==2.12.1
|
| 118 |
+
PyMuPDF==1.26.7
|
| 119 |
+
pyparsing==3.3.2
|
| 120 |
+
pypdfium2==5.6.0
|
| 121 |
+
PyPika==0.51.1
|
| 122 |
+
pyproject_hooks==1.2.0
|
| 123 |
+
python-dateutil==2.9.0.post0
|
| 124 |
+
python-docx==1.2.0
|
| 125 |
+
python-dotenv==1.1.1
|
| 126 |
+
python-multipart==0.0.22
|
| 127 |
+
pytube==15.0.0
|
| 128 |
+
PyYAML==6.0.3
|
| 129 |
+
referencing==0.37.0
|
| 130 |
+
regex==2026.1.15
|
| 131 |
+
requests==2.32.5
|
| 132 |
+
requests-oauthlib==2.0.0
|
| 133 |
+
rich==14.3.3
|
| 134 |
+
rpds-py==0.30.0
|
| 135 |
+
shellingham==1.5.4
|
| 136 |
+
six==1.17.0
|
| 137 |
+
sniffio==1.3.1
|
| 138 |
+
soupsieve==2.8.3
|
| 139 |
+
sse-starlette==3.3.2
|
| 140 |
+
starlette==0.52.1
|
| 141 |
+
sympy==1.14.0
|
| 142 |
+
tenacity==9.1.4
|
| 143 |
+
textual==8.1.1
|
| 144 |
+
tiktoken==0.8.0
|
| 145 |
+
tokenizers==0.22.2
|
| 146 |
+
tomli==2.0.2
|
| 147 |
+
tomli_w==1.1.0
|
| 148 |
+
tqdm==4.67.3
|
| 149 |
+
typer==0.23.1
|
| 150 |
+
typing-inspection==0.4.2
|
| 151 |
+
typing_extensions==4.15.0
|
| 152 |
+
uc-micro-py==2.0.0
|
| 153 |
+
uritemplate==4.2.0
|
| 154 |
+
urllib3==2.6.3
|
| 155 |
+
uv==0.9.30
|
| 156 |
+
uvicorn==0.41.0
|
| 157 |
+
uvloop==0.22.1
|
| 158 |
+
watchfiles==1.1.1
|
| 159 |
+
websocket-client==1.9.0
|
| 160 |
+
websockets==16.0
|
| 161 |
+
yarl==1.23.0
|
| 162 |
+
youtube-transcript-api==1.2.4
|
| 163 |
+
zipp==3.23.0
|