diff --git a/agentops/llms/providers/openai.py b/agentops/llms/providers/openai.py index eb318581e..36df289e1 100644 --- a/agentops/llms/providers/openai.py +++ b/agentops/llms/providers/openai.py @@ -1,4 +1,3 @@ -import inspect import pprint from typing import Optional @@ -16,6 +15,8 @@ class OpenAiProvider(InstrumentedProvider): original_create = None original_create_async = None + original_assistant_methods = None + assistants_run_steps = {} def __init__(self, client): super().__init__(client) @@ -138,6 +139,7 @@ async def async_generator(): def override(self): self._override_openai_v1_completion() self._override_openai_v1_async_completion() + self._override_openai_assistants_beta() def _override_openai_v1_completion(self): from openai.resources.chat import completions @@ -228,9 +230,114 @@ async def patched_function(*args, **kwargs): # Override the original method with the patched one completions.AsyncCompletions.create = patched_function + def _override_openai_assistants_beta(self): + """Override OpenAI Assistants API methods""" + from openai._legacy_response import LegacyAPIResponse + from openai.resources import beta + from openai.pagination import BasePage + + def handle_response(response, kwargs, init_timestamp, session: Optional[Session] = None) -> dict: + """Handle response based on return type""" + action_event = ActionEvent(init_timestamp=init_timestamp, params=kwargs) + if session is not None: + action_event.session_id = session.session_id + + try: + # Set action type and returns + action_event.action_type = ( + response.__class__.__name__.split("[")[1][:-1] + if isinstance(response, BasePage) + else response.__class__.__name__ + ) + action_event.returns = response.model_dump() if hasattr(response, "model_dump") else response + action_event.end_timestamp = get_ISO_time() + self._safe_record(session, action_event) + + # Create LLMEvent if usage data exists + response_dict = response.model_dump() if hasattr(response, "model_dump") else {} + + if "id" in response_dict and response_dict.get("id").startswith("run"): + if response_dict["id"] not in self.assistants_run_steps: + self.assistants_run_steps[response_dict.get("id")] = {"model": response_dict.get("model")} + + if "usage" in response_dict and response_dict["usage"] is not None: + llm_event = LLMEvent(init_timestamp=init_timestamp, params=kwargs) + if session is not None: + llm_event.session_id = session.session_id + + llm_event.model = response_dict.get("model") + llm_event.prompt_tokens = response_dict["usage"]["prompt_tokens"] + llm_event.completion_tokens = response_dict["usage"]["completion_tokens"] + llm_event.end_timestamp = get_ISO_time() + self._safe_record(session, llm_event) + + elif "data" in response_dict: + for item in response_dict["data"]: + if "usage" in item and item["usage"] is not None: + llm_event = LLMEvent(init_timestamp=init_timestamp, params=kwargs) + if session is not None: + llm_event.session_id = session.session_id + + llm_event.model = self.assistants_run_steps[item["run_id"]]["model"] + llm_event.prompt_tokens = item["usage"]["prompt_tokens"] + llm_event.completion_tokens = item["usage"]["completion_tokens"] + llm_event.end_timestamp = get_ISO_time() + self._safe_record(session, llm_event) + + except Exception as e: + self._safe_record(session, ErrorEvent(trigger_event=action_event, exception=e)) + + kwargs_str = pprint.pformat(kwargs) + response = pprint.pformat(response) + logger.warning( + f"Unable to parse response for Assistants API. Skipping upload to AgentOps\n" + f"response:\n {response}\n" + f"kwargs:\n {kwargs_str}\n" + ) + + return response + + def create_patched_function(original_func): + def patched_function(*args, **kwargs): + init_timestamp = get_ISO_time() + + session = kwargs.get("session", None) + if "session" in kwargs.keys(): + del kwargs["session"] + + response = original_func(*args, **kwargs) + if isinstance(response, LegacyAPIResponse): + return response + + return handle_response(response, kwargs, init_timestamp, session=session) + + return patched_function + + # Store and patch Assistant API methods + assistant_api_methods = { + beta.Assistants: ["create", "retrieve", "update", "delete", "list"], + beta.Threads: ["create", "retrieve", "update", "delete"], + beta.threads.Messages: ["create", "retrieve", "update", "list"], + beta.threads.Runs: ["create", "retrieve", "update", "list", "submit_tool_outputs", "cancel"], + beta.threads.runs.steps.Steps: ["retrieve", "list"], + } + + self.original_assistant_methods = { + (cls, method): getattr(cls, method) for cls, methods in assistant_api_methods.items() for method in methods + } + + # Override methods and verify + for (cls, method), original_func in self.original_assistant_methods.items(): + patched_function = create_patched_function(original_func) + setattr(cls, method, patched_function) + def undo_override(self): if self.original_create is not None and self.original_create_async is not None: from openai.resources.chat import completions completions.AsyncCompletions.create = self.original_create_async completions.Completions.create = self.original_create + + if self.original_assistant_methods is not None: + for (cls, method), original in self.original_assistant_methods.items(): + setattr(cls, method, original) diff --git a/agentops/llms/tracker.py b/agentops/llms/tracker.py index 615e3dfac..3609354f5 100644 --- a/agentops/llms/tracker.py +++ b/agentops/llms/tracker.py @@ -26,7 +26,41 @@ class LlmTracker: SUPPORTED_APIS = { "litellm": {"1.3.1": ("openai_chat_completions.completion",)}, "openai": { - "1.0.0": ("chat.completions.create",), + "1.0.0": ( + "chat.completions.create", + # Assistants + "beta.assistants.create", + "beta.assistants.retrieve", + "beta.assistants.update", + "beta.assistants.delete", + "beta.assistants.list", + "beta.assistants.files.create", + "beta.assistants.files.retrieve", + "beta.assistants.files.delete", + "beta.assistants.files.list", + # Threads + "beta.threads.create", + "beta.threads.retrieve", + "beta.threads.update", + "beta.threads.delete", + # Messages + "beta.threads.messages.create", + "beta.threads.messages.retrieve", + "beta.threads.messages.update", + "beta.threads.messages.list", + "beta.threads.messages.files.retrieve", + "beta.threads.messages.files.list", + # Runs + "beta.threads.runs.create", + "beta.threads.runs.retrieve", + "beta.threads.runs.update", + "beta.threads.runs.list", + "beta.threads.runs.cancel", + "beta.threads.runs.submit_tool_outputs", + # Run Steps + "beta.threads.runs.steps.Steps.retrieve", + "beta.threads.runs.steps.Steps.list", + ), "0.0.0": ( "ChatCompletion.create", "ChatCompletion.acreate", diff --git a/docs/v1/examples/examples.mdx b/docs/v1/examples/examples.mdx index c67d1def4..64b07b016 100644 --- a/docs/v1/examples/examples.mdx +++ b/docs/v1/examples/examples.mdx @@ -20,6 +20,10 @@ mode: "wide" Manage multiple sessions at the same time + + } iconType="image" href="/v1/integrations/openai" href="/v1/examples/openai_assistants"> + Observe OpenAI Assistants + ### Integration Examples diff --git a/docs/v1/examples/openai_assistants.mdx b/docs/v1/examples/openai_assistants.mdx new file mode 100644 index 000000000..77e162909 --- /dev/null +++ b/docs/v1/examples/openai_assistants.mdx @@ -0,0 +1,519 @@ +--- +title: 'OpenAI Assistants' +description: 'Learn how to use OpenAI Assistants API with AgentOps' +mode: "wide" +--- +_View Notebook on Github_ + + +{/* SOURCE_FILE: examples/openai_examples/openai_assistants_example.ipynb */} + +# Assistants API Overview with AgentOps + +This notebook has been adapted from this OpenAI Cookbook [example](https://cookbook.openai.com/examples/assistants_api_overview_python). + +The new [Assistants API](https://platform.openai.com/docs/assistants/overview) is a stateful evolution of our [Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) meant to simplify the creation of assistant-like experiences, and enable developer access to powerful tools like Code Interpreter and Retrieval. + +![Assistants API Diagram](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_diagram.png) + +## Chat Completions API vs Assistants API + +The primitives of the **Chat Completions API** are `Messages`, on which you perform a `Completion` with a `Model` (`gpt-3.5-turbo`, `gpt-4`, etc). It is lightweight and powerful, but inherently stateless, which means you have to manage conversation state, tool definitions, retrieval documents, and code execution manually. + +The primitives of the **Assistants API** are + +- `Assistants`, which encapsulate a base model, instructions, tools, and (context) documents, +- `Threads`, which represent the state of a conversation, and +- `Runs`, which power the execution of an `Assistant` on a `Thread`, including textual responses and multi-step tool use. + +We'll take a look at how these can be used to create powerful, stateful experiences. + +## Setup + +> **Note** +> The Assistants API is currently in beta so the latest [Python SDK](https://github.com/openai/openai-python) is needed (`1.58.1` at time of writing) for this example. + +```bash +%pip install -U openai +%pip install -U agentops +%pip install -U python-dotenv +``` + +### Pretty Printing Helper + +```python +import json + +def show_json(obj): + display(json.loads(obj.model_dump_json())) +``` + +## Complete Example with Assistants API + +### Assistants + +The easiest way to get started with the Assistants API is through the [Assistants Playground](https://platform.openai.com/playground). + +![Assistants Playground](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_assistants_playground.png) + +Let's begin by creating an assistant! We'll create a Math Tutor just like in our [docs](https://platform.openai.com/docs/assistants/overview). + +![Creating New Assistant](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_new_assistant.png) + +You can view Assistants you've created in the [Assistants Dashboard](https://platform.openai.com/assistants). + +![Assistants Dashboard](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_assistants_dashboard.png) + +You can also create Assistants directly through the Assistants API. But we need to have the AgentOps and OpenAI API keys first. + +You can get your OpenAI API key from the [OpenAI Dashboard](https://platform.openai.com/api-keys). + +To obtain the AgentOps API key, signup for an account on [AgentOps](https://agentops.ai/) and create a project. After creating the project, you can now create an API key in the [Project Settings](https://app.agentops.ai/settings/projects). + +Next, we'll set our API keys. There are several ways to do this, the code below is just the most foolproof way for the purposes of this notebook. It accounts for both users who use environment variables and those who just want to set the API Key here in this notebook. + +1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or... + +2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo! + +Now we are all set! Let's import the necessary libraries and initialize the AgentOps and OpenAI clients. + +```python +from openai import OpenAI +import agentops +from dotenv import load_dotenv +import os + +load_dotenv() +AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") or "" +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or "" +``` + +```python +agentops.init(api_key=AGENTOPS_API_KEY, default_tags=["openai", "beta-assistants"]) +client = OpenAI(api_key=OPENAI_API_KEY) +``` + +Next, we'll create an Assistant which will be our Math Tutor. + +```python +assistant = client.beta.assistants.create( + name="Math Tutor", + instructions="You are a personal math tutor. Answer questions briefly, in a sentence or less.", + model="gpt-4o-mini", +) +show_json(assistant) +``` + +Regardless of whether you create your Assistant through the Dashboard or with the API, you'll want to keep track of the Assistant ID. This is how you'll refer to your Assistant throughout Threads and Runs. + +Next, we'll create a new Thread and add a Message to it. This will hold the state of our conversation, so we don't have re-send the entire message history each time. + +### Threads + +Create a new thread: + +```python +thread = client.beta.threads.create() +show_json(thread) +``` + +Then add the Message to the thread: + +```python +message = client.beta.threads.messages.create( + thread_id=thread.id, + role="user", + content="I need to solve the equation `3x + 11 = 14`. Can you help me?", +) +show_json(message) +``` + +> **Note** +> Even though you're no longer sending the entire history each time, you will still be charged for the tokens of the entire conversation history with each Run. + +### Runs + +Notice how the Thread we created is **not** associated with the Assistant we created earlier! Threads exist independently from Assistants, which may be different from what you'd expect if you've used ChatGPT (where a thread is tied to a model/GPT). + +To get a completion from an Assistant for a given Thread, we must create a Run. Creating a Run will indicate to an Assistant it should look at the messages in the Thread and take action: either by adding a single response, or using tools. + +> **Note** +> Runs are a key difference between the Assistants API and Chat Completions API. While in Chat Completions the model will only ever respond with a single message, in the Assistants API a Run may result in an Assistant using one or multiple tools, and potentially adding multiple messages to the Thread. + +To get our Assistant to respond to the user, let's create the Run. As mentioned earlier, you must specify _both_ the Assistant and the Thread. + +```python +run = client.beta.threads.runs.create( + thread_id=thread.id, + assistant_id=assistant.id, +) +show_json(run) +``` + +Unlike creating a completion in the Chat Completions API, **creating a Run is an asynchronous operation**. It will return immediately with the Run's metadata, which includes a `status` that will initially be set to `queued`. The `status` will be updated as the Assistant performs operations (like using tools and adding messages). + +To know when the Assistant has completed processing, we can poll the Run in a loop. (Support for streaming is coming soon!) While here we are only checking for a `queued` or `in_progress` status, in practice a Run may undergo a [variety of status changes](https://platform.openai.com/docs/api-reference/runs/object#runs/object-status) which you can choose to surface to the user. (These are called Steps, and will be covered later.) + +```python +import time + +def wait_on_run(run, thread): + while run.status == "queued" or run.status == "in_progress": + run = client.beta.threads.runs.retrieve( + thread_id=thread.id, + run_id=run.id, + ) + time.sleep(0.5) + return run +``` + +```python +run = wait_on_run(run, thread) +show_json(run) +``` + +### Messages + +Now that the Run has completed, we can list the Messages in the Thread to see what got added by the Assistant. + +```python +messages = client.beta.threads.messages.list(thread_id=thread.id) +show_json(messages) +``` + +As you can see, Messages are ordered in reverse-chronological order – this was done so the most recent results are always on the first `page` (since results can be paginated). Do keep a look out for this, since this is the opposite order to messages in the Chat Completions API. + +Let's ask our Assistant to explain the result a bit further! + +```python +# Create a message to append to our thread +message = client.beta.threads.messages.create( + thread_id=thread.id, role="user", content="Could you explain this to me?" +) + +# Execute our run +run = client.beta.threads.runs.create( + thread_id=thread.id, + assistant_id=assistant.id, +) + +# Wait for completion +wait_on_run(run, thread) + +# Retrieve all the messages added after our last user message +messages = client.beta.threads.messages.list( + thread_id=thread.id, order="asc", after=message.id +) +show_json(messages) +``` + +This may feel like a lot of steps to get a response back, especially for this simple example. However, you'll soon see how we can add very powerful functionality to our Assistant without changing much code at all! + +Et voilà! + +You may have noticed that this code is not actually specific to our math Assistant at all... this code will work for any new Assistant you create simply by changing the Assistant ID! That is the power of the Assistants API. + +## Tools + +A key feature of the Assistants API is the ability to equip our Assistants with Tools, like Code Interpreter, Retrieval, and custom Functions. Let's take a look at each. + +### Code Interpreter + +Let's equip our Math Tutor with the [Code Interpreter](https://platform.openai.com/docs/assistants/tools/code-interpreter) tool, which we can do from the Dashboard... + +![Enabling code interpreter](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_code_interpreter.png) + +...or the API, using the Assistant ID. + +```python +assistant = client.beta.assistants.update( + MATH_ASSISTANT_ID, + tools=[{"type": "code_interpreter"}], +) +show_json(assistant) +``` + +Now, let's ask the Assistant to use its new tool. + +```python +thread, run = create_thread_and_run( + "Generate the first 20 fibbonaci numbers with code." +) +run = wait_on_run(run, thread) +pretty_print(get_response(thread)) +``` + +And that's it! The Assistant used Code Interpreter in the background, and gave us a final response. + +For some use cases this may be enough – however, if we want more details on what precisely an Assistant is doing we can take a look at a Run's Steps. + +### Steps + +A Run is composed of one or more Steps. Like a Run, each Step has a `status` that you can query. This is useful for surfacing the progress of a Step to a user (e.g. a spinner while the Assistant is writing code or performing retrieval). + +```python +run_steps = client.beta.threads.runs.steps.list( + thread_id=thread.id, run_id=run.id, order="asc" +) +``` + +Let's take a look at each Step's `step_details`. + +```python +for step in run_steps.data: + step_details = step.step_details + print(json.dumps(show_json(step_details), indent=4)) +``` + +We can see the `step_details` for two Steps: + +1. `tool_calls` (plural, since it could be more than one in a single Step) +2. `message_creation` + +The first Step is a `tool_calls`, specifically using the `code_interpreter` which contains: + +- `input`, which was the Python code generated before the tool was called, and +- `output`, which was the result of running the Code Interpreter. + +The second Step is a `message_creation`, which contains the `message` that was added to the Thread to communicate the results to the user. + +### Retrieval + +Another powerful tool in the Assistants API is [Retrieval](https://platform.openai.com/docs/assistants/tools/knowledge-retrieval): the ability to upload files that the Assistant will use as a knowledge base when answering questions. This can also be enabled from the Dashboard or the API, where we can upload files we want to be used. + +![Enabling retrieval](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_retrieval.png) + +```python +# Upload the file +file = client.files.create( + file=open( + "language_models_are_unsupervised_multitask_learners.pdf", + "rb", + ), + purpose="assistants", +) +# Update Assistant +assistant = client.beta.assistants.update( + MATH_ASSISTANT_ID, + tools=[{"type": "code_interpreter"}], + tool_resources={"code_interpreter": {"file_ids": [file.id]}}, +) +show_json(assistant) +``` + +```python +thread, run = create_thread_and_run( + "What are some cool math concepts behind this ML paper pdf? Explain in two sentences." +) +run = wait_on_run(run, thread) +pretty_print(get_response(thread)) +``` + +> **Note** +> There are more intricacies in Retrieval, like [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages), which may be covered in another cookbook. + +### Functions + +As a final powerful tool for your Assistant, you can specify custom [Functions](https://platform.openai.com/docs/assistants/tools/function-calling) (much like the [Function Calling](https://platform.openai.com/docs/guides/function-calling) in the Chat Completions API). During a Run, the Assistant can then indicate it wants to call one or more functions you specified. You are then responsible for calling the Function, and providing the output back to the Assistant. + +Let's take a look at an example by defining a `display_quiz()` Function for our Math Tutor. + +This function will take a `title` and an array of `question`s, display the quiz, and get input from the user for each: + +- `title` +- `questions` + - `question_text` + - `question_type`: [`MULTIPLE_CHOICE`, `FREE_RESPONSE`] + - `choices`: ["choice 1", "choice 2", ...] + +Unfortunately I don't know how to get user input within a Python Notebook, so I'll be mocking out responses with `get_mock_response...`. This is where you'd get the user's actual input. + +```python +def get_mock_response_from_user_multiple_choice(): + return "a" + + +def get_mock_response_from_user_free_response(): + return "I don't know." + + +def display_quiz(title, questions): + print("Quiz:", title) + print() + responses = [] + + for q in questions: + print(q["question_text"]) + response = "" + + # If multiple choice, print options + if q["question_type"] == "MULTIPLE_CHOICE": + for i, choice in enumerate(q["choices"]): + print(f"{i}. {choice}") + response = get_mock_response_from_user_multiple_choice() + + # Otherwise, just get response + elif q["question_type"] == "FREE_RESPONSE": + response = get_mock_response_from_user_free_response() + + responses.append(response) + print() + + return responses +``` + +Here's what a sample quiz would look like: + +```python +responses = display_quiz( + "Sample Quiz", + [ + {"question_text": "What is your name?", "question_type": "FREE_RESPONSE"}, + { + "question_text": "What is your favorite color?", + "question_type": "MULTIPLE_CHOICE", + "choices": ["Red", "Blue", "Green", "Yellow"], + }, + ], +) +print("Responses:", responses) +``` + +Now, let's define the interface of this function in JSON format, so our Assistant can call it: + +```python +function_json = { + "name": "display_quiz", + "description": "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "questions": { + "type": "array", + "description": "An array of questions, each with a title and potentially options (if multiple choice).", + "items": { + "type": "object", + "properties": { + "question_text": {"type": "string"}, + "question_type": { + "type": "string", + "enum": ["MULTIPLE_CHOICE", "FREE_RESPONSE"], + }, + "choices": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["question_text"], + }, + }, + }, + "required": ["title", "questions"], + }, +} +``` + +Once again, let's update our Assistant either through the Dashboard or the API. + +![Enabling custom function](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_function.png) + +> **Note** +> Pasting the function JSON into the Dashboard was a bit finicky due to indentation, etc. I just asked ChatGPT to format my function the same as one of the examples on the Dashboard :). + +```python +assistant = client.beta.assistants.update( + MATH_ASSISTANT_ID, + tools=[ + {"type": "code_interpreter"}, + {"type": "function", "function": function_json}, + ], +) +show_json(assistant) +``` + +And now, we ask for a quiz. + +```python +thread, run = create_thread_and_run( + "Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses." +) +run = wait_on_run(run, thread) +run.status +``` + +Now, however, when we check the Run's `status` we see `requires_action`! Let's take a closer look. + +```python +show_json(run) +``` + +The `required_action` field indicates a Tool is waiting for us to run it and submit its output back to the Assistant. Specifically, the `display_quiz` function! Let's start by parsing the `name` and `arguments`. + +> **Note** +> While in this case we know there is only one Tool call, in practice the Assistant may choose to call multiple tools. + +```python +# Extract single tool call +tool_call = run.required_action.submit_tool_outputs.tool_calls[0] +name = tool_call.function.name +arguments = json.loads(tool_call.function.arguments) + +print("Function Name:", name) +print("Function Arguments:") +arguments +``` + +Now let's actually call our `display_quiz` function with the arguments provided by the Assistant: + +```python +responses = display_quiz(arguments["title"], arguments["questions"]) +print("Responses:", responses) +``` + +Great! (Remember these responses are the one's we mocked earlier. In reality, we'd be getting input from the back from this function call.) + +Now that we have our responses, let's submit them back to the Assistant. We'll need the `tool_call` ID, found in the `tool_call` we parsed out earlier. We'll also need to encode our `list`of responses into a `str`. + +```python +tool_outputs = [] +tool_calls = run.required_action.submit_tool_outputs.tool_calls + +for tool_call in tool_calls: + arguments = json.loads(tool_call.function.arguments) + responses = display_quiz(arguments["title"], arguments["questions"]) + tool_outputs.append({ + "tool_call_id": tool_call.id, + "output": json.dumps(responses), + }) +``` + +```python +run = client.beta.threads.runs.submit_tool_outputs( + thread_id=thread.id, + run_id=run.id, + tool_outputs=tool_outputs +) +show_json(run) +``` + +We can now wait for the Run to complete once again, and check our Thread! + +```python +run = wait_on_run(run, thread) +pretty_print(get_response(thread)) +``` + +Now let's end the AgentOps session. By default, AgentOps will end the session in the "Intedeterminate" state. You can also end the session in the "Success" or "Failure" state. + +We will end the session in the "Success" state. + +```python +agentops.end_session(end_state="Success") +``` + +Woohoo 🎉 + +## Conclusion + +We covered the basics of the Assistants API using OpenAI's Python SDK and AgentOps for observability. + +For more information, check out the Assistants API deep [deep dive](https://platform.openai.com/docs/assistants/deep-dive) guide and its [documentation](https://platform.openai.com/docs/api-reference/assistants). diff --git a/docs/v1/integrations/openai.mdx b/docs/v1/integrations/openai.mdx index ba5ec54b1..6adf3fe49 100644 --- a/docs/v1/integrations/openai.mdx +++ b/docs/v1/integrations/openai.mdx @@ -186,6 +186,10 @@ Explore the [OpenAI API](https://platform.openai.com/) for more information. asyncio.run(main()) ``` +### Assistants example + +You can find the example in the [Assistants](/v1/examples/openai_assistants) section. + diff --git a/examples/openai_examples/images/assistants_overview_assistants_dashboard.png b/examples/openai_examples/images/assistants_overview_assistants_dashboard.png new file mode 100644 index 000000000..dff577c19 Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_assistants_dashboard.png differ diff --git a/examples/openai_examples/images/assistants_overview_assistants_playground.png b/examples/openai_examples/images/assistants_overview_assistants_playground.png new file mode 100644 index 000000000..b55af212a Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_assistants_playground.png differ diff --git a/examples/openai_examples/images/assistants_overview_diagram.png b/examples/openai_examples/images/assistants_overview_diagram.png new file mode 100644 index 000000000..3ac068a73 Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_diagram.png differ diff --git a/examples/openai_examples/images/assistants_overview_enable_code_interpreter.png b/examples/openai_examples/images/assistants_overview_enable_code_interpreter.png new file mode 100644 index 000000000..81d498da2 Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_enable_code_interpreter.png differ diff --git a/examples/openai_examples/images/assistants_overview_enable_function.png b/examples/openai_examples/images/assistants_overview_enable_function.png new file mode 100644 index 000000000..b17aa3066 Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_enable_function.png differ diff --git a/examples/openai_examples/images/assistants_overview_enable_retrieval.png b/examples/openai_examples/images/assistants_overview_enable_retrieval.png new file mode 100644 index 000000000..fd6684f1b Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_enable_retrieval.png differ diff --git a/examples/openai_examples/images/assistants_overview_new_assistant.png b/examples/openai_examples/images/assistants_overview_new_assistant.png new file mode 100644 index 000000000..318f860cd Binary files /dev/null and b/examples/openai_examples/images/assistants_overview_new_assistant.png differ diff --git a/examples/openai_examples/language_models_are_unsupervised_multitask_learners.pdf b/examples/openai_examples/language_models_are_unsupervised_multitask_learners.pdf new file mode 100644 index 000000000..b714945e2 Binary files /dev/null and b/examples/openai_examples/language_models_are_unsupervised_multitask_learners.pdf differ diff --git a/examples/openai_examples/openai_assistants_example.ipynb b/examples/openai_examples/openai_assistants_example.ipynb new file mode 100644 index 000000000..b9e10f02b --- /dev/null +++ b/examples/openai_examples/openai_assistants_example.ipynb @@ -0,0 +1,1108 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Assistants API Overview with AgentOps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook has been adapted from this OpenAI Cookbook [example](https://cookbook.openai.com/examples/assistants_api_overview_python)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The new [Assistants API](https://platform.openai.com/docs/assistants/overview) is a stateful evolution of our [Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) meant to simplify the creation of assistant-like experiences, and enable developer access to powerful tools like Code Interpreter and Retrieval." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants API Diagram](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_diagram.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat Completions API vs Assistants API\n", + "\n", + "The primitives of the **Chat Completions API** are `Messages`, on which you perform a `Completion` with a `Model` (`gpt-3.5-turbo`, `gpt-4`, etc). It is lightweight and powerful, but inherently stateless, which means you have to manage conversation state, tool definitions, retrieval documents, and code execution manually.\n", + "\n", + "The primitives of the **Assistants API** are\n", + "\n", + "- `Assistants`, which encapsulate a base model, instructions, tools, and (context) documents,\n", + "- `Threads`, which represent the state of a conversation, and\n", + "- `Runs`, which power the execution of an `Assistant` on a `Thread`, including textual responses and multi-step tool use.\n", + "\n", + "We'll take a look at how these can be used to create powerful, stateful experiences.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "> **Note**\n", + "> The Assistants API is currently in beta so the latest [Python SDK](https://github.com/openai/openai-python) is needed (`1.58.1` at time of writing) for this example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -U openai\n", + "%pip install -U agentops\n", + "%pip install -U python-dotenv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pretty Printing Helper\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "def show_json(obj):\n", + " display(json.loads(obj.model_dump_json()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Complete Example with Assistants API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Assistants\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The easiest way to get started with the Assistants API is through the [Assistants Playground](https://platform.openai.com/playground).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants Playground](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_assistants_playground.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's begin by creating an assistant! We'll create a Math Tutor just like in our [docs](https://platform.openai.com/docs/assistants/overview).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Creating New Assistant](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_new_assistant.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can view Assistants you've created in the [Assistants Dashboard](https://platform.openai.com/assistants).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants Dashboard](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_assistants_dashboard.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also create Assistants directly through the Assistants API. But we need to have the AgentOps and OpenAI API keys first.\n", + "\n", + "You can get your OpenAI API key from the [OpenAI Dashboard](https://platform.openai.com/api-keys).\n", + "\n", + "To obtain the AgentOps API key, signup for an account on [AgentOps](https://agentops.ai/) and create a project. After creating the project, you can now create an API key in the [Project Settings](https://app.agentops.ai/settings/projects)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll set our API keys. There are several ways to do this, the code below is just the most foolproof way for the purposes of this notebook. It accounts for both users who use environment variables and those who just want to set the API Key here in this notebook.\n", + "\n", + "1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or...\n", + "\n", + "2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are all set! Let's import the necessary libraries and initialize the AgentOps and OpenAI clients." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "import agentops\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "load_dotenv()\n", + "AGENTOPS_API_KEY = os.getenv(\"AGENTOPS_API_KEY\") or \"\"\n", + "OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\") or \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agentops.init(api_key=AGENTOPS_API_KEY, default_tags=[\"openai\", \"beta-assistants\"])\n", + "client = OpenAI(api_key=OPENAI_API_KEY)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll create an Assistant which will be our Math Tutor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assistant = client.beta.assistants.create(\n", + " name=\"Math Tutor\",\n", + " instructions=\"You are a personal math tutor. Answer questions briefly, in a sentence or less.\",\n", + " model=\"gpt-4o-mini\",\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Regardless of whether you create your Assistant through the Dashboard or with the API, you'll want to keep track of the Assistant ID. This is how you'll refer to your Assistant throughout Threads and Runs.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll create a new Thread and add a Message to it. This will hold the state of our conversation, so we don't have re-send the entire message history each time.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Threads\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a new thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "thread = client.beta.threads.create()\n", + "show_json(thread)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then add the Message to the thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id,\n", + " role=\"user\",\n", + " content=\"I need to solve the equation `3x + 11 = 14`. Can you help me?\",\n", + ")\n", + "show_json(message)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Even though you're no longer sending the entire history each time, you will still be charged for the tokens of the entire conversation history with each Run.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Runs\n", + "\n", + "Notice how the Thread we created is **not** associated with the Assistant we created earlier! Threads exist independently from Assistants, which may be different from what you'd expect if you've used ChatGPT (where a thread is tied to a model/GPT).\n", + "\n", + "To get a completion from an Assistant for a given Thread, we must create a Run. Creating a Run will indicate to an Assistant it should look at the messages in the Thread and take action: either by adding a single response, or using tools.\n", + "\n", + "> **Note**\n", + "> Runs are a key difference between the Assistants API and Chat Completions API. While in Chat Completions the model will only ever respond with a single message, in the Assistants API a Run may result in an Assistant using one or multiple tools, and potentially adding multiple messages to the Thread.\n", + "\n", + "To get our Assistant to respond to the user, let's create the Run. As mentioned earlier, you must specify _both_ the Assistant and the Thread.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unlike creating a completion in the Chat Completions API, **creating a Run is an asynchronous operation**. It will return immediately with the Run's metadata, which includes a `status` that will initially be set to `queued`. The `status` will be updated as the Assistant performs operations (like using tools and adding messages).\n", + "\n", + "To know when the Assistant has completed processing, we can poll the Run in a loop. (Support for streaming is coming soon!) While here we are only checking for a `queued` or `in_progress` status, in practice a Run may undergo a [variety of status changes](https://platform.openai.com/docs/api-reference/runs/object#runs/object-status) which you can choose to surface to the user. (These are called Steps, and will be covered later.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = wait_on_run(run, thread)\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Messages\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the Run has completed, we can list the Messages in the Thread to see what got added by the Assistant.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "messages = client.beta.threads.messages.list(thread_id=thread.id)\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, Messages are ordered in reverse-chronological order – this was done so the most recent results are always on the first `page` (since results can be paginated). Do keep a look out for this, since this is the opposite order to messages in the Chat Completions API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's ask our Assistant to explain the result a bit further!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a message to append to our thread\n", + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=\"Could you explain this to me?\"\n", + ")\n", + "\n", + "# Execute our run\n", + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "\n", + "# Wait for completion\n", + "wait_on_run(run, thread)\n", + "\n", + "# Retrieve all the messages added after our last user message\n", + "messages = client.beta.threads.messages.list(\n", + " thread_id=thread.id, order=\"asc\", after=message.id\n", + ")\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This may feel like a lot of steps to get a response back, especially for this simple example. However, you'll soon see how we can add very powerful functionality to our Assistant without changing much code at all!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at how we could potentially put all of this together. Below is all the code you need to use an Assistant you've created.\n", + "\n", + "Since we've already created our Math Assistant, I've saved its ID in `MATH_ASSISTANT_ID`. I then defined two functions:\n", + "\n", + "- `submit_message`: create a Message on a Thread, then start (and return) a new Run\n", + "- `get_response`: returns the list of Messages in a Thread\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "\n", + "MATH_ASSISTANT_ID = assistant.id # or a hard-coded ID like \"asst-...\"\n", + "\n", + "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", + "\n", + "def submit_message(assistant_id, thread, user_message):\n", + " client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=user_message\n", + " )\n", + " return client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant_id,\n", + " )\n", + "\n", + "\n", + "def get_response(thread):\n", + " return client.beta.threads.messages.list(thread_id=thread.id, order=\"asc\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I've also defined a `create_thread_and_run` function that I can re-use (which is actually almost identical to the [`client.beta.threads.create_and_run`](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun) compound function in our API ;) ). Finally, we can submit our mock user requests each to a new Thread.\n", + "\n", + "Notice how all of these API calls are asynchronous operations; this means we actually get async behavior in our code without the use of async libraries! (e.g. `asyncio`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def create_thread_and_run(user_input):\n", + " thread = client.beta.threads.create()\n", + " run = submit_message(MATH_ASSISTANT_ID, thread, user_input)\n", + " return thread, run\n", + "\n", + "\n", + "# Emulating concurrent user requests\n", + "thread1, run1 = create_thread_and_run(\n", + " \"I need to solve the equation `3x + 11 = 14`. Can you help me?\"\n", + ")\n", + "thread2, run2 = create_thread_and_run(\"Could you explain linear algebra to me?\")\n", + "thread3, run3 = create_thread_and_run(\"I don't like math. What can I do?\")\n", + "\n", + "# Now all Runs are executing..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once all Runs are going, we can wait on each and get the responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "# Pretty printing helper\n", + "def pretty_print(messages):\n", + " print(\"# Messages\")\n", + " for m in messages:\n", + " print(f\"{m.role}: {m.content[0].text.value}\")\n", + " print()\n", + "\n", + "\n", + "# Waiting in a loop\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run\n", + "\n", + "\n", + "# Wait for Run 1\n", + "run1 = wait_on_run(run1, thread1)\n", + "pretty_print(get_response(thread1))\n", + "\n", + "# Wait for Run 2\n", + "run2 = wait_on_run(run2, thread2)\n", + "pretty_print(get_response(thread2))\n", + "\n", + "# Wait for Run 3\n", + "run3 = wait_on_run(run3, thread3)\n", + "pretty_print(get_response(thread3))\n", + "\n", + "# Thank our assistant on Thread 3 :)\n", + "run4 = submit_message(MATH_ASSISTANT_ID, thread3, \"Thank you!\")\n", + "run4 = wait_on_run(run4, thread3)\n", + "pretty_print(get_response(thread3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Et voilà!\n", + "\n", + "You may have noticed that this code is not actually specific to our math Assistant at all... this code will work for any new Assistant you create simply by changing the Assistant ID! That is the power of the Assistants API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tools\n", + "\n", + "A key feature of the Assistants API is the ability to equip our Assistants with Tools, like Code Interpreter, Retrieval, and custom Functions. Let's take a look at each.\n", + "\n", + "### Code Interpreter\n", + "\n", + "Let's equip our Math Tutor with the [Code Interpreter](https://platform.openai.com/docs/assistants/tools/code-interpreter) tool, which we can do from the Dashboard...\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling code interpreter](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_code_interpreter.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "...or the API, using the Assistant ID.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's ask the Assistant to use its new tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Generate the first 20 fibbonaci numbers with code.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And that's it! The Assistant used Code Interpreter in the background, and gave us a final response.\n", + "\n", + "For some use cases this may be enough – however, if we want more details on what precisely an Assistant is doing we can take a look at a Run's Steps.\n", + "\n", + "### Steps\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A Run is composed of one or more Steps. Like a Run, each Step has a `status` that you can query. This is useful for surfacing the progress of a Step to a user (e.g. a spinner while the Assistant is writing code or performing retrieval).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "run_steps = client.beta.threads.runs.steps.list(\n", + " thread_id=thread.id, run_id=run.id, order=\"asc\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at each Step's `step_details`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for step in run_steps.data:\n", + " step_details = step.step_details\n", + " print(json.dumps(show_json(step_details), indent=4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the `step_details` for two Steps:\n", + "\n", + "1. `tool_calls` (plural, since it could be more than one in a single Step)\n", + "2. `message_creation`\n", + "\n", + "The first Step is a `tool_calls`, specifically using the `code_interpreter` which contains:\n", + "\n", + "- `input`, which was the Python code generated before the tool was called, and\n", + "- `output`, which was the result of running the Code Interpreter.\n", + "\n", + "The second Step is a `message_creation`, which contains the `message` that was added to the Thread to communicate the results to the user.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Retrieval\n", + "\n", + "Another powerful tool in the Assistants API is [Retrieval](https://platform.openai.com/docs/assistants/tools/knowledge-retrieval): the ability to upload files that the Assistant will use as a knowledge base when answering questions. This can also be enabled from the Dashboard or the API, where we can upload files we want to be used.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling retrieval](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_retrieval.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload the file\n", + "file = client.files.create(\n", + " file=open(\n", + " \"language_models_are_unsupervised_multitask_learners.pdf\",\n", + " \"rb\",\n", + " ),\n", + " purpose=\"assistants\",\n", + ")\n", + "# Update Assistant\n", + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}],\n", + " tool_resources={\"code_interpreter\": {\"file_ids\": [file.id]}},\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"What are some cool math concepts behind this ML paper pdf? Explain in two sentences.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> There are more intricacies in Retrieval, like [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages), which may be covered in another cookbook.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Functions\n", + "\n", + "As a final powerful tool for your Assistant, you can specify custom [Functions](https://platform.openai.com/docs/assistants/tools/function-calling) (much like the [Function Calling](https://platform.openai.com/docs/guides/function-calling) in the Chat Completions API). During a Run, the Assistant can then indicate it wants to call one or more functions you specified. You are then responsible for calling the Function, and providing the output back to the Assistant.\n", + "\n", + "Let's take a look at an example by defining a `display_quiz()` Function for our Math Tutor.\n", + "\n", + "This function will take a `title` and an array of `question`s, display the quiz, and get input from the user for each:\n", + "\n", + "- `title`\n", + "- `questions`\n", + " - `question_text`\n", + " - `question_type`: [`MULTIPLE_CHOICE`, `FREE_RESPONSE`]\n", + " - `choices`: [\"choice 1\", \"choice 2\", ...]\n", + "\n", + "Unfortunately I don't know how to get user input within a Python Notebook, so I'll be mocking out responses with `get_mock_response...`. This is where you'd get the user's actual input.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "def get_mock_response_from_user_multiple_choice():\n", + " return \"a\"\n", + "\n", + "\n", + "def get_mock_response_from_user_free_response():\n", + " return \"I don't know.\"\n", + "\n", + "\n", + "def display_quiz(title, questions):\n", + " print(\"Quiz:\", title)\n", + " print()\n", + " responses = []\n", + "\n", + " for q in questions:\n", + " print(q[\"question_text\"])\n", + " response = \"\"\n", + "\n", + " # If multiple choice, print options\n", + " if q[\"question_type\"] == \"MULTIPLE_CHOICE\":\n", + " for i, choice in enumerate(q[\"choices\"]):\n", + " print(f\"{i}. {choice}\")\n", + " response = get_mock_response_from_user_multiple_choice()\n", + "\n", + " # Otherwise, just get response\n", + " elif q[\"question_type\"] == \"FREE_RESPONSE\":\n", + " response = get_mock_response_from_user_free_response()\n", + "\n", + " responses.append(response)\n", + " print()\n", + "\n", + " return responses" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's what a sample quiz would look like:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "responses = display_quiz(\n", + " \"Sample Quiz\",\n", + " [\n", + " {\"question_text\": \"What is your name?\", \"question_type\": \"FREE_RESPONSE\"},\n", + " {\n", + " \"question_text\": \"What is your favorite color?\",\n", + " \"question_type\": \"MULTIPLE_CHOICE\",\n", + " \"choices\": [\"Red\", \"Blue\", \"Green\", \"Yellow\"],\n", + " },\n", + " ],\n", + ")\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's define the interface of this function in JSON format, so our Assistant can call it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "function_json = {\n", + " \"name\": \"display_quiz\",\n", + " \"description\": \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"title\": {\"type\": \"string\"},\n", + " \"questions\": {\n", + " \"type\": \"array\",\n", + " \"description\": \"An array of questions, each with a title and potentially options (if multiple choice).\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"question_text\": {\"type\": \"string\"},\n", + " \"question_type\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"MULTIPLE_CHOICE\", \"FREE_RESPONSE\"],\n", + " },\n", + " \"choices\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n", + " },\n", + " \"required\": [\"question_text\"],\n", + " },\n", + " },\n", + " },\n", + " \"required\": [\"title\", \"questions\"],\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once again, let's update our Assistant either through the Dashboard or the API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling custom function](https://github.com/AgentOps-AI/agentops/blob/main/examples/openai_examples/images/assistants_overview_enable_function.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Pasting the function JSON into the Dashboard was a bit finicky due to indentation, etc. I just asked ChatGPT to format my function the same as one of the examples on the Dashboard :).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[\n", + " {\"type\": \"code_interpreter\"},\n", + " {\"type\": \"function\", \"function\": function_json},\n", + " ],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, we ask for a quiz.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "run.status" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, however, when we check the Run's `status` we see `requires_action`! Let's take a closer look.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `required_action` field indicates a Tool is waiting for us to run it and submit its output back to the Assistant. Specifically, the `display_quiz` function! Let's start by parsing the `name` and `arguments`.\n", + "\n", + "> **Note**\n", + "> While in this case we know there is only one Tool call, in practice the Assistant may choose to call multiple tools.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Extract single tool call\n", + "tool_call = run.required_action.submit_tool_outputs.tool_calls[0]\n", + "name = tool_call.function.name\n", + "arguments = json.loads(tool_call.function.arguments)\n", + "\n", + "print(\"Function Name:\", name)\n", + "print(\"Function Arguments:\")\n", + "arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's actually call our `display_quiz` function with the arguments provided by the Assistant:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "responses = display_quiz(arguments[\"title\"], arguments[\"questions\"])\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! (Remember these responses are the one's we mocked earlier. In reality, we'd be getting input from the back from this function call.)\n", + "\n", + "Now that we have our responses, let's submit them back to the Assistant. We'll need the `tool_call` ID, found in the `tool_call` we parsed out earlier. We'll also need to encode our `list`of responses into a `str`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tool_outputs = []\n", + "tool_calls = run.required_action.submit_tool_outputs.tool_calls\n", + "\n", + "for tool_call in tool_calls:\n", + " arguments = json.loads(tool_call.function.arguments)\n", + " responses = display_quiz(arguments[\"title\"], arguments[\"questions\"])\n", + " tool_outputs.append({\n", + " \"tool_call_id\": tool_call.id,\n", + " \"output\": json.dumps(responses),\n", + " })" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = client.beta.threads.runs.submit_tool_outputs(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " tool_outputs=tool_outputs\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now wait for the Run to complete once again, and check our Thread!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's end the AgentOps session. By default, AgentOps will end the session in the \"Intedeterminate\" state. You can also end the session in the \"Success\" or \"Failure\" state.\n", + "\n", + "We will end the session in the \"Success\" state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agentops.end_session(end_state=\"Success\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Woohoo 🎉" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "We covered the basics of the Assistants API using OpenAI's Python SDK and AgentOps for observability.\n", + "\n", + "For more information, check out the Assistants API deep [deep dive](https://platform.openai.com/docs/assistants/deep-dive) guide and its [documentation](https://platform.openai.com/docs/api-reference/assistants)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openai", + "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", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/core_manual_tests/providers/openai_assistants_canary.py b/tests/core_manual_tests/providers/openai_assistants_canary.py new file mode 100644 index 000000000..3cf843ba8 --- /dev/null +++ b/tests/core_manual_tests/providers/openai_assistants_canary.py @@ -0,0 +1,96 @@ +import agentops +from openai import OpenAI +from dotenv import load_dotenv + +load_dotenv() +agentops.init(default_tags=["openai-assistants-test"]) +openai = OpenAI() + +try: + # Basic Assistant Creation + assistant = openai.beta.assistants.create( + name="Math Tutor", + instructions="You are a personal math tutor. Write and run code to answer math questions.", + tools=[{"type": "code_interpreter"}], + model="gpt-4o-mini", + ) + print(f"\nCreated assistant: {assistant.id}") + print(f"Assistant name: {assistant.name}") + print(f"Assistant instructions: {assistant.instructions}") + print(f"Assistant model: {assistant.model}") + print(f"Assistant tools: {assistant.tools}") + + # Thread Creation and Message Handling + thread = openai.beta.threads.create() + print(f"\nCreated thread: {thread.id}") + + # Add Multiple Messages + message1 = openai.beta.threads.messages.create( + thread_id=thread.id, role="user", content="I need to solve the equation `3x + 11 = 14`. Can you help me?" + ) + message2 = openai.beta.threads.messages.create( + thread_id=thread.id, role="user", content="Also, what is the square root of 144?" + ) + print("\nAdded messages:") + print(f"Message 1 ID: {message1.id}") + print(f"Message 1 content: {message1.content[0].text.value}") + print(f"Message 2 ID: {message2.id}") + print(f"Message 2 content: {message2.content[0].text.value}") + + # Run Assistant with Multiple Questions + run = openai.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id) + print(f"\nStarted run: {run.id}") + + # Run Status Monitoring + while run.status not in ["completed", "failed"]: + run = openai.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id) + print(f"Run status: {run.status}") + + if run.status == "failed": + print("\nRun failed!") + if run.last_error: + print(f"Error code: {run.last_error.code}") + print(f"Error message: {run.last_error.message}") + + # Message Retrieval and Display + messages = openai.beta.threads.messages.list(thread_id=thread.id) + print("\nThread Messages:") + for msg in reversed(messages.data): + print(f"{msg.role.capitalize()}: {msg.content[0].text.value}") + + # Assistant Update + updated_assistant = openai.beta.assistants.update( + assistant.id, + name="Advanced Math Tutor", + instructions="You are an advanced math tutor. Explain concepts in detail.", + ) + print(f"\nUpdated assistant: {updated_assistant.id}") + print(f"New name: {updated_assistant.name}") + print(f"New instructions: {updated_assistant.instructions}") + + # Run Step Retrieval + run_steps = openai.beta.threads.runs.steps.list(thread_id=thread.id, run_id=run.id) + print("\nRun Steps:") + for step in run_steps.data: + print(f"Step ID: {step.id}") + print(f"Status: {step.status}") + if hasattr(step.step_details, "message_creation"): + print(f"Message ID: {step.step_details.message_creation.message_id}") + + # Thread Update + updated_thread = openai.beta.threads.update(thread.id, metadata={"test": "value"}) + print(f"\nUpdated thread: {updated_thread.id}") + + # Message Retrieval and Display + messages = openai.beta.threads.messages.list(thread_id=updated_thread.id) + print("\nThread Messages:") + for msg in reversed(messages.data): + print(f"{msg.role.capitalize()}: {msg.content[0].text.value}") + +finally: + # Clean up + if "assistant" in locals(): + openai.beta.assistants.delete(assistant.id) + print(f"\nDeleted assistant: {assistant.id}") + +agentops.end_session(end_state="Success")