forked from crewAIInc/crewAI-examples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
João Moura
committed
Jan 23, 2024
1 parent
78e8433
commit d4c272f
Showing
15 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
OPENAI_API_KEY=... | ||
TAVILY_API_KEY=... (tavily.com) | ||
MY_EMAIL=... (your email) | ||
|
||
# Optional | ||
LANGCHAIN_TRACING_V2=... | ||
LANGCHAIN_ENDPOINT=... | ||
LANGCHAIN_API_KEY=... | ||
LANGCHAIN_PROJECT=... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
__pycache__ | ||
.env | ||
credentials.json | ||
token.json |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# CrewAI + LangGraph | ||
|
||
## Introduction | ||
This is an example of how to use the [CrewAI](https://github.com/joaomdmoura/crewai) with LangChain and LangGraph to automate the process of automatically checking emails and creating drafts. CrewAI orchestrates autonomous AI agents, enabling them to collaborate and execute complex tasks efficiently. | ||
|
||
 | ||
|
||
|
||
By [@joaomdmoura](https://x.com/joaomdmoura) | ||
|
||
- [CrewAI Framework](#crewai-framework) | ||
- [Running the code](#running-the-code) | ||
- [Details & Explanation](#details--explanation) | ||
- [Using Local Models with Ollama](#using-local-models-with-ollama) | ||
- [License](#license) | ||
|
||
## CrewAI Framework | ||
CrewAI is designed to facilitate the collaboration of role-playing AI agents. In this example, these agents work together to give a complete stock analysis and investment recommendation | ||
|
||
## Running the Code | ||
This example uses GPT-4. | ||
|
||
- **Configure Environment**: Copy ``.env.example` and set up the environment variable | ||
- **Setup a credentials.json**: Follow the [google instructions](https://developers.google.com/gmail/api/quickstart/python#authorize_credentials_for_a_desktop_application), once you’ve downloaded the file, name it `credentials.json` and add to the root of the project, | ||
- **Install Dependencies**: Run `pip install -r requirements.txt` | ||
- **Execute the Script**: Run `python main.py` | ||
|
||
## Details & Explanation | ||
- **Running the Script**: Execute `python main.py` | ||
- **Key Components**: | ||
- `./src/graph.py`: Class defining the nodes and edges. | ||
- `./src/nodes.py`: Class with the function for each node. | ||
- `./src/state.py`: State declaration. | ||
- `./src/crew/agents.py`: Class defining the CrewAI Agents. | ||
- `./src/crew/taks.py`: Class definig the CrewAI Tasks. | ||
- `./src/crew/crew.py`: Class defining the CrewAI Crew. | ||
- `./src/crew/tools.py`: Class implementing the GmailDraft Tool. | ||
|
||
## License | ||
This project is released under the MIT License. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from src.graph import WorkFlow | ||
|
||
app = WorkFlow().app | ||
app.invoke({}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
crewai==0.1.32 | ||
langgraph==0.0.15 | ||
langchain-community==0.0.14 | ||
python-dotenv==1.0.0 | ||
google-search-results==2.1.0 | ||
google-api-python-client==2.114.0 | ||
google-auth-oauthlib==1.2.0 | ||
google-auth-httplib2==0.2.0 | ||
beautifulsoup4==4.12.3 | ||
tavily-python==0.3.1 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
from langchain_community.agent_toolkits import GmailToolkit | ||
from langchain_community.tools.gmail.get_thread import GmailGetThread | ||
from langchain_community.tools.tavily_search import TavilySearchResults | ||
|
||
from textwrap import dedent | ||
from crewai import Agent | ||
from .tools import CreateDraftTool | ||
|
||
class EmailFilterAgents(): | ||
def __init__(self): | ||
self.gmail = GmailToolkit() | ||
|
||
def email_filter_agent(self): | ||
return Agent( | ||
role='Senior Email Analyst', | ||
goal='Filter out non-essential emails like newsletters and promotional content', | ||
backstory=dedent("""\ | ||
As a Senior Email Analyst, you have extensive experience in email content analysis. | ||
You are adept at distinguishing important emails from spam, newsletters, and other | ||
irrelevant content. Your expertise lies in identifying key patterns and markers that | ||
signify the importance of an email."""), | ||
verbose=True, | ||
allow_delegation=False | ||
) | ||
|
||
def email_action_agent(self): | ||
|
||
return Agent( | ||
role='Email Action Specialist', | ||
goal='Identify action-required emails and compile a list of their IDs', | ||
backstory=dedent("""\ | ||
With a keen eye for detail and a knack for understanding context, you specialize | ||
in identifying emails that require immediate action. Your skill set includes interpreting | ||
the urgency and importance of an email based on its content and context."""), | ||
tools=[ | ||
GmailGetThread(api_resource=self.gmail.api_resource), | ||
TavilySearchResults() | ||
], | ||
verbose=True, | ||
allow_delegation=False, | ||
) | ||
|
||
def email_response_writer(self): | ||
return Agent( | ||
role='Email Response Writer', | ||
goal='Draft responses to action-required emails', | ||
backstory=dedent("""\ | ||
You are a skilled writer, adept at crafting clear, concise, and effective email responses. | ||
Your strength lies in your ability to communicate effectively, ensuring that each response is | ||
tailored to address the specific needs and context of the email."""), | ||
tools=[ | ||
TavilySearchResults(), | ||
GmailGetThread(api_resource=self.gmail.api_resource), | ||
CreateDraftTool.create_draft | ||
], | ||
verbose=True, | ||
allow_delegation=False, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from crewai import Crew | ||
|
||
from .agents import EmailFilterAgents | ||
from .tasks import EmailFilterTasks | ||
|
||
class EmailFilterCrew(): | ||
def __init__(self): | ||
agents = EmailFilterAgents() | ||
self.filter_agent = agents.email_filter_agent() | ||
self.action_agent = agents.email_action_agent() | ||
self.writer_agent = agents.email_response_writer() | ||
|
||
def kickoff(self, state): | ||
print("### Filtering emails") | ||
tasks = EmailFilterTasks() | ||
crew = Crew( | ||
agents=[self.filter_agent, self.action_agent, self.writer_agent], | ||
tasks=[ | ||
tasks.filter_emails_task(self.filter_agent, self._format_emails(state['emails'])), | ||
tasks.action_required_emails_task(self.action_agent), | ||
tasks.draft_responses_task(self.writer_agent) | ||
], | ||
verbose=True | ||
) | ||
result = crew.kickoff() | ||
return {**state, "action_required_emails": result} | ||
|
||
def _format_emails(self, emails): | ||
emails_string = [] | ||
for email in emails: | ||
print(email) | ||
arr = [ | ||
f"ID: {email['id']}", | ||
f"- Thread ID: {email['threadId']}", | ||
f"- Snippet: {email['snippet']}", | ||
f"- From: {email['sender']}", | ||
f"--------" | ||
] | ||
emails_string.append("\n".join(arr)) | ||
return "\n".join(emails_string) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
from crewai import Task | ||
from textwrap import dedent | ||
|
||
class EmailFilterTasks: | ||
def filter_emails_task(self, agent, emails): | ||
return Task( | ||
description=dedent(f"""\ | ||
Analyze a batch of emails and filter out | ||
non-essential ones such as newsletters, promotional content and notifications. | ||
Use your expertise in email content analysis to distinguish | ||
important emails from the rest, pay attention to the sender and avoind invalid emails. | ||
Make sure to filter for the messages actually directed at the user and avoid notifications. | ||
EMAILS | ||
------- | ||
{emails} | ||
Your final answer MUST be a the relevant thread_ids and the sender, use bullet points. | ||
"""), | ||
agent=agent | ||
) | ||
|
||
def action_required_emails_task(self, agent): | ||
return Task( | ||
description=dedent("""\ | ||
For each email thread, pull and analyze the complete threads using only the actual Thread ID. | ||
understand the context, key points, and the overall sentiment | ||
of the conversation. | ||
Identify the main query or concerns that needs to be | ||
addressed in the response for each | ||
Your final answer MUST be a list for all emails with: | ||
- the thread_id | ||
- a summary of the email thread | ||
- a highlighting with the main points | ||
- identify the user and who he will be answering to | ||
- communication style in the thread | ||
- the sender's email address | ||
"""), | ||
agent=agent | ||
) | ||
|
||
def draft_responses_task(self, agent): | ||
return Task( | ||
description=dedent(f"""\ | ||
Based on the action-required emails identified, draft responses for each. | ||
Ensure that each response is tailored to address the specific needs | ||
and context outlined in the email. | ||
Assume the persona of the user and mimic the communication style in the thread. | ||
Use the tool provided to draft each of the responses. | ||
When using the tool pass the following input: | ||
- to (sender to be responded) | ||
- subject | ||
- message | ||
You MUST create all drafts before sending your final answer. | ||
Your final answer MUST be a confirmation that all responses have been drafted. | ||
"""), | ||
agent=agent | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from langchain_community.agent_toolkits import GmailToolkit | ||
from langchain_community.tools.gmail.create_draft import GmailCreateDraft | ||
from langchain.tools import tool | ||
|
||
class CreateDraftTool(): | ||
@tool("Create Draft") | ||
def create_draft(data): | ||
""" | ||
Useful to create an email draft. | ||
The input to this tool should be a pipe (|) separated text | ||
of length 3 (three), representing who to send the email to, | ||
the subject of the email and the actual message. | ||
For example, `[email protected]|Nice To Meet You|Hey it was great to meet you.`. | ||
""" | ||
email, subject, message = data.split('|') | ||
gmail = GmailToolkit() | ||
draft = GmailCreateDraft(api_resource=gmail.api_resource) | ||
resutl = draft({ | ||
'to': [email], | ||
'subject': subject, | ||
'message': message | ||
}) | ||
return f"\nDraft created: {resutl}\n" | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from dotenv import load_dotenv | ||
load_dotenv() | ||
|
||
from langgraph.graph import StateGraph | ||
|
||
from .state import EmailsState | ||
from .nodes import Nodes | ||
from .crew.crew import EmailFilterCrew | ||
|
||
class WorkFlow(): | ||
def __init__(self): | ||
nodes = Nodes() | ||
workflow = StateGraph(EmailsState) | ||
|
||
workflow.add_node("check_new_emails", nodes.check_email) | ||
workflow.add_node("wait_next_run", nodes.wait_next_run) | ||
workflow.add_node("draft_responses", EmailFilterCrew().kickoff) | ||
|
||
workflow.set_entry_point("check_new_emails") | ||
workflow.add_conditional_edges( | ||
"check_new_emails", | ||
nodes.new_emails, | ||
{ | ||
"continue": 'draft_responses', | ||
"end": 'wait_next_run' | ||
} | ||
) | ||
workflow.add_edge('draft_responses', 'wait_next_run') | ||
workflow.add_edge('wait_next_run', 'check_new_emails') | ||
self.app = workflow.compile() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import os | ||
import time | ||
|
||
from langchain_community.agent_toolkits import GmailToolkit | ||
from langchain_community.tools.gmail.search import GmailSearch | ||
|
||
class Nodes(): | ||
def __init__(self): | ||
self.gmail = GmailToolkit() | ||
|
||
def check_email(self, state): | ||
print("# Checking for new emails") | ||
search = GmailSearch(api_resource=self.gmail.api_resource) | ||
emails = search('after:newer_than:1d') | ||
checked_emails = state['checked_emails_ids'] if state['checked_emails_ids'] else [] | ||
thread = [] | ||
new_emails = [] | ||
for email in emails: | ||
if (email['id'] not in checked_emails) and (email['threadId'] not in thread) and ( os.environ['MY_EMAIL'] not in email['sender']): | ||
thread.append(email['threadId']) | ||
new_emails.append( | ||
{ | ||
"id": email['id'], | ||
"threadId": email['threadId'], | ||
"snippet": email['snippet'], | ||
"sender": email["sender"] | ||
} | ||
) | ||
checked_emails.extend([email['id'] for email in emails]) | ||
return { | ||
**state, | ||
"emails": new_emails, | ||
"checked_emails_ids": checked_emails | ||
} | ||
|
||
def wait_next_run(self, state): | ||
print("## Waiting for 180 seconds") | ||
time.sleep(180) | ||
return state | ||
|
||
def new_emails(self, state): | ||
if len(state['emails']) == 0: | ||
print("## No new emails") | ||
return "end" | ||
else: | ||
print("## New emails") | ||
return "continue" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import datetime | ||
from typing import TypedDict | ||
|
||
class EmailsState(TypedDict): | ||
checked_emails_ids: list[str] | ||
emails: list[dict] | ||
action_required_emails: dict |