umar-100 commited on
Commit
da90d9b
·
1 Parent(s): 2bd2b06
.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-intel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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