├── crewai_be ├── tools │ ├── __init__.py │ └── youtube_search_tools.py ├── utils │ ├── __init__.py │ └── logging.py ├── .gitignore ├── models.py ├── pyproject.toml ├── job_manager.py ├── crew.py ├── api.py ├── tasks.py └── agents.py ├── nextjs_app ├── .eslintrc.json ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── next.config.mjs ├── postcss.config.js ├── types │ └── index.ts ├── components │ ├── Header.tsx │ ├── EventLog.tsx │ ├── InputSection.tsx │ └── FinalOutput.tsx ├── .gitignore ├── tailwind.config.ts ├── public │ ├── vercel.svg │ └── next.svg ├── tsconfig.json ├── package.json ├── README.md └── hooks │ └── useCrewJob.tsx └── README.md /crewai_be/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crewai_be/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crewai_be/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ -------------------------------------------------------------------------------- /nextjs_app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /nextjs_app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/nextjs-crewai-basic-tutorial/HEAD/nextjs_app/app/favicon.ico -------------------------------------------------------------------------------- /nextjs_app/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /nextjs_app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /nextjs_app/types/index.ts: -------------------------------------------------------------------------------- 1 | // Define the structure of an event. 2 | export type Event = { 3 | timestamp: Date; 4 | data: string; 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the NextJS CrewAI Full Stack Tutorial 2 | To see how to build and run this fullstack application, checkout the step-by-step YouTube tutorial here: https://youtu.be/d8juNbo3onk 3 | -------------------------------------------------------------------------------- /nextjs_app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | export default function Header() { 2 | return ( 3 |
4 |
5 |

6 | NextJS CrewAI Tutorial 7 |

8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /crewai_be/utils/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | # Set up basic configuration for logging 4 | logging.basicConfig(level=logging.INFO, 5 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 6 | datefmt='%Y-%m-%d %H:%M:%S') 7 | 8 | # Now you can import 'logger' in other modules 9 | logger = logging.getLogger(__name__) 10 | -------------------------------------------------------------------------------- /crewai_be/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from pydantic import BaseModel 3 | 4 | 5 | class NamedUrl(BaseModel): 6 | name: str 7 | url: str 8 | 9 | 10 | class PositionInfo(BaseModel): 11 | company: str 12 | position: str 13 | name: str 14 | blog_articles_urls: List[str] 15 | youtube_interviews_urls: List[NamedUrl] 16 | 17 | 18 | class PositionInfoList(BaseModel): 19 | positions: List[PositionInfo] 20 | -------------------------------------------------------------------------------- /nextjs_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /nextjs_app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /nextjs_app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs_app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /nextjs_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.6.8", 13 | "next": "14.1.3", 14 | "react": "^18", 15 | "react-dom": "^18", 16 | "react-hot-toast": "^2.4.1" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20", 20 | "@types/react": "^18", 21 | "@types/react-dom": "^18", 22 | "autoprefixer": "^10.0.1", 23 | "eslint": "^8", 24 | "eslint-config-next": "14.1.3", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.3.0", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nextjs_app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /nextjs_app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Header from "@/components/Header"; 5 | import { Toaster } from "react-hot-toast"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "NextJS CrewAI Tutorial", 11 | description: "Build a CrewAI app with Next.js and TypeScript", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 |
23 | {children} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /crewai_be/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nextjs-crewai-tutorial" 3 | version = "0.1.0" 4 | description = "Automate YouTube tasks with CrewAI" 5 | authors = ["bhancock_ai ", "João Moura "] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.10.0,<3.12" 9 | crewai = {extras = ["tools"], version = "^0.22.4"} 10 | pydantic = "^2.6.3" 11 | load-dotenv = "^0.1.0" 12 | crewai-tools = "^0.0.15" 13 | flask = "^3.0.2" 14 | flask-cors = "^4.0.0" 15 | 16 | [tool.pyright] 17 | # https://github.com/microsoft/pyright/blob/main/docs/configuration.md 18 | useLibraryCodeForTypes = true 19 | exclude = [".cache"] 20 | 21 | [tool.ruff] 22 | # https://beta.ruff.rs/docs/configuration/ 23 | select = ['E', 'W', 'F', 'I', 'B', 'C4', 'ARG', 'SIM'] 24 | ignore = ['W291', 'W292', 'W293'] 25 | 26 | [build-system] 27 | requires = ["poetry-core>=1.0.0"] 28 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /crewai_be/job_manager.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import List, Dict 4 | from threading import Lock 5 | from utils.logging import logger 6 | 7 | jobs_lock = Lock() 8 | jobs: Dict[str, "Job"] = {} 9 | 10 | 11 | @dataclass 12 | class Event: 13 | timestamp: datetime 14 | data: str 15 | 16 | 17 | @dataclass 18 | class Job: 19 | status: str 20 | events: List[Event] 21 | result: str 22 | 23 | 24 | def append_event(job_id: str, event_data: str): 25 | with jobs_lock: 26 | if job_id not in jobs: 27 | logger.info("Job %s started", job_id) 28 | jobs[job_id] = Job( 29 | status='STARTED', 30 | events=[], 31 | result='') 32 | else: 33 | logger.info("Appending event for job %s: %s", job_id, event_data) 34 | jobs[job_id].events.append( 35 | Event(timestamp=datetime.now(), data=event_data)) 36 | -------------------------------------------------------------------------------- /nextjs_app/components/EventLog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Event } from "../types"; 3 | import { EventType } from "@/hooks/useCrewJob"; 4 | 5 | // This component will receive props to update events. 6 | type EventLogProps = { 7 | events: EventType[]; 8 | }; 9 | 10 | export const EventLog: React.FC = ({ events }) => { 11 | return ( 12 |
13 |

Event Details

14 |
15 | {events.length === 0 ? ( 16 |

No events yet.

17 | ) : ( 18 | events.map((event, index) => ( 19 |
20 |

21 | {event.timestamp}: {event.data} 22 |

23 |
24 | )) 25 | )} 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /nextjs_app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs_app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /nextjs_app/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { EventLog } from "@/components/EventLog"; 4 | import { FinalOutput } from "@/components/FinalOutput"; 5 | import InputSection from "@/components/InputSection"; 6 | import { useCrewJob } from "@/hooks/useCrewJob"; 7 | 8 | export default function Home() { 9 | // Hooks 10 | const crewJob = useCrewJob(); 11 | 12 | return ( 13 |
14 |
15 |
16 | 22 | 28 |
29 |
30 |
31 |

Output

32 | 39 |
40 | 41 | 42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /crewai_be/tools/youtube_search_tools.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type 2 | from pydantic.v1 import BaseModel, Field 3 | import os 4 | import requests 5 | from crewai_tools import BaseTool 6 | 7 | 8 | class VideoSearchResult(BaseModel): 9 | title: str 10 | video_url: str 11 | 12 | 13 | class YoutubeVideoSearchToolInput(BaseModel): 14 | """Input for YoutubeVideoSearchTool.""" 15 | keyword: str = Field(..., description="The search keyword.") 16 | max_results: int = Field( 17 | 10, description="The maximum number of results to return.") 18 | 19 | 20 | class YoutubeVideoSearchTool(BaseTool): 21 | name: str = "Search YouTube Videos" 22 | description: str = "Searches YouTube videos based on a keyword and returns a list of video search results." 23 | args_schema: Type[BaseModel] = YoutubeVideoSearchToolInput 24 | 25 | def _run(self, keyword: str, max_results: int = 10) -> List[VideoSearchResult]: 26 | api_key = os.getenv("YOUTUBE_API_KEY") 27 | url = "https://www.googleapis.com/youtube/v3/search" 28 | params = { 29 | "part": "snippet", 30 | "q": keyword, 31 | "maxResults": max_results, 32 | "type": "video", 33 | "key": api_key 34 | } 35 | response = requests.get(url, params=params) 36 | response.raise_for_status() 37 | items = response.json().get("items", []) 38 | 39 | results = [] 40 | for item in items: 41 | title = item["snippet"]["title"] 42 | video_id = item["id"]["videoId"] 43 | video_url = f"https://www.youtube.com/watch?v={video_id}" 44 | results.append(VideoSearchResult( 45 | title=title, 46 | video_url=video_url, 47 | )) 48 | 49 | return results 50 | -------------------------------------------------------------------------------- /crewai_be/crew.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Callable 3 | from langchain_openai import ChatOpenAI 4 | from agents import CompanyResearchAgents 5 | from job_manager import append_event 6 | from tasks import CompanyResearchTasks 7 | from crewai import Crew 8 | 9 | 10 | class CompanyResearchCrew: 11 | def __init__(self, job_id: str): 12 | self.job_id = job_id 13 | self.crew = None 14 | self.llm = ChatOpenAI(model="gpt-4-turbo-preview") 15 | 16 | def setup_crew(self, companies: list[str], positions: list[str]): 17 | agents = CompanyResearchAgents() 18 | tasks = CompanyResearchTasks( 19 | job_id=self.job_id) 20 | 21 | research_manager = agents.research_manager( 22 | companies, positions) 23 | company_research_agent = agents.company_research_agent() 24 | 25 | company_research_tasks = [ 26 | tasks.company_research(company_research_agent, company, positions) 27 | for company in companies 28 | ] 29 | 30 | manage_research_task = tasks.manage_research( 31 | research_manager, companies, positions, company_research_tasks) 32 | 33 | self.crew = Crew( 34 | agents=[research_manager, company_research_agent], 35 | tasks=[*company_research_tasks, manage_research_task], 36 | verbose=2, 37 | ) 38 | 39 | def kickoff(self): 40 | if not self.crew: 41 | append_event(self.job_id, "Crew not set up") 42 | return "Crew not set up" 43 | 44 | append_event(self.job_id, "Task Started") 45 | try: 46 | results = self.crew.kickoff() 47 | append_event(self.job_id, "Task Complete") 48 | return results 49 | except Exception as e: 50 | append_event(self.job_id, f"An error occurred: {e}") 51 | return str(e) 52 | -------------------------------------------------------------------------------- /nextjs_app/components/InputSection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Dispatch, SetStateAction, useState } from "react"; 4 | 5 | type InputSectionProps = { 6 | title: string; 7 | placeholder: string; 8 | data: string[]; 9 | setData: Dispatch>; 10 | }; 11 | 12 | export default function InputSection({ 13 | title, 14 | placeholder, 15 | setData, 16 | data, 17 | }: InputSectionProps) { 18 | const [inputValue, setInputValue] = useState(""); 19 | 20 | const handleAddClick = () => { 21 | if (inputValue.trim() !== "") { 22 | setData((prevItems) => [...prevItems, inputValue]); 23 | setInputValue(""); 24 | } 25 | }; 26 | 27 | const handleRemoveItem = (index: number) => { 28 | setData(data.filter((_, i) => i !== index)); 29 | }; 30 | 31 | return ( 32 |
33 |

{title}

34 |
35 | setInputValue(e.target.value)} 39 | placeholder={placeholder} 40 | className="p-2 border border-gray-300 rounded mr-2 flex-grow" 41 | /> 42 | 48 |
49 |
    50 | {data.map((item, index) => ( 51 |
  • 55 | {item} 56 | 62 |
  • 63 | ))} 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /crewai_be/api.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | from datetime import datetime 3 | import json 4 | from threading import Thread 5 | from uuid import uuid4 6 | 7 | # Related third-party imports 8 | from flask import Flask, jsonify, request, abort 9 | from flask_cors import CORS 10 | from dotenv import load_dotenv 11 | 12 | # Local application/library specific imports 13 | from crew import CompanyResearchCrew 14 | from job_manager import append_event, jobs, jobs_lock, Event 15 | from utils.logging import logger 16 | 17 | 18 | load_dotenv() 19 | 20 | app = Flask(__name__) 21 | CORS(app, resources={r"/api/*": {"origins": "*"}}) 22 | 23 | 24 | def kickoff_crew(job_id, companies: list[str], positions: list[str]): 25 | logger.info(f"Crew for job {job_id} is starting") 26 | 27 | results = None 28 | try: 29 | company_research_crew = CompanyResearchCrew(job_id) 30 | company_research_crew.setup_crew( 31 | companies, positions) 32 | results = company_research_crew.kickoff() 33 | logger.info(f"Crew for job {job_id} is complete", results) 34 | 35 | except Exception as e: 36 | logger.error(f"Error in kickoff_crew for job {job_id}: {e}") 37 | append_event(job_id, f"An error occurred: {e}") 38 | with jobs_lock: 39 | jobs[job_id].status = 'ERROR' 40 | jobs[job_id].result = str(e) 41 | 42 | with jobs_lock: 43 | jobs[job_id].status = 'COMPLETE' 44 | jobs[job_id].result = results 45 | jobs[job_id].events.append( 46 | Event(timestamp=datetime.now(), data="Crew complete")) 47 | 48 | 49 | @app.route('/api/crew', methods=['POST']) 50 | def run_crew(): 51 | logger.info("Received request to run crew") 52 | # Validation 53 | data = request.json 54 | if not data or 'companies' not in data or 'positions' not in data: 55 | abort(400, description="Invalid input data provided.") 56 | 57 | job_id = str(uuid4()) 58 | companies = data['companies'] 59 | positions = data['positions'] 60 | 61 | thread = Thread(target=kickoff_crew, args=( 62 | job_id, companies, positions)) 63 | thread.start() 64 | 65 | return jsonify({"job_id": job_id}), 202 66 | 67 | 68 | @app.route('/api/crew/', methods=['GET']) 69 | def get_status(job_id): 70 | with jobs_lock: 71 | job = jobs.get(job_id) 72 | if job is None: 73 | abort(404, description="Job not found") 74 | 75 | # Parse the job.result string into a JSON object 76 | try: 77 | result_json = json.loads(job.result) 78 | except json.JSONDecodeError: 79 | # If parsing fails, set result_json to the original job.result string 80 | result_json = job.result 81 | 82 | return jsonify({ 83 | "job_id": job_id, 84 | "status": job.status, 85 | "result": result_json, 86 | "events": [{"timestamp": event.timestamp.isoformat(), "data": event.data} for event in job.events] 87 | }) 88 | 89 | 90 | if __name__ == '__main__': 91 | app.run(debug=True, port=3001) 92 | -------------------------------------------------------------------------------- /nextjs_app/components/FinalOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PositionInfo } from "@/hooks/useCrewJob"; 3 | 4 | interface FinalOutputProps { 5 | positionInfoList: PositionInfo[]; 6 | } 7 | 8 | export const FinalOutput: React.FC = ({ 9 | positionInfoList, 10 | }) => { 11 | const capitalizeFirstLetter = (string: string) => { 12 | return string.charAt(0).toUpperCase() + string.slice(1); 13 | }; 14 | 15 | return ( 16 |
17 |

Final Output

18 |
19 | {positionInfoList.length === 0 ? ( 20 |

No job result yet.

21 | ) : ( 22 | positionInfoList.map((position, index) => ( 23 |
24 |
25 |

26 | Company:{" "} 27 | {capitalizeFirstLetter(position.company)} 28 |

29 |

30 | Position:{" "} 31 | {capitalizeFirstLetter(position.position)} 32 |

33 |

34 | Name: {position.name} 35 |

36 |
37 | Blog Articles URLs: 38 |
    39 | {position.blog_articles_urls.length > 0 ? ( 40 | position.blog_articles_urls.map((url, urlIndex) => ( 41 |
  • 42 | 48 | {url} 49 | 50 |
  • 51 | )) 52 | ) : ( 53 |

    None

    54 | )} 55 |
56 |
57 |
58 | YouTube Interviews: 59 |
    60 | {position.youtube_interviews_urls.map( 61 | (video, videoIndex) => ( 62 |
  • 63 | 69 | {video.name} 70 | 71 |
  • 72 | ) 73 | )} 74 |
75 |
76 |
77 |
78 | )) 79 | )} 80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /crewai_be/tasks.py: -------------------------------------------------------------------------------- 1 | from crewai import Task, Agent 2 | from textwrap import dedent 3 | 4 | 5 | from job_manager import append_event 6 | from models import PositionInfo, PositionInfoList 7 | from utils.logging import logger 8 | 9 | 10 | class CompanyResearchTasks(): 11 | 12 | def __init__(self, job_id): 13 | self.job_id = job_id 14 | 15 | def append_event_callback(self, task_output): 16 | logger.info("Callback called: %s", task_output) 17 | append_event(self.job_id, task_output.exported_output) 18 | 19 | def manage_research(self, agent: Agent, companies: list[str], positions: list[str], tasks: list[Task]): 20 | return Task( 21 | description=dedent(f"""Based on the list of companies {companies} and the positions {positions}, 22 | use the results from the Company Research Agent to research each position in each company. 23 | to put together a json object containing the URLs for 3 blog articles, the URLs and title 24 | for 3 YouTube interviews for each position in each company. 25 | 26 | """), 27 | agent=agent, 28 | expected_output=dedent( 29 | """A json object containing the URLs for 3 blog articles and the URLs and 30 | titles for 3 YouTube interviews for each position in each company."""), 31 | callback=self.append_event_callback, 32 | context=tasks, 33 | output_json=PositionInfoList 34 | ) 35 | 36 | def company_research(self, agent: Agent, company: str, positions: list[str]): 37 | return Task( 38 | description=dedent(f"""Research the position {positions} for the {company} company. 39 | For each position, 40 | 41 | nd the URLs for 3 recent blog articles and the URLs and titles for 42 | 3 recent YouTube interviews for the person in each position. 43 | Return this collected information in a JSON object. 44 | 45 | Helpful Tips: 46 | - To find the blog articles names and URLs, perform searches on Google such like the following: 47 | - "{company} [POSITION HERE] blog articles" 48 | - To find the youtube interviews, perform searches on YouTube such as the following: 49 | - "{company} [POSITION HERE] interview" 50 | 51 | Important: 52 | - Once you've found the information, immediately stop searching for additional information. 53 | - Only return the requested information. NOTHING ELSE! 54 | - Do not generate fake information. Only return the information you find. Nothing else! 55 | - Do not stop researching until you find the requested information for each position in the company. 56 | """), 57 | agent=agent, 58 | expected_output="""A JSON object containing the researched information for each position in the company.""", 59 | callback=self.append_event_callback, 60 | output_json=PositionInfo, 61 | async_execution=True 62 | ) 63 | -------------------------------------------------------------------------------- /crewai_be/agents.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from crewai import Agent 3 | from langchain_openai import ChatOpenAI 4 | from crewai_tools import SerperDevTool 5 | from tools.youtube_search_tools import YoutubeVideoSearchTool 6 | 7 | 8 | class CompanyResearchAgents(): 9 | 10 | def __init__(self): 11 | self.searchInternetTool = SerperDevTool() 12 | self.youtubeSearchTool = YoutubeVideoSearchTool() 13 | self.llm = ChatOpenAI(model="gpt-4-turbo-preview") 14 | 15 | def research_manager(self, companies: List[str], positions: List[str]) -> Agent: 16 | return Agent( 17 | role="Company Research Manager", 18 | goal=f"""Generate a list of JSON objects containing the urls for 3 recent blog articles and 19 | the url and title for 3 recent YouTube interview, for each position in each company. 20 | 21 | Companies: {companies} 22 | Positions: {positions} 23 | 24 | Important: 25 | - The final list of JSON objects must include all companies and positions. Do not leave any out. 26 | - If you can't find information for a specific position, fill in the information with the word "MISSING". 27 | - Do not generate fake information. Only return the information you find. Nothing else! 28 | - Do not stop researching until you find the requested information for each position in each company. 29 | - All the companies and positions exist so keep researching until you find the information for each one. 30 | - Make sure you each researched position for each company contains 3 blog articles and 3 YouTube interviews. 31 | """, 32 | backstory="""As a Company Research Manager, you are responsible for aggregating all the researched information 33 | into a list.""", 34 | llm=self.llm, 35 | tools=[self.searchInternetTool, self.youtubeSearchTool], 36 | verbose=True, 37 | allow_delegation=True 38 | ) 39 | 40 | def company_research_agent(self) -> Agent: 41 | return Agent( 42 | role="Company Research Agent", 43 | goal="""Look up the specific positions for a given company and find urls for 3 recent blog articles and 44 | the url and title for 3 recent YouTube interview for each person in the specified positions. It is your job to return this collected 45 | information in a JSON object""", 46 | backstory="""As a Company Research Agent, you are responsible for looking up specific positions 47 | within a company and gathering relevant information. 48 | 49 | Important: 50 | - Once you've found the information, immediately stop searching for additional information. 51 | - Only return the requested information. NOTHING ELSE! 52 | - Make sure you find the persons name who holds the position. 53 | - Do not generate fake information. Only return the information you find. Nothing else! 54 | """, 55 | tools=[self.searchInternetTool, self.youtubeSearchTool], 56 | llm=self.llm, 57 | verbose=True 58 | ) 59 | -------------------------------------------------------------------------------- /nextjs_app/hooks/useCrewJob.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import { useEffect, useState } from "react"; 5 | import toast from "react-hot-toast"; 6 | 7 | export type EventType = { 8 | data: string; 9 | timestamp: string; 10 | }; 11 | 12 | export type NamedUrl = { 13 | name: string; 14 | url: string; 15 | }; 16 | 17 | export type PositionInfo = { 18 | company: string; 19 | position: string; 20 | name: string; 21 | blog_articles_urls: string[]; 22 | youtube_interviews_urls: NamedUrl[]; 23 | }; 24 | 25 | export const useCrewJob = () => { 26 | // State 27 | const [running, setRunning] = useState(false); 28 | const [companies, setCompanies] = useState([]); 29 | const [positions, setPositions] = useState([]); 30 | const [events, setEvents] = useState([]); 31 | const [positionInfoList, setPositionInfoList] = useState([]); 32 | const [currentJobId, setCurrentJobId] = useState(""); 33 | 34 | // useEffects 35 | useEffect(() => { 36 | let intervalId: number; 37 | console.log("currentJobId", currentJobId); 38 | 39 | const fetchJobStatus = async () => { 40 | try { 41 | console.log("calling fetchJobStatus"); 42 | const response = await axios.get<{ 43 | status: string; 44 | result: { positions: PositionInfo[] }; 45 | events: EventType[]; 46 | }>(`http://localhost:3001/api/crew/${currentJobId}`); 47 | const { status, events: fetchedEvents, result } = response.data; 48 | 49 | console.log("status update", response.data); 50 | 51 | setEvents(fetchedEvents); 52 | if (result) { 53 | console.log("setting job result", result); 54 | console.log("setting job positions", result.positions); 55 | setPositionInfoList(result.positions || []); 56 | } 57 | 58 | if (status === "COMPLETE" || status === "ERROR") { 59 | if (intervalId) { 60 | clearInterval(intervalId); 61 | } 62 | setRunning(false); 63 | toast.success(`Job ${status.toLowerCase()}.`); 64 | } 65 | } catch (error) { 66 | if (intervalId) { 67 | clearInterval(intervalId); 68 | } 69 | setRunning(false); 70 | toast.error("Failed to get job status."); 71 | console.error(error); 72 | } 73 | }; 74 | 75 | if (currentJobId !== "") { 76 | intervalId = setInterval(fetchJobStatus, 1000) as unknown as number; 77 | } 78 | 79 | return () => { 80 | if (intervalId) { 81 | clearInterval(intervalId); 82 | } 83 | }; 84 | }, [currentJobId]); 85 | 86 | const startJob = async () => { 87 | // Clear previous job data 88 | setEvents([]); 89 | setPositionInfoList([]); 90 | setRunning(true); 91 | 92 | try { 93 | const response = await axios.post<{ job_id: string }>( 94 | "http://localhost:3001/api/crew", 95 | { 96 | companies, 97 | positions, 98 | } 99 | ); 100 | 101 | toast.success("Job started"); 102 | 103 | console.log("jobId", response.data.job_id); 104 | setCurrentJobId(response.data.job_id); 105 | } catch (error) { 106 | toast.error("Failed to start job"); 107 | console.error(error); 108 | setCurrentJobId(""); 109 | } 110 | }; 111 | 112 | return { 113 | running, 114 | events, 115 | setEvents, 116 | positionInfoList, 117 | setPositionInfoList, 118 | currentJobId, 119 | setCurrentJobId, 120 | companies, 121 | setCompanies, 122 | positions, 123 | setPositions, 124 | startJob, 125 | }; 126 | }; 127 | --------------------------------------------------------------------------------