├── .env.sample ├── .gitignore ├── .gitmodules ├── README.md ├── app.py ├── backend └── agent.py ├── images ├── diagram.png ├── diagram.svg ├── logo_circle.png ├── meeting-prep-agent.gif ├── meeting-prep-agent.mp4 └── meeting_prep_workflow.png ├── notebooks ├── mcp-test.ipynb └── test-agent.ipynb ├── requirements.txt └── ui ├── assets └── tavily_logo.svg ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── App.tsx ├── index.css ├── main.tsx └── types.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env.sample: -------------------------------------------------------------------------------- 1 | TAVILY_API_KEY= 2 | OPENAI_API_KEY= 3 | GOOGLE_CALENDAR_CONFIG = "/mcp-use-case/google-calendar-mcp/build/index.js" 4 | GROQ_API_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | /ui/node_modules 176 | notebooks/mcp-use.ipynb 177 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "google-calendar-mcp"] 2 | path = google-calendar-mcp 3 | url = https://github.com/nspady/google-calendar-mcp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🗓️ Meeting Prep Agent 2 | 3 |
4 | Tavily Chatbot Demo 5 |
6 | 7 | Download a full demo video by [clicking here](images/meeting-prep-agent.mp4) 8 | 9 | ## 👋 Welcome to the Tavily Meeting Prep Agent! 10 | 11 | This repository demonstrates how to build a meeting preparation agent with real-time web access, leveraging Tavily's advanced search capabilities. This agent will connect to your Google Calendar via MCP, extract meeting information, and use Tavily search for profile research on the meeting attendees and general information on the companies you are meeting with. 12 | 13 | The project is designed for easy customization and extension, allowing you to: 14 | 15 | - Integrate proprietary or internal data sources 16 | - Modify the agent architecture or swap out LLMs 17 | - Add additional Meeting Coordination Platform (MCP) integrations 18 | 19 | --- 20 | 21 | ## 🚀 Features 22 | 23 | - 🌐 **Real-time Web Search:** Instantly fetches up-to-date information using Tavily's search API. 24 | - 🧠 **Agentic Reasoning:** Combines MCP and ReAct agent flows for smarter, context-aware responses. 25 | - 🔄 **Streaming Substeps:** See agentic reasoning and substeps streamed live for transparency. 26 | - 🔗 **Citations:** All web search results are cited for easy verification. 27 | - 🗓️ **Google Calendar Integration:** (via MCP) Access and analyze your meeting data. 28 | - ⚡ **Async FastAPI Backend:** High-performance, async-ready backend for fast responses. 29 | - 💻 **Modern React Frontend:** Interactive UI for dynamic user interactions. 30 | 31 | ## System Diagram 32 | ![LangGraph Backend Architecture](images/diagram.png) 33 | 34 | --- 35 | ## 📂 Repository Structure 36 | 37 | - **Backend** ([`backend/`](./backend)) 38 | - [`agent.py`](./backend/agent.py): Agentic flow (MCP + LangChain-Tavily ReAct agent) 39 | - **Frontend** ([`ui/`](./ui)): React-based UI for meeting insights 40 | - **Server** ([`app.py`](./app.py)): FastAPI server for API endpoints and streaming 41 | 42 | --- 43 | 44 | 45 | ## 🛠️ Local Setup 46 | 47 | **Python version:** 3.13.2 (local development) 48 | 49 | ### Google Calendar MCP Setup 50 | 51 | See [google-calendar-mcp](https://github.com/nspady/google-calendar-mcp) for full details. 52 | 53 | **Google Cloud Setup:** 54 | 1. Go to the Google Cloud Console and create/select a project. 55 | 2. Enable the Google Calendar API. 56 | 3. Create OAuth 2.0 credentials: 57 | - Go to Credentials 58 | - Click "Create Credentials" > "OAuth client ID" 59 | - Choose "User data" for the type of data that the app will be accessing 60 | - Add your app name and contact information 61 | - Select "Desktop app" as the application type 62 | 4. Add your email as a test user under the OAuth Consent screen. 63 | 5. Create a file `gcp-oauth.keys.json` in the root of `google-calendar-mcp` directory. 64 | 5. Download your credentials and paste them in `gcp-oauth.keys.json`. 65 | 66 | This file should look like: 67 | 68 | ```json 69 | { 70 | "installed": { 71 | "client_id": "", 72 | "project_id": "", 73 | "auth_uri": "", 74 | "token_uri": "", 75 | "auth_provider_x509_cert_url": "", 76 | "client_secret": "", 77 | "redirect_uris": ["http://localhost"] 78 | } 79 | } 80 | ``` 81 | 82 | **Install the MCP:** 83 | ```bash 84 | cd google-calendar-mcp 85 | npm install 86 | ``` 87 | 88 | **Set config path:** 89 | ```bash 90 | GOOGLE_CALENDAR_CONFIG=/mcp-use-case/google-calendar-mcp/build/index.js 91 | ``` 92 | Run the notebook [`mcp-test.ipynb`](./notebooks/mcp-test.ipynb) to check that your MCP setup is working before proceeding. 93 | 94 | 95 | ### Backend Setup 96 | 97 | 1. Create and activate a virtual environment: 98 | ```bash 99 | python3 -m venv venv 100 | source venv/bin/activate # On Windows: .\venv\Scripts\activate 101 | ``` 102 | 2. Install dependencies: 103 | ```bash 104 | python3 -m pip install -r requirements.txt 105 | ``` 106 | 3. Set environment variables: 107 | ```bash 108 | export TAVILY_API_KEY="your-tavily-api-key" 109 | export OPENAI_API_KEY="your-openai-api-key" 110 | export GROQ_API_KEY=<"your-groq-api-key> 111 | export GOOGLE_CALENDAR_CONFIG="/mcp-use-case/google-calendar-mcp/build/index.js" 112 | ``` 113 | 4. Run the backend server: 114 | ```bash 115 | python app.py 116 | ``` 117 | 118 | ### Frontend Setup 119 | 120 | 1. Navigate to the frontend directory: 121 | ```bash 122 | cd ui 123 | ``` 124 | 2. Install dependencies: 125 | ```bash 126 | npm install 127 | ``` 128 | 3. Start the development server: 129 | ```bash 130 | npm run dev 131 | ``` 132 | 133 | **.env file example:** 134 | ```env 135 | TAVILY_API_KEY=your-tavily-api-key 136 | OPENAI_API_KEY=your-openai-api-key 137 | GROQ_API_KEY=your-groq-api-key 138 | GOOGLE_CALENDAR_CONFIG=your-google-config 139 | ``` 140 | 141 | --- 142 | 143 | 144 | ## 📡 API Endpoints 145 | 146 | - `POST /api/analyze-meetings`: Handles streamed LangGraph execution 147 | 148 | --- 149 | 150 | ## 🤝 Contributing 151 | 152 | Feel free to submit issues and enhancement requests! 153 | 154 | --- 155 | 156 | ## 📞 Contact 157 | 158 | Questions, feedback, or want to build something custom? Reach out! 159 | 160 | - Email: [Dean Sacoransky](mailto:deansa@tavily.com) 161 | 162 | --- 163 | 164 |
165 | Tavily Logo 166 |

Powered by Tavily – The web API built for AI agents

167 |
168 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from fastapi import FastAPI, HTTPException 4 | from fastapi.middleware.cors import CORSMiddleware 5 | from fastapi.responses import StreamingResponse 6 | from pydantic import BaseModel 7 | 8 | from backend.agent import MeetingPlanner 9 | 10 | app = FastAPI() 11 | 12 | # Add CORS middleware 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=["http://localhost:5173"], # Vite's default port 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | 21 | 22 | class DateRequest(BaseModel): 23 | date: str 24 | 25 | 26 | @app.post("/api/analyze-meetings") 27 | async def analyze_meetings(request: DateRequest): 28 | try: 29 | # Create and initialize the meeting planner 30 | planner = MeetingPlanner() 31 | 32 | # Build the graph 33 | graph = planner.build_graph() 34 | 35 | async def event_generator(): 36 | # Run the graph with the given date and stream events 37 | async for event in graph.astream_events({"date": request.date}): 38 | kind = event["event"] 39 | tags = event.get("tags", []) 40 | 41 | if kind == "on_chat_model_stream": 42 | content = event["data"]["chunk"].content 43 | if "streaming" in tags: 44 | yield json.dumps( 45 | {"type": "streaming", "content": content} 46 | ) + "\n" 47 | print(content) 48 | 49 | elif kind == "on_custom_event": 50 | event_name = event["name"] 51 | if event_name in [ 52 | "calendar_status", 53 | "calendar_parser_status", 54 | "react_status", 55 | "markdown_formatter_status", 56 | "company_event", 57 | ]: 58 | yield json.dumps( 59 | {"type": event_name, "content": event["data"]} 60 | ) + "\n" 61 | # if event_name == "company_event": 62 | # print(f"Company Event Data: {event['data']}") 63 | 64 | return StreamingResponse(event_generator(), media_type="application/json") 65 | 66 | except Exception as e: 67 | raise HTTPException(status_code=500, detail=str(e)) 68 | 69 | 70 | if __name__ == "__main__": 71 | import uvicorn 72 | 73 | uvicorn.run(app, host="0.0.0.0", port=5000) 74 | -------------------------------------------------------------------------------- /backend/agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from typing import Dict, List 5 | from typing import Optional as OptionalType 6 | 7 | from dotenv import load_dotenv 8 | from langchain import hub 9 | from langchain.agents import AgentExecutor, create_react_agent 10 | from langchain_core.callbacks.manager import dispatch_custom_event 11 | from langchain_groq import ChatGroq 12 | from langchain_openai import ChatOpenAI 13 | from langchain_tavily import TavilySearch 14 | from langgraph.graph import END, START, StateGraph 15 | from mcp_use import MCPAgent, MCPClient 16 | from pydantic import BaseModel, Field 17 | from tavily import TavilyClient 18 | from typing_extensions import TypedDict 19 | 20 | load_dotenv() 21 | 22 | # Set up logging 23 | logging.basicConfig(level=logging.INFO) 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | # Pydantic models for structured output 28 | class Attendee(BaseModel): 29 | email: str 30 | name: OptionalType[str] = None 31 | status: OptionalType[str] = None 32 | info: OptionalType[str] = None 33 | 34 | 35 | class Meeting(BaseModel): 36 | title: str 37 | company: str # This will be the client company name 38 | attendees: List[Attendee] = Field(default_factory=list) 39 | meeting_time: str 40 | 41 | 42 | class CalendarData(BaseModel): 43 | meetings: List[Meeting] = Field(default_factory=list) 44 | 45 | 46 | class State(TypedDict): 47 | date: str 48 | calendar_data: str 49 | calendar_events: List[Dict] 50 | react_results: List[str] 51 | markdown_results: str 52 | 53 | 54 | class MeetingPlanner: 55 | def __init__( 56 | self, 57 | ): 58 | # Initialize 59 | self.stream_insights_llm = ChatOpenAI(model="gpt-4.1").with_config( 60 | {"tags": ["streaming"]} 61 | ) 62 | self.react_llm = ChatOpenAI(model="o3-mini-2025-01-31") 63 | self.fast_llm = ChatGroq( 64 | api_key=os.getenv("GROQ_API_KEY"), model="llama-3.3-70b-versatile" 65 | ) 66 | self.react_prompt = hub.pull("hwchase17/react") 67 | self.tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) 68 | self.react_tools = [ 69 | TavilySearch( 70 | max_results=3, include_raw_content=True, search_depth="advanced" 71 | ) 72 | ] 73 | self.react_agent = create_react_agent( 74 | self.react_llm, self.react_tools, self.react_prompt 75 | ) 76 | self.react_agent_executor = AgentExecutor( 77 | agent=self.react_agent, tools=self.react_tools, handle_parsing_errors=True 78 | ) 79 | 80 | async def calendar_node(self, state: State): 81 | """Fetch calendar events for a specified date""" 82 | dispatch_custom_event("calendar_status", "Connecting to Google Calendar MCP...") 83 | google_calendar_config = { 84 | "mcpServers": { 85 | "google-calendar": { 86 | "command": "node", 87 | "args": [os.getenv("GOOGLE_CALENDAR_CONFIG")], 88 | } 89 | } 90 | } 91 | 92 | # Create MCPClient from configuration dictionary 93 | client = MCPClient.from_dict(google_calendar_config) 94 | 95 | # Create agent with the client 96 | agent = MCPAgent(llm=self.fast_llm, client=client, max_steps=30) 97 | 98 | date = state["date"] 99 | # Run the query 100 | calendar_data = await agent.run( 101 | f"What is on my calendar for {date}. Include the meeting title, time, and the attendees - names and emails." 102 | ) 103 | 104 | return { 105 | "calendar_data": calendar_data, 106 | } 107 | 108 | def calendar_parser_node(self, state: State): 109 | """Parse the calendar data into a structured format using structured output from LLM""" 110 | dispatch_custom_event("calendar_parser_status", "Analyzing Your Calendar...") 111 | 112 | calendar_data = state["calendar_data"] 113 | 114 | # Create a structured parser using ChatOpenAI with structured output 115 | parser_llm = ChatOpenAI( 116 | temperature=0, model="gpt-4.1-nano" 117 | ).with_structured_output(CalendarData) 118 | # Define the prompt for extraction 119 | extraction_prompt = """ 120 | Extract meeting information from the following calendar data: 121 | 122 | {calendar_data} 123 | 124 | Important context: 125 | - You work for Tavily, so "Tavily" is your company, not the client company 126 | - For each meeting, identify the client company name (the company Tavily is meeting with) 127 | - Only include attendees from the client company (exclude anyone with @tavily.com email) 128 | 129 | For each meeting, extract: 130 | 1. The meeting title 131 | 2. The client company name (the external company Tavily is meeting with) 132 | 3. All client attendees with their emails and names (exclude Tavily employees) 133 | 4. The meeting time in the format [Hour:Minute AM/PM] 134 | 5. Any additional information about the meeting attendees 135 | Return the information in a structured json format. 136 | """ 137 | 138 | # Parse the calendar data 139 | structured_data = parser_llm.invoke( 140 | extraction_prompt.format(calendar_data=calendar_data) 141 | ) 142 | 143 | # Process into the format needed by the rest of the application 144 | calendar_events = [] 145 | 146 | for meeting in structured_data.meetings: 147 | # Extract company name from meeting title if needed 148 | company = meeting.company 149 | # Create event data 150 | event_data = { 151 | "company": company, 152 | "title": meeting.title, 153 | "attendees": {}, 154 | "meeting_time": meeting.meeting_time, 155 | } 156 | 157 | # Process attendees (only client attendees) 158 | for attendee in meeting.attendees: 159 | email = attendee.email 160 | name = ( 161 | attendee.name if attendee.name else email.split("@")[0] 162 | ) # Use part of email as name if not available 163 | 164 | # Skip Tavily employees 165 | if "tavily.com" in email.lower(): 166 | continue 167 | 168 | # Add to event attendees 169 | event_data["attendees"][email] = name 170 | dispatch_custom_event( 171 | "company_event", f"{company} @ {meeting.meeting_time}" 172 | ) 173 | calendar_events.append(event_data) 174 | return {"calendar_events": calendar_events} 175 | 176 | def react_node(self, state: State): 177 | """Use react architecture to search for information about the attendees""" 178 | 179 | calendar_events = state["calendar_events"] 180 | dispatch_custom_event( 181 | "react_status", "Searching Tavily for Meeting Insights..." 182 | ) 183 | # Create a function to process a single event 184 | formatted_prompt = f""" 185 | Your goal is to help me prepare for an upcoming meeting. 186 | You will be provided with the name of a company we are meeting with and a list of attendees. 187 | 188 | meeting information: 189 | {calendar_events} 190 | 191 | Please find the profile information (e.g. linkedin profile) of the attendees using tavily search. 192 | 193 | 1. Search for the attendees name using all available information such as their email, initials/last name, etc. 194 | - provide details on the attendees experience, education, and skills, and location 195 | - If there are multiple attendees with the same name, only focus on the one that works at the relevant company 196 | - it is important you find the profile of all the attendees! 197 | 2. Research the company in the context of AI initiatives using tavily search. 198 | 3. Provide your findings summarized concisely with the relevant links. Do not include anything else in the output. 199 | """ 200 | 201 | result = self.react_agent_executor.invoke({"input": formatted_prompt}) 202 | 203 | return {"react_results": result["output"]} 204 | 205 | def markdown_formatter_node(self, state: State): 206 | """Format the react results into a markdown string""" 207 | dispatch_custom_event( 208 | "markdown_formatter_status", "Formatting Meeting Insights..." 209 | ) 210 | research_results = state["react_results"] 211 | calendar_events = state["calendar_events"] 212 | 213 | # Create a formatting prompt for the LLM 214 | formatting_prompt = """ 215 | You are a meeting preparation assistant. You are given a list of calendar events and research results. 216 | Your job is to prepare your colleagues for a day of meetings. 217 | You must optimize for clarity and conciseness. Do not include any information that is not relevant to the meeting preparation. 218 | 219 | Create a well-structured markdown document from the following meeting research results. 220 | 221 | For each company, create a section with: 222 | 1. ## Company name @ Time of meeting 223 | 2. ### Meeting context (only if available) 224 | - relevant background information about the company (only if available) 225 | - relevant background information about the meeting (only if available) 226 | 3. ### Attendee subsections with their roles, background, and relevant information 227 | 4. Use proper markdown formatting including bold, italics, and bullet points where appropriate 228 | 5. Please include inline citations as Markdown hyperlinks directly in the response text. 229 | 230 | Calendar Events: {calendar_events} 231 | Research Results: {research_results} 232 | 233 | Format the output as clean, well-structured markdown with clear sections and subsections. 234 | """ 235 | print("research results: ", research_results) 236 | 237 | # Use the LLM to format the results 238 | formatted_results = self.stream_insights_llm.invoke( 239 | formatting_prompt.format( 240 | calendar_events=json.dumps(calendar_events, indent=2), 241 | research_results=research_results, 242 | ) 243 | ) 244 | return {"markdown_results": formatted_results.content} 245 | 246 | def build_graph(self): 247 | """Build and compile the graph""" 248 | graph_builder = StateGraph(State) 249 | 250 | graph_builder.add_node("Google Calendar MCP", self.calendar_node) 251 | graph_builder.add_node("Calendar Data Parser", self.calendar_parser_node) 252 | graph_builder.add_node("ReAct", self.react_node) 253 | graph_builder.add_node("Markdown Formatter", self.markdown_formatter_node) 254 | 255 | graph_builder.add_edge(START, "Google Calendar MCP") 256 | graph_builder.add_edge("Google Calendar MCP", "Calendar Data Parser") 257 | graph_builder.add_edge("Calendar Data Parser", "ReAct") 258 | graph_builder.add_edge("ReAct", "Markdown Formatter") 259 | graph_builder.add_edge("Markdown Formatter", END) 260 | 261 | compiled_graph = graph_builder.compile() 262 | 263 | return compiled_graph 264 | -------------------------------------------------------------------------------- /images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tavily-ai/meeting-prep-agent/aebc7e1711442f654164b985966322863bf80bf5/images/diagram.png -------------------------------------------------------------------------------- /images/diagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/logo_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tavily-ai/meeting-prep-agent/aebc7e1711442f654164b985966322863bf80bf5/images/logo_circle.png -------------------------------------------------------------------------------- /images/meeting-prep-agent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tavily-ai/meeting-prep-agent/aebc7e1711442f654164b985966322863bf80bf5/images/meeting-prep-agent.gif -------------------------------------------------------------------------------- /images/meeting-prep-agent.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tavily-ai/meeting-prep-agent/aebc7e1711442f654164b985966322863bf80bf5/images/meeting-prep-agent.mp4 -------------------------------------------------------------------------------- /images/meeting_prep_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tavily-ai/meeting-prep-agent/aebc7e1711442f654164b985966322863bf80bf5/images/meeting_prep_workflow.png -------------------------------------------------------------------------------- /notebooks/mcp-test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "#### Env Var Setup\n", 8 | "Before starting the MCP experimentation, make sure your `GOOGLE_CALENDAR_CONFIG` variable is properly set up! Please follow the readme instructions or email deansa@tavily.com if you get stuck" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "import os\n", 18 | "from dotenv import load_dotenv\n", 19 | "from langchain_openai import ChatOpenAI\n", 20 | "from mcp_use import MCPAgent, MCPClient\n", 21 | "load_dotenv()\n" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "google_calendar_config = {\n", 31 | " \"mcpServers\": {\n", 32 | " \"google-calendar\": {\n", 33 | " \"command\": \"node\",\n", 34 | " \"args\": [os.getenv(\"GOOGLE_CALENDAR_CONFIG\")]\n", 35 | " }\n", 36 | " }\n", 37 | "}" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# Create MCPClient from configuration dictionary\n", 47 | "client = MCPClient.from_dict(google_calendar_config)\n", 48 | "\n", 49 | "# Create LLM\n", 50 | "llm = ChatOpenAI(model=\"gpt-4o\")\n", 51 | "\n", 52 | "# Create agent with the client\n", 53 | "agent = MCPAgent(llm=llm, client=client, max_steps=30)\n", 54 | "\n", 55 | "# Run the query\n", 56 | "result = await agent.run(\n", 57 | " \"What events do I have on May 1 2025?\",\n", 58 | ")\n", 59 | "print(f\"\\nResult: {result}\")" 60 | ] 61 | } 62 | ], 63 | "metadata": { 64 | "kernelspec": { 65 | "display_name": "venv", 66 | "language": "python", 67 | "name": "python3" 68 | }, 69 | "language_info": { 70 | "codemirror_mode": { 71 | "name": "ipython", 72 | "version": 3 73 | }, 74 | "file_extension": ".py", 75 | "mimetype": "text/x-python", 76 | "name": "python", 77 | "nbconvert_exporter": "python", 78 | "pygments_lexer": "ipython3", 79 | "version": "3.13.2" 80 | } 81 | }, 82 | "nbformat": 4, 83 | "nbformat_minor": 2 84 | } 85 | -------------------------------------------------------------------------------- /notebooks/test-agent.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "import sys\n", 11 | "\n", 12 | "# Get the absolute path to the project root directory\n", 13 | "project_root = os.path.abspath(os.path.join(os.path.dirname(\"__file__\"), \"..\"))\n", 14 | "sys.path.insert(0, project_root)\n", 15 | "\n", 16 | "# Now import using absolute import\n", 17 | "from meeting_prep.agent import MeetingPlanner" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Create and initialize the meeting planner\n", 27 | "planner = MeetingPlanner()\n", 28 | "\n", 29 | "# Build the graph\n", 30 | "graph = planner.build_graph()\n", 31 | "\n", 32 | "# Run the graph with the given date\n", 33 | "result = await graph.ainvoke(\n", 34 | " {\"date\": \"April 30 2025\"}\n", 35 | ")" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "# Get just the research outputs\n", 45 | "react_results = result['react_results']\n", 46 | "for research in react_results:\n", 47 | " print(research['output'])\n", 48 | " print(\"\\n---\\n\")" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "kernelspec": { 54 | "display_name": "venv", 55 | "language": "python", 56 | "name": "python3" 57 | }, 58 | "language_info": { 59 | "codemirror_mode": { 60 | "name": "ipython", 61 | "version": 3 62 | }, 63 | "file_extension": ".py", 64 | "mimetype": "text/x-python", 65 | "name": "python", 66 | "nbconvert_exporter": "python", 67 | "pygments_lexer": "ipython3", 68 | "version": "3.13.2" 69 | } 70 | }, 71 | "nbformat": 4, 72 | "nbformat_minor": 2 73 | } 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.6.1 2 | aiohttp==3.11.18 3 | aiosignal==1.3.2 4 | annotated-types==0.7.0 5 | anyio==4.9.0 6 | attrs==25.3.0 7 | certifi==2025.1.31 8 | charset-normalizer==3.4.1 9 | dataclasses-json==0.6.7 10 | fastapi==0.110.0 11 | httpcore==1.0.8 12 | httpx==0.28.1 13 | httpx-sse==0.4.0 14 | idna==3.10 15 | jsonpatch==1.33 16 | jsonpointer==3.0.0 17 | jsonschema_pydantic==0.6 18 | langchain==0.3.23 19 | langchain-community==0.3.21 20 | langchain-core==0.3.55 21 | langchain-mcp-adapters==0.0.9 22 | langchain-openai==0.3.14 23 | langchain-tavily==0.1.6 24 | langchain-text-splitters==0.3.8 25 | langgraph==0.3.31 26 | langgraph-checkpoint==2.0.24 27 | langgraph-prebuilt==0.1.8 28 | langgraph-sdk==0.1.63 29 | langsmith==0.3.33 30 | mcp==1.6.0 31 | mcp-use==1.2.7 32 | openai==1.75.0 33 | pydantic==2.11.3 34 | pydantic-settings==2.9.1 35 | python-dotenv==1.1.0 36 | requests==2.32.3 37 | tavily-python==0.5.4 38 | typing_extensions==4.13.2 39 | uvicorn==0.27.1 40 | langchain-groq==0.3.2 -------------------------------------------------------------------------------- /ui/assets/tavily_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Meeting Prep Assistant 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meeting-prep-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@types/node": "^20.11.24", 8 | "@types/react": "^18.2.61", 9 | "@types/react-datepicker": "^4.19.6", 10 | "@types/react-dom": "^18.2.19", 11 | "axios": "^1.6.7", 12 | "react": "^18.2.0", 13 | "react-datepicker": "^6.1.0", 14 | "react-dom": "^18.2.0", 15 | "react-icons": "^5.5.0", 16 | "react-markdown": "^9.0.1", 17 | "typescript": "^5.3.3" 18 | }, 19 | "devDependencies": { 20 | "@tailwindcss/typography": "^0.5.10", 21 | "@vitejs/plugin-react": "^4.2.1", 22 | "autoprefixer": "^10.4.17", 23 | "postcss": "^8.4.35", 24 | "tailwindcss": "^3.4.1", 25 | "vite": "^5.1.4" 26 | }, 27 | "scripts": { 28 | "start": "vite", 29 | "build": "tsc && vite build", 30 | "serve": "vite preview" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import DatePicker from 'react-datepicker'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import "react-datepicker/dist/react-datepicker.css"; 5 | import tavilyLogo from '../assets/tavily_logo.svg'; 6 | import { FaCalendarAlt, FaClock, FaCogs } from 'react-icons/fa'; 7 | 8 | type StatusUpdate = { 9 | type: string; 10 | content: string; 11 | }; 12 | 13 | enum MessageTypes { 14 | CALENDAR_STATUS = "calendar_status", 15 | CALENDAR_PARSER_STATUS = "calendar_parser_status", 16 | REACT_STATUS = "react_status", 17 | MARKDOWN_FORMATTER_STATUS = "markdown_formatter_status", 18 | COMPANY_EVENT = "company_event", 19 | STREAMING = "streaming" 20 | } 21 | 22 | function App() { 23 | const [selectedDate, setSelectedDate] = useState(null); 24 | const [loading, setLoading] = useState(false); 25 | const [error, setError] = useState(''); 26 | const [statusType, setStatusType] = useState(''); 27 | const [companyEvents, setCompanyEvents] = useState([]); 28 | const [streamContent, setStreamContent] = useState(''); 29 | 30 | // Clear status when streaming starts, but keep company events visible 31 | useEffect(() => { 32 | if (streamContent.length > 0) { 33 | // Only clear non-company event statuses 34 | if (statusType !== MessageTypes.COMPANY_EVENT) { 35 | setStatusType(''); 36 | } 37 | setLoading(false); 38 | } 39 | }, [streamContent, statusType]); 40 | 41 | // Reset states when selecting a new date 42 | const handleDateSelect = async (date: Date | null) => { 43 | if (!date) return; 44 | 45 | setSelectedDate(date); 46 | setLoading(true); 47 | setError(''); 48 | setStreamContent(''); 49 | setStatusType(''); 50 | setCompanyEvents([]); 51 | 52 | try { 53 | const formattedDate = date.toLocaleDateString('en-US', { 54 | year: 'numeric', 55 | month: 'long', 56 | day: 'numeric' 57 | }); 58 | 59 | const response = await fetch('/api/analyze-meetings', { 60 | method: 'POST', 61 | headers: { 62 | 'Content-Type': 'application/json', 63 | }, 64 | body: JSON.stringify({ date: formattedDate }), 65 | }); 66 | 67 | const reader = response.body?.getReader(); 68 | if (!reader) throw new Error('No reader available'); 69 | 70 | const decoder = new TextDecoder(); 71 | let buffer = ""; 72 | 73 | while (true) { 74 | const { done, value } = await reader.read(); 75 | if (done) break; 76 | 77 | const decodedChunk = decoder.decode(value, { stream: true }); 78 | buffer += decodedChunk; 79 | 80 | const lines = buffer.split('\n'); 81 | buffer = lines.pop() || ''; 82 | 83 | for (const line of lines) { 84 | if (!line.trim()) continue; 85 | 86 | try { 87 | const event: StatusUpdate = JSON.parse(line); 88 | 89 | switch (event.type) { 90 | case MessageTypes.CALENDAR_STATUS: 91 | case MessageTypes.CALENDAR_PARSER_STATUS: 92 | case MessageTypes.REACT_STATUS: 93 | setStatusType(event.type); 94 | break; 95 | case MessageTypes.COMPANY_EVENT: 96 | console.log("Company Event received from backend:", event); 97 | console.log("Content:", event.content); 98 | 99 | // Add to the array of company events instead of replacing 100 | setCompanyEvents(prev => [...prev, event.content]); 101 | break; 102 | case MessageTypes.STREAMING: 103 | setStreamContent(prev => prev + event.content); 104 | break; 105 | default: 106 | console.log('Unknown event type:', event.type); 107 | } 108 | } catch (e) { 109 | console.error('Error parsing event:', e); 110 | } 111 | } 112 | } 113 | 114 | } catch (error) { 115 | console.error('Error fetching meeting analysis:', error); 116 | setError('Error analyzing meetings. Please try again.'); 117 | setLoading(false); 118 | } 119 | }; 120 | 121 | const renderStatusIcon = () => { 122 | switch (statusType) { 123 | case MessageTypes.CALENDAR_STATUS: 124 | return ; 125 | case MessageTypes.CALENDAR_PARSER_STATUS: 126 | return ; 127 | case MessageTypes.REACT_STATUS: 128 | return Tavily Logo; 129 | case MessageTypes.COMPANY_EVENT: 130 | return ; 131 | default: 132 | return null; 133 | } 134 | }; 135 | 136 | const getStatusLabel = () => { 137 | switch (statusType) { 138 | case MessageTypes.CALENDAR_STATUS: 139 | return "Accessing calendar"; 140 | case MessageTypes.CALENDAR_PARSER_STATUS: 141 | return "Processing data"; 142 | case MessageTypes.REACT_STATUS: 143 | return "Researching"; 144 | default: 145 | return ""; 146 | } 147 | }; 148 | 149 | return ( 150 |
151 | {/* Header with Tavily logo */} 152 |
153 |
154 |
155 | Powered by 156 | Tavily Logo 157 |
158 |
159 |
160 | 161 | {/* Main content */} 162 |
163 |
164 |
165 |
166 |

Your Meetings, Fully Prepared

167 | 168 |
169 | 176 |
177 | 178 | {/* Company events section - display all events */} 179 | {companyEvents.length > 0 && ( 180 |
181 | {companyEvents.map((event, index) => ( 182 |
183 |
184 |
{event}
185 |
186 | ))} 187 |
188 | )} 189 | 190 | {/* Loading and status indicators (except company events) */} 191 | {(loading || (statusType && statusType !== MessageTypes.COMPANY_EVENT)) && ( 192 |
193 | {loading && ( 194 |
195 |
201 |
208 |
215 |
216 | )} 217 | {statusType && statusType !== MessageTypes.COMPANY_EVENT && ( 218 |
219 |
{renderStatusIcon()}
220 |
{getStatusLabel()}
221 |
222 | )} 223 |
224 | )} 225 | 226 | {error && ( 227 |
228 | {error} 229 |
230 | )} 231 | 232 | {streamContent && ( 233 |
234 |
235 |

, 238 | h3: ({node, ...props}) =>

, 239 | h4: ({node, ...props}) =>

, 240 | p: ({node, ...props}) =>

, 241 | ul: ({node, ...props}) =>

    , 242 | li: ({node, ...props}) =>
  • 243 | }} 244 | > 245 | {streamContent} 246 | 247 |

248 |
249 | )} 250 |
251 |
252 |
253 |
254 |
255 | ); 256 | } 257 | 258 | export default App; -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /ui/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('@tailwindcss/typography'), 12 | ], 13 | } -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:5000', 10 | changeOrigin: true, 11 | }, 12 | }, 13 | }, 14 | }); --------------------------------------------------------------------------------