From bf8d518c8251ad26b85426a8b9ac71421a9d3e80 Mon Sep 17 00:00:00 2001 From: Cole Medin <47287758+coleam00@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:47:55 -0500 Subject: [PATCH] AI Agents Masterclass #6 - RAG AI Agent --- .gitignore | 3 +- 6-rag-task-agent/.env.example | 34 ++ 6-rag-task-agent/meeting_notes/2024-07-18.pdf | Bin 0 -> 4309 bytes 6-rag-task-agent/meeting_notes/2024-07-19.pdf | Bin 0 -> 4395 bytes 6-rag-task-agent/meeting_notes/2024-07-20.txt | 71 ++++ 6-rag-task-agent/meeting_notes/2024-07-21.txt | 71 ++++ 6-rag-task-agent/meeting_notes/2024-07-22.txt | 78 ++++ 6-rag-task-agent/rag-document-loader.py | 35 ++ 6-rag-task-agent/rag-task-agent.py | 336 ++++++++++++++++++ 6-rag-task-agent/requirements.txt | 12 + 10 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 6-rag-task-agent/.env.example create mode 100644 6-rag-task-agent/meeting_notes/2024-07-18.pdf create mode 100644 6-rag-task-agent/meeting_notes/2024-07-19.pdf create mode 100644 6-rag-task-agent/meeting_notes/2024-07-20.txt create mode 100644 6-rag-task-agent/meeting_notes/2024-07-21.txt create mode 100644 6-rag-task-agent/meeting_notes/2024-07-22.txt create mode 100644 6-rag-task-agent/rag-document-loader.py create mode 100644 6-rag-task-agent/rag-task-agent.py create mode 100644 6-rag-task-agent/requirements.txt diff --git a/.gitignore b/.gitignore index dad015d2..9e1fce8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ prep -.env \ No newline at end of file +.env +chroma_db \ No newline at end of file diff --git a/6-rag-task-agent/.env.example b/6-rag-task-agent/.env.example new file mode 100644 index 00000000..530ac9f6 --- /dev/null +++ b/6-rag-task-agent/.env.example @@ -0,0 +1,34 @@ +# Rename this file to .env once you have filled in the below environment variables! + +# Get your Open AI API Key by following these instructions - +# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key +# You only need this environment variable set if you set LLM_MODEL to a GPT model +OPENAI_API_KEY= + +# Get your Anthropic API Key in your account settings - +# https://console.anthropic.com/settings/keys +# You only need this environment variable set if you set LLM_MODEL to a Claude model +ANTHROPIC_API_KEY= + +# See all Open AI models you can use here - +# https://platform.openai.com/docs/models +# And all Anthropic models you can use here - +# https://docs.anthropic.com/en/docs/about-claude/models +# A good default to go with here is gpt-4o or claude-3-5-sonnet-20240620 +LLM_MODEL=gpt-4o + +# The absolute or relative path to the folder that has all the files for retrieval for RAG +# If using a relative path, the path is relative to the directoy containing this .env.example file +DIRECTORY=meeting_notes + +# Get your personal Asana access token through the developer console in Asana. +# Feel free to follow these instructions - +# https://developers.asana.com/docs/personal-access-token +ASANA_ACCESS_TOKEN= + +# The Asana workspace ID is in the URL when you visit your Asana Admin Console (when logged in). +# Go to the URL "https://app.asana.com/admin" and then your workspace ID +# will appear in the URL as a slew of digits once the site loads. +# If your URL is https://app.asana.com/admin/987654321/insights, then your +# Asana workspace ID is 987654321 +ASANA_WORKPLACE_ID= \ No newline at end of file diff --git a/6-rag-task-agent/meeting_notes/2024-07-18.pdf b/6-rag-task-agent/meeting_notes/2024-07-18.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fdf0900ebdaf1b10679258a4c6df63e40bb6cfd2 GIT binary patch literal 4309 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 la%`zF;~Z5-;wrPlrSul!_N{cN+jy*KWPLdX6 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 z|1X+lcreate_task: {e}" + +@tool +def get_asana_projects(): + """ + Gets all of the projects in the user's Asana workspace + + Returns: + str: The API response from getting the projects or an error message if the projects couldn't be fetched. + The API response is an array of project objects, where each project object looks like: + {'gid': '1207789085525921', 'name': 'Project Name', 'resource_type': 'project'} + """ + opts = { + 'limit': 50, # int | Results per page. The number of objects to return per page. The value must be between 1 and 100. + 'workspace': workspace_gid, # str | The workspace or organization to filter projects on. + 'archived': False # bool | Only return projects whose `archived` field takes on the value of this parameter. + } + + try: + api_response = projects_api_instance.get_projects(opts) + return json.dumps(list(api_response), indent=2) + except ApiException as e: + return "Exception when calling ProjectsApi->create_project: %s\n" % e + +@tool +def create_asana_project(project_name, due_on=None): + """ + Creates a project in Asana given the name of the project and optionally when it is due + + Example call: + + create_asana_project("Test Project", "2024-06-24") + Args: + project_name (str): The name of the project in Asana + due_on (str): The date the project is due in the format YYYY-MM-DD. If not supplied, the project is not given a due date + Returns: + str: The API response of adding the project to Asana or an error message if the API call threw an error + """ + body = { + "data": { + "name": project_name, "due_on": due_on, "workspace": workspace_gid + } + } # dict | The project to create. + + try: + # Create a project + api_response = projects_api_instance.create_project(body, {}) + return json.dumps(api_response, indent=2) + except ApiException as e: + return "Exception when calling ProjectsApi->create_project: %s\n" % e + +@tool +def get_asana_tasks(project_gid): + """ + Gets all the Asana tasks in a project + + Example call: + + get_asana_tasks("1207789085525921") + Args: + project_gid (str): The ID of the project in Asana to fetch the tasks for + Returns: + str: The API response from fetching the tasks for the project in Asana or an error message if the API call threw an error + The API response is an array of tasks objects where each task object is in the format: + {'gid': '1207780961742158', 'created_at': '2024-07-11T16:25:46.380Z', 'due_on': None or date in format "YYYY-MM-DD", 'name': 'Test Task'} + """ + opts = { + 'limit': 50, # int | Results per page. The number of objects to return per page. The value must be between 1 and 100. + 'project': project_gid, # str | The project to filter tasks on. + 'opt_fields': "created_at,name,due_on", # list[str] | This endpoint returns a compact resource, which excludes some properties by default. To include those optional properties, set this query parameter to a comma-separated list of the properties you wish to include. + } + + try: + # Get multiple tasks + api_response = tasks_api_instance.get_tasks(opts) + return json.dumps(list(api_response), indent=2) + except ApiException as e: + return "Exception when calling TasksApi->get_tasks: %s\n" % e + +@tool +def update_asana_task(task_gid, data): + """ + Updates a task in Asana by updating one or both of completed and/or the due date + + Example call: + + update_asana_task("1207780961742158", {"completed": True, "due_on": "2024-07-13"}) + Args: + task_gid (str): The ID of the task to update + data (dict): A dictionary with either one or both of the keys 'completed' and/or 'due_on' + If given, completed needs to be either True or False. + If given, the due date needs to be in the format 'YYYY-MM-DD'. + Returns: + str: The API response of updating the task or an error message if the API call threw an error + """ + # Data: {"completed": True or False, "due_on": "YYYY-MM-DD"} + body = {"data": data} # dict | The task to update. + + try: + # Update a task + api_response = tasks_api_instance.update_task(body, task_gid, {}) + return json.dumps(api_response, indent=2) + except ApiException as e: + return "Exception when calling TasksApi->update_task: %s\n" % e + +@tool +def delete_task(task_gid): + """ + Deletes a task in Asana + + Example call: + + delete_task("1207780961742158") + Args: + task_gid (str): The ID of the task to delete + Returns: + str: The API response of deleting the task or an error message if the API call threw an error + """ + try: + # Delete a task + api_response = tasks_api_instance.delete_task(task_gid) + return json.dumps(api_response, indent=2) + except ApiException as e: + return "Exception when calling TasksApi->delete_task: %s\n" % e + +@tool +def query_documents(question): + """ + Uses RAG to query documents for information to answer a question + that requires specific context that could be found in documents + + Example call: + + query_documents("What are the action items from the meeting on the 20th?") + Args: + question (str): The question the user asked that might be answerable from the searchable documents + Returns: + str: The list of texts (and their sources) that matched with the question the closest using RAG + """ + similar_docs = db.similarity_search(question, k=3) + docs_formatted = list(map(lambda doc: f"Source: {doc.metadata.get('source', 'NA')}\nContent: {doc.page_content}", similar_docs)) + + return str(docs_formatted) + +# Maps the function names to the actual function object in the script +# This mapping will also be used to create the list of tools to bind to the agent +available_functions = { + "create_asana_task": create_asana_task, + "get_asana_projects": get_asana_projects, + "create_asana_project": create_asana_project, + "get_asana_tasks": get_asana_tasks, + "update_asana_task": update_asana_task, + "delete_task": delete_task, + "query_documents": query_documents +} + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~ AI Prompting Function ~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def prompt_ai(messages, nested_calls=0): + if nested_calls > 5: + raise "AI is tool calling too much!" + + # First, prompt the AI with the latest user message + tools = [tool for _, tool in available_functions.items()] + asana_chatbot = ChatOpenAI(model=model) if "gpt" in model.lower() else ChatAnthropic(model=model) + asana_chatbot_with_tools = asana_chatbot.bind_tools(tools) + + stream = asana_chatbot_with_tools.stream(messages) + first = True + for chunk in stream: + if first: + gathered = chunk + first = False + else: + gathered = gathered + chunk + + yield chunk + + has_tool_calls = len(gathered.tool_calls) > 0 + + # Second, see if the AI decided it needs to invoke a tool + if has_tool_calls: + # Add the tool request to the list of messages so the AI knows later it invoked the tool + messages.append(gathered) + + # If the AI decided to invoke a tool, invoke it + # For each tool the AI wanted to call, call it and add the tool result to the list of messages + for tool_call in gathered.tool_calls: + tool_name = tool_call["name"].lower() + selected_tool = available_functions[tool_name] + tool_output = selected_tool.invoke(tool_call["args"]) + messages.append(ToolMessage(tool_output, tool_call_id=tool_call["id"])) + + # Call the AI again so it can produce a response with the result of calling the tool(s) + additional_stream = prompt_ai(messages, nested_calls + 1) + for additional_chunk in additional_stream: + yield additional_chunk + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~ Main Function with UI Creation ~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +system_message = f""" +You are a personal assistant who helps manage tasks in Asana. +You never give IDs to the user since those are just for you to keep track of. +When a user asks to create a task and you don't know the project to add it to for sure, clarify with the user. +The current date is: {datetime.now().date()} +""" + +def main(): + st.title("Asana Chatbot") + + # Initialize chat history + if "messages" not in st.session_state: + st.session_state.messages = [ + SystemMessage(content=system_message) + ] + + # Display chat messages from history on app rerun + for message in st.session_state.messages: + message_json = json.loads(message.json()) + message_type = message_json["type"] + if message_type in ["human", "ai", "system"]: + with st.chat_message(message_type): + st.markdown(message_json["content"]) + + # React to user input + if prompt := st.chat_input("What would you like to do today?"): + # Display user message in chat message container + st.chat_message("user").markdown(prompt) + # Add user message to chat history + st.session_state.messages.append(HumanMessage(content=prompt)) + + # Display assistant response in chat message container + with st.chat_message("assistant"): + stream = prompt_ai(st.session_state.messages) + response = st.write_stream(stream) + + st.session_state.messages.append(AIMessage(content=response)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/6-rag-task-agent/requirements.txt b/6-rag-task-agent/requirements.txt new file mode 100644 index 00000000..1f4a89e3 --- /dev/null +++ b/6-rag-task-agent/requirements.txt @@ -0,0 +1,12 @@ +asana==5.0.7 +openai==1.10.0 +python-dotenv==0.13.0 +langchain==0.2.6 +langchain-anthropic==0.1.16 +langchain-community==0.2.6 +langchain-core==0.2.10 +langchain-openai==0.1.10 +langchain-chroma==0.1.2 +streamlit==1.36.0 +pdfminer.six==20240706 +ustructured[all-docs] \ No newline at end of file