diff --git a/scenarios/Agents/samples/investment_advisor/.env.example b/scenarios/Agents/samples/investment_advisor/.env.example new file mode 100644 index 00000000..d3398967 --- /dev/null +++ b/scenarios/Agents/samples/investment_advisor/.env.example @@ -0,0 +1,2 @@ +PROJECT_CONNECTION_STRING=";;;" +AZURE_OPENAI_DEPLOYMENT="gpt-4o-mini" \ No newline at end of file diff --git a/scenarios/Agents/samples/investment_advisor/README.md b/scenarios/Agents/samples/investment_advisor/README.md new file mode 100644 index 00000000..5077e5f6 --- /dev/null +++ b/scenarios/Agents/samples/investment_advisor/README.md @@ -0,0 +1,25 @@ +--- +page_type: sample + +languages: + - Python + +products: + - ai-services + - azure-openai +--- + +# Personal Financial Assistant +## Overview + +Function Calling with Yfinance to get latest stock prices. Summarization of user provided article. Extract country info from article, extract country, capital and other aspects, and call an API to get more information about each country. + +## Objective + +The objective of the provided notebook file is to create an Azure OpenAI Assistant named "Portfolio Management Assistant" using the Azure OpenAI API. The assistant is designed to act as a personal financial assistant, providing information and insights related to a user's investment portfolio. The script initiates a conversation with the assistant, guiding it through various financial queries and scenarios to showcase its capabilities. + +## Programming Languages + +- Python + +## Estimated Runtime: 10 mins \ No newline at end of file diff --git a/scenarios/Agents/samples/investment_advisor/agent-investment_advisor.ipynb b/scenarios/Agents/samples/investment_advisor/agent-investment_advisor.ipynb new file mode 100644 index 00000000..c20b3b15 --- /dev/null +++ b/scenarios/Agents/samples/investment_advisor/agent-investment_advisor.ipynb @@ -0,0 +1,945 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Investment Advisor**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **About the Scenario**\n", + "\n", + "This notebook demonstrates a practical use case where an AI Agent is leveraged to gain actionable insights and address key analytical questions related to managing an investment portfolio. It combines data fetching, computation, and visualization to streamline decision-making.\n", + "\n", + "The scenario utilizes an AI Agent integrated with two powerful tools: Function Calling and Code Interpreter. These tools work together to retrieve stock prices and calculate portfolio metrics, replicating real-world workflows in investment management.\n", + "\n", + "### Key Steps:\n", + "\n", + "1. *Upload Investment Data*: Import a CSV file containing the user’s investment portfolio into the OpenAI Project.\n", + "2. *Fetch Real-Time Stock Prices*: Use the Yahoo! Finance API via *Function Calling* to retrieve up-to-date closing stock data.\n", + "3. *Perform Portfolio Analysis*: Leverage *Code Interpreter* to compute key portfolio metrics and insights.\n", + "4. *Create Data Visualations*: Leverage *Code Interpreter* to generate portfolio visualization, and leverage Python libraries to render image." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Data**\n", + "This scenario uses files from the folder [`data/`](./data/) in this repo. You can clone this repo or copy this folder to make sure you have access to these files when running the sample." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Time**\n", + "You should expect to spend 10-15 minutes building and running this scenario. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Before you begin**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 1: Install required libraries\n", + "Install dependencies directly within a Jupyter notebook is a good practice because it ensures that all required packages are installed in the correct versions, making the notebook self-contained and reproducible. This approach helps other users or collaborators to set up the environment quickly and avoid potential issues related to missing or incompatible packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the packages\n", + "%pip install -r ./requirements.txt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 2: Setting up the environment\n", + "Before we begin, we need to load the necessary environment variables from a `.env` file. These variables include sensitive information such as API keys and endpoint URLs, which are crucial for running the code successfully.\n", + "\n", + "Here’s what you need to do:\n", + "- Ensure your `.env` file is properly configured in the `.venv/.env` format. We have provided an template `.env` file, `.env.example` for your reference.\n", + "- Verify that all required secrets are included in the file before running the code.\n", + "\n", + "\n", + "The `.env` file must contain the following secrets:\n", + "- PROJECT_CONNECTION_STRING: URL to connect to the Azure OpenAI Project to access project resources.\n", + "- AZURE_OPENAI_DEPLOYMENT: The name of the Azure OpenAI model deployment.\n", + "\n", + "Now, let’s load these variables and get started!\n", + "\n", + "Note: Make sure to keep your `.env` file secure and avoid sharing it publicly. \n", + "\n", + "*For more information about leveraging Python Virtual Environments can be found [here](https://docs.python.org/3/library/venv.html).*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "# Retrieve the secrets\n", + "__PROJECT_CONNECTION_STRING = os.getenv(\"PROJECT_CONNECTION_STRING\")\n", + "__AZURE_OPENAI_DEPLOYMENT = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\n", + "\n", + "# Verify environment variables\n", + "if not all([__PROJECT_CONNECTION_STRING, __AZURE_OPENAI_DEPLOYMENT]):\n", + " raise EnvironmentError(\"One or more environment variables are missing. Please check the .env file.\")\n", + "else:\n", + " print(\"Environment variables loaded successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Azure Agent Runtime setup**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 1: Initializing the Azure Agent Runtime Client\n", + "\n", + "Next, we’ll initialize the Azure Agent Runtime client. This client allows us to interact with Azure OpenAI Project Agents. We will use a `DefaultAzureCredential` to authenticate, meaning you will have to be logged in with the Azure CLI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from azure.ai.projects import AIProjectClient\n", + "from azure.identity import DefaultAzureCredential\n", + "\n", + "# Initialize the Azure AI Project client\n", + "project_client = AIProjectClient.from_connection_string(\n", + " credential=DefaultAzureCredential(),\n", + " conn_str=os.environ[\"PROJECT_CONNECTION_STRING\"],\n", + ")\n", + "\n", + "agents_client = project_client.agents\n", + "\n", + "print(\"Agent client created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 2: Upload supporting file to Azure OpenAI deployment\n", + "\n", + "Now, we'll upload the `investment_portfolio.csv` file from the `\\data` directory to Azure OpenAI, ensuring any existing `investment_portfolio.csv` is removed beforehand to ensure the latest version of the file is used and no duplicates exist. This process will handle the entire upload. The file is necessary for this scenario, but its contents can be modified as long as the file structure remains unchanged. The file schema must be as followed:\n", + "\n", + "- Symbol\n", + "- Average_Cost\n", + "- QTY" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Directory containing files to upload\n", + "directory = \"data\"\n", + "portfolio_file = \"investment_portfolio.csv\"\n", + "portfolio_file_id = None\n", + "from pathlib import Path\n", + "\n", + "\n", + "# Check if the directory exists\n", + "if not Path(directory).is_dir():\n", + " print(f\"Directory '{directory}' does not exist.\")\n", + " raise FileNotFoundError(f\"Directory '{directory}' does not exist.\")\n", + "\n", + "file_path = Path(directory) / portfolio_file\n", + "\n", + "# Check if the file exists\n", + "if not Path(file_path).is_file():\n", + " print(f\"Skipping non-file item: {portfolio_file}\")\n", + "\n", + "try:\n", + " # Delete existing file on Azure if it has the same name and purpose\n", + " existing_files = agents_client.list_files()\n", + " for f in existing_files.data:\n", + " if f.filename == portfolio_file and f.purpose == \"assistants\":\n", + " agents_client.delete_file(f.id)\n", + " print(f\"Deleted existing file: {portfolio_file}\")\n", + "\n", + " # Upload new file\n", + " with file_path.open(\"rb\") as file_data:\n", + " file = agents_client.upload_file(file=file_data, purpose=\"assistants\")\n", + " portfolio_file_id = file.id\n", + " print(f\"Uploaded file: {portfolio_file}\")\n", + "except Exception as e:\n", + " print(f\"Error processing file '{portfolio_file}': {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Azure OpenAI Agent**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Define function for Agent Function Calling tool\n", + "In this scenario, we will utilize the *Function Calling* tool within the Agent to enhance our insights by invoking a custom function. Since the LLM and Agent currently lack direct access to the internet for user prompts, *Function Calling* serves as a powerful alternative.\n", + "\n", + "We will use the `fetch_stock_price` function to retrieve stock data for a specified *ticker symbol*. This function leverages the `yfinance` library to fetch the most recent stock price data, specifically for the last trading day.\n", + "\n", + "This approach enables the Agent to provide up-to-date, actionable insights by integrating external data sources through custom functions. \n", + "\n", + "Let’s explore how to implement and use this functionality!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yfinance as yf\n", + "\n", + "\n", + "def fetch_stock_price(ticker_symbol: str) -> str:\n", + " \"\"\"\n", + " Fetch the latest stock price for a given ticker symbol.\n", + "\n", + " Parameters:\n", + " - ticker_symbol (str): The ticker symbol of the stock to retrieve data for.\n", + "\n", + " Returns:\n", + " - str: The closing price of the stock for the latest trading day, or an error message if data is unavailable.\n", + "\n", + " Example:\n", + " >>> fetch_stock_price(\"AAPL\")\n", + " \"148.9\"\n", + " \"\"\"\n", + "\n", + " try:\n", + " # Fetch the stock's trading history for the last day\n", + " stock = yf.Ticker(ticker_symbol)\n", + " stock_data = stock.history(period=\"1d\")\n", + "\n", + " # Check if the data is empty, indicating an invalid ticker or no data available\n", + " if stock_data.empty:\n", + " return f\"Error: No data found for ticker symbol: {ticker_symbol}\"\n", + "\n", + " # Retrieve and return the latest closing price\n", + " latest_close_price = stock_data[\"Close\"].iloc[-1]\n", + " return str(round(latest_close_price, 3))\n", + "\n", + " except KeyError as e:\n", + " return f\"Error: Data missing for key: {e}. Verify the ticker symbol.\"\n", + "\n", + " except Exception as e:\n", + " return f\"Error: Unexpected issue occurred - {type(e).__name__}: {e}\"\n", + "\n", + "\n", + "print(\"Function defined successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define a dictionary, `available_functions`, to map function names to their respective implementations, such as `fetch_stock_price`. This structure is designed to support scalability, allowing easy addition of multiple callable functions to expand your solution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "available_functions = {\"fetch_stock_price\": fetch_stock_price}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Configure Agent Tool-Calling for Enhanced Functionality\n", + "The tool-calling definition informs the LLM about the tools available for its use. In our scenario, this configuration enables both *code interpretation* and *function calling*.\n", + "\n", + "We will specifically configure the `fetch_stock_price` function alongside the *code interpreter tool*. By integrating this tool into the Agent, we empower it to:\n", + "\n", + "- Retrieve stock data for a specified ticker symbol using the `fetch_stock_price` function.\n", + "- Interpret and analyze the retrieved data to provide meaningful insights in response to user queries.\n", + "\n", + "This setup allows the Agent to seamlessly combine external data retrieval with code-based reasoning, ensuring accurate and actionable responses to stock-related prompts. \n", + "\n", + "Let’s define and integrate this tool to unlock its full potential!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tools_list = [\n", + " {\"type\": \"code_interpreter\"},\n", + " {\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"fetch_stock_price\",\n", + " \"description\": \"Retrieve the latest closing price of a stock using its ticker symbol.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\"ticker_symbol\": {\"type\": \"string\", \"description\": \"The ticker symbol of the stock\"}},\n", + " \"required\": [\"ticker_symbol\"],\n", + " },\n", + " },\n", + " },\n", + "]\n", + "\n", + "print(\"Tools list defined successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Creating an Investment Analysis Agent\n", + "In this step, we’ll set up an Azure OpenAI Agent with the specified tools to handle investment-related queries. This Agent will also leverage the previously defined function calls to provide accurate and actionable insights. \n", + "\n", + "Let’s configure the Agent and enable its capabilities! " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the Agent with code interpreter and function calling tools enabled\n", + "try:\n", + " agent = agents_client.create_agent(\n", + " name=\"Investment Advisor Agent\",\n", + " instructions=(\n", + " \"You are an expert investment analyst. \"\n", + " \"Use your knowledge base to answer questions about personal investment portfolio management.\"\n", + " ),\n", + " model=__AZURE_OPENAI_DEPLOYMENT,\n", + " tools=tools_list,\n", + " )\n", + " print(\"Agent created successfully.\\n\", agent.id)\n", + "except Exception as e:\n", + " print(\"Error creating Agent:\", e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Starting a New Conversation Thread\n", + "Let’s create a new thread to handle user interactions. Each thread provides a dedicated space for conversations with the Agent, keeping the context focused and organized. \n", + "\n", + "This setup ensures clear and seamless communication for each user interaction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a conversation thread\n", + "try:\n", + " thread = agents_client.create_thread()\n", + " print(\"Thread created successfully:\", thread.id)\n", + "except Exception as e:\n", + " print(\"Error creating thread:\", e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Demonstrating the Thread Run Process (Manual)**\n", + "\n", + "The next six cells will demonstrate how the Agent's thread run process works. Instead of running a typical loop with a polling mechanism to monitor the run status, we’ll manually step through the cells to better understand each stage of the process.\n", + "\n", + "In the subsequent section, these cells will be wrapped into a processing function to expand the functionality by incorporating polling and different completion content type, providing a more streamlined and automated workflow. This approach helps clarify how the thread execution operates while offering a hands-on view of each step." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Adding User Message to the Thread\n", + "In this step, we’ll add a user message to the thread to demonstrate the Agent’s capabilities. Specifically, we’ll ask for the latest closing price of a specified company (*Microsoft*), which will leverage *function calling* to retrieve this data using the `fetch_stock_price` function.\n", + "\n", + "The Agent will then use the *code interpreter* to combine this information with the `QTY` data from the uploaded `investment_portfolio.csv` file to calculate the **total investment**. \n", + "\n", + "This workflow showcases how the Agent integrates external data retrieval with computational analysis to deliver actionable insights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the user question\n", + "prompt_content = \"What is the latest closing price for Microsoft? What is my total investment for MSFT as of today?\"\n", + "\n", + "# Add the question to the thread\n", + "try:\n", + " message = agents_client.create_message(\n", + " thread_id=thread.id,\n", + " role=\"user\",\n", + " content=prompt_content,\n", + " attachments=[ # Add files by using the attachments parameter\n", + " {\"file_id\": portfolio_file_id, \"tools\": [{\"type\": \"code_interpreter\"}]}\n", + " ],\n", + " )\n", + " print(\"Successfully added User prompt to the thread.\\n\", message.id)\n", + "except Exception as e:\n", + " print(\"Error adding user question:\", e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Running the Agent\n", + "Now that the Agent and thread are set up, we'll initiate the Agent's response process. This will analyze the user prompt and provide insights based on the investment portfolio data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initiate the Agent's response\n", + "try:\n", + " run = agents_client.create_run(\n", + " thread_id=thread.id,\n", + " assistant_id=agent.id,\n", + " instructions=prompt_content,\n", + " )\n", + " print(\"Run started:\", run.id)\n", + "except Exception as e:\n", + " print(\"Error starting run:\", e)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Monitor run status" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A Run is an instance where an Agent operates within a Thread. During a Run, the Agent processes the Thread's Messages and its configuration to perform tasks using models and tools, adding additional Messages to the Thread as part of its operations.\n", + "\n", + "The Agent might take some time to analyze and respond, so it's important to monitor the run status. Based on the status, *specific actions are executed*. \n", + "\n", + "Let's dive deeper into these status-driven actions!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "# Retrieve the run status\n", + "run = agents_client.get_run(thread_id=thread.id, run_id=run.id)\n", + "# print(run.model_dump_json(indent=4))\n", + "\n", + "if run.status in (\"queued\", \"in_progress\"):\n", + " print(f\"Run this cell again to monitor the status.\\nCurrent Status: {run.status}\")\n", + "else:\n", + " print(f\"Monitoring the run status...\\nNavigate to `{run.status}` cell.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 4a: Failed status\n", + "\n", + "If the status is `failed` we will print the error message with relevant information to aid in troubleshooting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# print(run.model_dump_json(indent=4))\n", + "\n", + "if run.status == \"failed\":\n", + " print(\"Agent run failed. Please try again.\")\n", + " print(run)\n", + "else:\n", + " print(f\"Agent run has not failed...\\nNavigate to and execute the '{run.status}' cell.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 4b: Requires Action status\n", + "\n", + "If the status is `requires_action`, first we need to check if the required action is to submit tool outputs and iterate over the tool calls, ensuring that the requested function exists in the available_functions dictionary. If the function exists, it is called with the provided arguments, and the response is stored. After processing all tool calls, we submit the tool outputs back to the OpenAI client." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if run.status == \"requires_action\":\n", + " print(\"Function Calling ...\")\n", + " tool_responses = []\n", + " if (\n", + " run.required_action.type == \"submit_tool_outputs\"\n", + " and run.required_action.submit_tool_outputs.tool_calls is not None\n", + " ):\n", + " tool_calls = run.required_action.submit_tool_outputs.tool_calls\n", + "\n", + " for call in tool_calls:\n", + " if call.type == \"function\":\n", + " if call.function.name not in available_functions:\n", + " raise Exception(\"Function requested by the model does not exist\")\n", + " function_to_call = available_functions[call.function.name]\n", + " tool_response = function_to_call(**json.loads(call.function.arguments))\n", + " tool_responses.append({\"tool_call_id\": call.id, \"output\": tool_response})\n", + " print(f\"Function '{call.function.name}' called successfully. \\nOutput: {tool_response}\\n\")\n", + "\n", + " run = agents_client.submit_tool_outputs_to_run(thread_id=thread.id, run_id=run.id, tool_outputs=tool_responses)\n", + "\n", + " print(\"Results submitted successfully. Go back to the first cell, 'Monitor Run Status' and execute again.\")\n", + "else:\n", + " print(f\"Navigate to and execute the {run.status} cell.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Step 4c: Completed status\n", + "\n", + "If the status is `completed`, we will fetch and print all messages in the thread, displaying the role and content of each message.The messages are printed in reverse order because messages in a thread are in FILO (First-In-Last-Out) order and in order to make the messages more conversational for ease of user reading, we must reverse the order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if run.status == \"completed\":\n", + " messages = agents_client.list_messages(thread_id=thread.id, order=\"asc\", after=message.id)\n", + "\n", + " print(\"Run completed!\\n\\nMESSAGES\\n\")\n", + "\n", + " # Loop through messages and print content based on role\n", + " for msg in messages.data:\n", + " role = msg.role\n", + " content = msg.content[0].text.value\n", + " print(f\"{role.capitalize()}: {content}\")\n", + "\n", + "else:\n", + " print(f\"Navigate to and execute the {run.status} cell.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Refining the Process**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Wrapping the Thread Run Process into a Function\n", + "In this section we will refine the manual approach and encapsulate the thread run process into a single function. This function automates the steps demonstrated in the previous cells by including the polling functionality to monitor the run status.\n", + "\n", + "By wrapping the process into a reusable function, we streamline the workflow and ensure that the thread execution is both efficient and easy to integrate into broader applications. The function also expands the functionality for handling different completion content types, such as image rendering.\n", + "\n", + "Let’s implement this functionality!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "from PIL import Image\n", + "from pathlib import Path\n", + "\n", + "available_functions = {\"fetch_stock_price\": fetch_stock_price}\n", + "\n", + "\n", + "def process_message(thread_id: str, prompt_message: str, attachments_list: list[str] | None = None) -> None:\n", + " try:\n", + " # Add the prompt to the thread\n", + " agents_client.create_message(\n", + " thread_id=thread_id,\n", + " role=\"user\",\n", + " content=prompt_message,\n", + " attachments=attachments_list,\n", + " )\n", + " print(\"User message added...\") #:\", message)\n", + "\n", + " # Initiate the Agent's response\n", + " run = agents_client.create_run(\n", + " thread_id=thread_id,\n", + " assistant_id=agent.id,\n", + " instructions=prompt_message,\n", + " )\n", + " print(\"Run started...\") #:\", run)\n", + " except Exception as e:\n", + " print(\"Error starting run:\", e)\n", + "\n", + " while True: # Polling to monitor Run status\n", + " time.sleep(5) # Wait 5 seconds to give the process time to move past `queued` state\n", + "\n", + " # Retrieves the thread's response.\n", + " run = agents_client.get_run(thread_id=thread_id, run_id=run.id)\n", + "\n", + " run_status = run.status\n", + " print(f\"Run Status: {run_status}\\n\")\n", + " if run_status == \"completed\":\n", + " # Get all messages in thread to read\n", + " thread_messages = agents_client.list_messages(thread_id=thread_id, order=\"asc\") # , after=message.id)\n", + "\n", + " # Loop through thread messages and print content\n", + " for thread_message in thread_messages.data:\n", + " role = thread_message.role\n", + " content = None\n", + " if isinstance(thread_message.content, list) and thread_message.content:\n", + " for message_content in thread_message.content:\n", + " # handle text\n", + " if hasattr(message_content, \"text\") and hasattr(message_content.text, \"value\"):\n", + " content = message_content.text.value\n", + "\n", + " # handle image file\n", + " elif hasattr(message_content, \"image_file\"):\n", + " # Download the file locally\n", + " file_content = agents_client.get_file_content(message_content.image_file.file_id)\n", + "\n", + " # Delete existing file locally if it has the same name\n", + " if Path(\"./data/sample_chart.png\").exists():\n", + " Path(\"./data/sample_chart.png\").unlink()\n", + " print(\"Deleted existing file: ./data/sample_chart.png\")\n", + "\n", + " file_path = Path(\"./data/sample_chart.png\")\n", + " with file_path.open(\"wb\") as f:\n", + " for chunk in file_content:\n", + " f.write(chunk)\n", + " print(\"Image File downloaded to './data' successfully.\")\n", + "\n", + " # Display the image using Pillow and Matplotlib\n", + " img = Image.open(\"./data/sample_chart.png\")\n", + " plt.imshow(img)\n", + " plt.axis(\"off\")\n", + " plt.show()\n", + "\n", + " else:\n", + " content = str(message_content)\n", + "\n", + " print(f\"{role.capitalize()}: {content}\")\n", + "\n", + " break\n", + " if run.status == \"failed\":\n", + " messages = agents_client.list_messages(thread_id=thread.id)\n", + " answer = messages.data[0].content[0].text.value\n", + " print(f\"Failed User:\\n{prompt_message}\\nAgent:\\n{answer}\\n\")\n", + "\n", + " # Handle failed\n", + " break\n", + "\n", + " if run.status == \"requires_action\" and run.required_action.type == \"submit_tool_outputs\":\n", + " print(\"Function calling initiated...\")\n", + " tool_calls = run.required_action.submit_tool_outputs.tool_calls\n", + " tool_responses = []\n", + "\n", + " # Iterate over each function call requested by the Agent\n", + " for call in tool_calls:\n", + " # Check if the call is a function and if the function exists is our custom function\n", + " if call.type == \"function\" and call.function.name in available_functions:\n", + " func = available_functions[call.function.name] # Retrieve the function reference\n", + "\n", + " # Parse the function arguments from JSON and execute the function\n", + " tool_response = func(**json.loads(call.function.arguments))\n", + "\n", + " # Store the tool call ID and output to later send back to the Agent\n", + " tool_responses.append({\"tool_call_id\": call.id, \"output\": tool_response})\n", + " print(f\"Executed '{call.function.name}'. Output: {tool_response}\")\n", + "\n", + " else:\n", + " # Raise an error if the function is not in available_functions to handle unexpected requests\n", + " raise ValueError(f\"Requested function '{call.function.name}' is not available.\")\n", + "\n", + " # Submit all collected tool outputs back to the Agent to satisfy the required action\n", + " run = agents_client.submit_tool_outputs_to_run(\n", + " thread_id=thread_id, run_id=run.id, tool_outputs=tool_responses\n", + " )\n", + " print(\"Function call(s) completed successfully.\")\n", + "\n", + " else:\n", + " time.sleep(5)\n", + "\n", + "\n", + "print(\"Function `process_message` created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2a: Basic Message Sample\n", + "This sample demonstrates how the Agent uses the standard LLM system to answer the query: *\"What is today's date?\"* The Agent responds by retrieving the current date directly from its built-in capabilities, without relying on additional tools or attachments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "process_message(thread_id=thread.id, prompt_message=\"What is today's date?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2b: Code Interpreter Sample\n", + "This sample demonstrates the use of *Code Interpreter* to analyze an uploaded portfolio file and answer the query: \"What stock do I have the most investment in?\". The Code Interpreter analyzes the file to identify the stock with the highest investment and provides the result, showcasing its ability to integrate data analysis seamlessly into the conversation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attachments = [ # Add files by using the attachments parameter\n", + " {\"file_id\": portfolio_file_id, \"tools\": [{\"type\": \"code_interpreter\"}]}\n", + "]\n", + "\n", + "process_message(\n", + " thread_id=thread.id, prompt_message=\"Which company have I invested in the most?\", attachments_list=attachments\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2c: Function Calling Sample\n", + "This sample demonstrates how the Agent utilizes the *Function Calling* tool to answer the query: *\"What is the current stock price for Amazon?\"*. The Agent invokes the previously defined `fetch_stock_price` function to retrieve the latest stock price for Amazon, showcasing how the Function Calling tool integrates external data retrieval into the Agent's workflow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "process_message(\n", + " thread_id=thread.id,\n", + " prompt_message=\"What is the current stock price for Amazon? Based on this price, what is the total investment value for AMZN?\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2d: Generating and Visualizing Data\n", + "This sample shows how an Agent can be used to generate data visualizations, save them locally, and leverage with Python libraries like `Pillow` and `Matplotlib` for rendering. For example, when asked *\"Show a pie chart of my investments\"*, the Agent analyzes the `investment_portfolio` file using Code Interpreter and creates a downloadable pie chart image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "process_message(\n", + " thread_id=thread.id,\n", + " prompt_message=\"Show a pie chart of my investments? Ensure the pie chart color schema aligns with accessibility standards.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Clean Up**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Delete Agent Resource\n", + "To avoid creating redundant resources and ensure a clean environment, this cell deletes the Agent, thread, and any other created resources.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get agent list\n", + "agents = agents_client.list_agents()\n", + "\n", + "# If the agent exists in the list, delete it\n", + "if agent.id in [a.id for a in agents.data]:\n", + " response = agents_client.delete_agent(agent.id)\n", + " print(\"Deleted Agent Client\\n\", response)\n", + "else:\n", + " print(\"Agent does not exist to delete.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Delete File\n", + "Deletes the file that was uploaded during the execution of the scenario notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " # Delete existing file on Azure if it has the same name and purpose\n", + " existing_files = agents_client.list_files()\n", + " for f in existing_files.data:\n", + " if f.filename == portfolio_file and f.purpose == \"assistants\":\n", + " agents_client.delete_file(f.id)\n", + " print(f\"Deleted existing file: {portfolio_file}\")\n", + "\n", + "\n", + "except Exception as e:\n", + " print(f\"Error processing file '{portfolio_file}': {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Wrap Up**\n", + "This notebook demonstrated how Generative AI can be leveraged to streamline investment portfolio analysis by automating data retrieval, computation, and insight generation. Key aspects included:\n", + "\n", + "1. *Data Import*:\n", + " - Uploaded and processed a CSV file containing the investment portfolio details.\n", + "\n", + "2. *Real-Time Stock Data Retrieval*:\n", + " - Integrated the Yahoo! Finance API to fetch live stock prices for portfolio assets.\n", + "\n", + "3. *Portfolio Analysis*:\n", + " - Computed key metrics such as total portfolio value, asset-wise allocation, and performance indicators.\n", + " - Derived actionable insights into asset performance and allocation efficiency.\n", + "\n", + "4. *Visualization and Insights*:\n", + " - Presented key findings with graphs and charts for better interpretation.\n", + " - Highlighted trends and potential areas for optimization.\n", + "\n", + "5. *AI Workflow*:\n", + " - Demonstrated how Function Calling and Code Interpreter tools integrate to handle real-world investment management tasks.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Next Steps: Expanding Generative AI Applications**\n", + "\n", + "Building on this scenario, the following Generative AI-focused advancements can be pursued:\n", + "\n", + "1. *Interactive Portfolio Management Application*:\n", + " - Develop a web or mobile application powered by Generative AI to provide real-time portfolio insights, visualizations, and recommendations.\n", + " - Integrate conversational interfaces for users to query portfolio performance or simulate investment strategies in natural language.\n", + "\n", + "2. *Enhanced Personalization*:\n", + " - Train models to provide personalized investment advice based on user-defined goals, risk tolerance, and preferences.\n", + " - Implement scenario-based AI recommendations for portfolio rebalancing.\n", + "\n", + "3. *Proactive Alerts and Forecasts*:\n", + " - Use Generative AI to provide alerts for significant market movements or deviations in portfolio performance.\n", + " - Implement predictive models to forecast asset performance or potential market risks." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/scenarios/Agents/samples/investment_advisor/data/investment_portfolio.csv b/scenarios/Agents/samples/investment_advisor/data/investment_portfolio.csv new file mode 100644 index 00000000..bb2df97b --- /dev/null +++ b/scenarios/Agents/samples/investment_advisor/data/investment_portfolio.csv @@ -0,0 +1,7 @@ +Symbol,Average_Cost,QTY +MSFT,200,300 +AAPL,114,200 +amzn,125,50 +TSLA,900,100 +NfLx,540,80 +NVDA,450,50 \ No newline at end of file diff --git a/scenarios/Agents/samples/investment_advisor/requirements.txt b/scenarios/Agents/samples/investment_advisor/requirements.txt new file mode 100644 index 00000000..bcd326b0 --- /dev/null +++ b/scenarios/Agents/samples/investment_advisor/requirements.txt @@ -0,0 +1,7 @@ +azure-identity==1.19.0 +pandas==2.2.3 +yfinance==0.2.48 +python-dotenv==1.0.1 +Pillow==11.0.0 +matplotlib==3.9.2 +azure-ai-projects==1.0.0b1 \ No newline at end of file