Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added OpenAI Agents lab with Terraform #124

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions labs/openai-agents-tf/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# APIM ❤️ AI Agents

## [OpenAI Agents lab](openai-agents.ipynb) (with Terraform)

[![flow](../../images/openai-agents.gif)](openai-agents.ipynb)

Playground to try the [OpenAI Agents](https://openai.github.io/openai-agents-python/) with Azure OpenAI models and API based tools through Azure API Management. This enables limitless opportunities for AI agents while maintaining control through Azure API Management!

### Prerequisites

- [Python 3.12 or later version](https://www.python.org/) installed
- [VS Code](https://code.visualstudio.com/) installed with the [Jupyter notebook extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) enabled
- [Python environment](https://code.visualstudio.com/docs/python/environments#_creating-environments) with the [requirements.txt](../../requirements.txt) or run `pip install -r requirements.txt` in your terminal
- [An Azure Subscription](https://azure.microsoft.com/free/) with [Contributor](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#contributor) + [RBAC Administrator](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#role-based-access-control-administrator) or [Owner](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#owner) roles
- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and [Signed into your Azure subscription](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively)
- [Terraform CLI](https://learn.hashicorp.com/tutorials/terraform/install-cli) installed


▶️ Click `Run All` to execute all steps sequentially, or execute them `Step by Step`...

### 🚀 Get started

Proceed by opening the [Jupyter notebook](openai-agents-tf.ipynb), and follow the steps provided.

### 🗑️ Clean up resources

When you're finished with the lab, you should remove all your deployed resources from Azure to avoid extra charges and keep your Azure subscription uncluttered.
Use the [clean-up-resources notebook](clean-up-resources.ipynb) for that.
44 changes: 44 additions & 0 deletions labs/openai-agents-tf/city-weather-mock-policy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<policies>
<inbound>
<base />
<return-response>
<set-status code="200" />
<set-body>@{
var random = new Random();
double temperature;
var format = "Celsius";
var descriptions = new[] { "Clear skies", "Partly cloudy", "Overcast", "Rainy" };
var city = context.Request.MatchedParameters["city"];
switch (city.ToLower())
{
case "seattle":
case "new york city":
case "los angeles":
format = "Fahrenheit";
temperature = random.Next(14, 95) + random.NextDouble();
break;
default:
temperature = random.Next(-5, 35) + random.NextDouble();
break;
}
return new JObject(
new JProperty("city", city),
new JProperty("temperature", Math.Round(temperature, 1)),
new JProperty("temperature_format", format),
new JProperty("description", descriptions[random.Next(descriptions.Length)]),
new JProperty("humidity", random.Next(20, 100)),
new JProperty("wind_speed", Math.Round(random.NextDouble() * 10, 1))
).ToString();
}</set-body>
</return-response>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
120 changes: 120 additions & 0 deletions labs/openai-agents-tf/city-weather-openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"openapi": "3.0.1",
"info": {
"title": "City Weather API",
"description": "API to retrieve weather information, including the current temperature, for a specified city. Generated with GitHub Copilot and the following prompt - create an openapi file that retrieves weather information with the city query parameter and reply with temperature. Add examples to the specification.",
"version": "1.0"
},
"servers": [{
"url": "https://replace-me.local/weatherservice"
}],
"paths": {
"/weather": {
"get": {
"summary": "Get current weather information for a specified city",
"description": "Retrieves weather information, including the current temperature, for a specified city.",
"operationId": "get-weather-city-city",
"parameters": [{
"name": "city",
"in": "query",
"description": "Name of the city to retrieve weather information for",
"required": true,
"schema": {
"type": "string",
"example": "Tokyo"
}
}],
"responses": {
"200": {
"description": "Weather information for the specified city",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CityWeather"
},
"examples": {
"Tokyo": {
"summary": "Example response for Tokyo",
"value": {
"city": "Tokyo",
"temperature": 15.5,
"temperature_format": "Celsius",
"description": "Clear sky",
"humidity": 60,
"wind_speed": 5.5
}
},
"default": {
"value": {
"city": "New York City",
"temperature": 59,
"temperature_format": "Fahrenheit",
"description": "Partly cloudy",
"humidity": 70,
"wind_speed": 3.2
}
}
}
}
}
},
"400": {
"description": "Bad request"
},
"404": {
"description": "City not found"
},
"500": {
"description": "Internal server error"
}
}
}
}
},
"components": {
"schemas": {
"CityWeather": {
"type": "object",
"properties": {
"city": {
"type": "string",
"example": "Tokyo"
},
"temperature": {
"type": "number",
"format": "float",
"example": 15.5
},
"temperature_format": {
"type": "string",
"example": "Celsius"
},
"description": {
"type": "string",
"example": "Clear sky"
},
"humidity": {
"type": "number",
"format": "int32",
"example": 60
},
"wind_speed": {
"type": "number",
"format": "float",
"example": 5.5
}
}
}
},
"securitySchemes": {
"apiKeyHeader": {
"type": "apiKey",
"name": "api-key",
"in": "header"
}
}
},
"security": [{
"apiKeyHeader": []
}]
}
61 changes: 61 additions & 0 deletions labs/openai-agents-tf/clean-up-resources.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 🗑️ Clean up resources\n",
"\n",
"When you're finished with the lab, you should remove all your deployed resources from Azure to avoid extra charges and keep your Azure subscription uncluttered."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os, sys\n",
"sys.path.insert(1, '../../shared') # add the shared directory to the Python path\n",
"import utils\n",
"\n",
"output = utils.run(\"az account show\", \"Retrieved az account\", \"Failed to get the current az account\")\n",
"\n",
"subscription_id = ''\n",
"if output.success and output.json_data:\n",
" subscription_id = output.json_data['id']\n",
"\n",
"# Specify the target subscription for Terraform\n",
"os.environ['ARM_SUBSCRIPTION_ID'] = subscription_id\n",
"\n",
"# Intialize terraform\n",
"output = utils.run(\n",
" f\"terraform destroy -auto-approve\",\n",
" f\"Resources deletion succeeded\",\n",
" f\"Resources deletion failed\",\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.12.8"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
1 change: 1 addition & 0 deletions labs/openai-agents-tf/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "azurerm_client_config" "current" {}
1,809 changes: 1,809 additions & 0 deletions labs/openai-agents-tf/inference-openapi.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions labs/openai-agents-tf/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
locals {
location = var.location != null ? var.location : azurerm_resource_group.rg.location
resource_suffix = random_string.suffix.result
apim_logger_name = "apim-logger-${local.resource_suffix}"
log_settings = {
headers = ["Content-type", "User-agent", "x-ms-region", "x-ratelimit-remaining-tokens", "x-ratelimit-remaining-requests"]
body = { bytes = 8191 }
}
callback_url = azapi_resource_action.logic_app_callback.output["value"]
base_path = regex("^(https://[^?]+/triggers)(/|$)", local.callback_url)[0]
sig = regex("sig=([^&]+)", local.callback_url)[0]
api-version = "2016-10-01"
sp = regex("sp=([^&]+)", local.callback_url)[0]
sv = regex("sv=([^&]+)", local.callback_url)[0]
}
533 changes: 533 additions & 0 deletions labs/openai-agents-tf/main.tf

Large diffs are not rendered by default.

524 changes: 524 additions & 0 deletions labs/openai-agents-tf/openai-agents-tf.ipynb

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions labs/openai-agents-tf/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
output "application_insights_instrumentation_key" {
value = azurerm_application_insights.appinsights.instrumentation_key
sensitive = true
}

output "api_management_gateway_url" {
value = azapi_resource.apim.output.properties.gatewayUrl
}

output "openai_subscription_key" {
value = azurerm_api_management_subscription.openai-subscription.primary_key
sensitive = true
}

output "tools_subscription_key" {
value = azurerm_api_management_subscription.tools-apis-subscription.primary_key
sensitive = true
}
97 changes: 97 additions & 0 deletions labs/openai-agents-tf/place-order-openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"openapi": "3.0.1",
"info": {
"title": "Place Order API",
"description": "Place an Order to the specified sku and quantity.",
"version": "1.0"
},
"servers": [
{
"url": "https://replace-me.local/orderservice"
}
],
"security": [
{
"apiKeyHeader": []
}
],
"tags": [],
"paths": {
"/PlaceOrder/paths/invoke": {
"post": {
"summary": "PlaceOrder-invoke",
"description": "Place an Order to the specified sku and quantity.",
"operationId": "PlaceOrder-invoke",
"requestBody": {
"description": "The request body.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/request-PlaceOrder"
}
}
},
"required": false
},
"responses": {
"200": {
"description": "The Logic App Response.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlaceOrderPathsInvokePost200ApplicationJsonResponse"
},
"example": {}
}
}
},
"500": {
"description": "The Logic App Response.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlaceOrderPathsInvokePost500ApplicationJsonResponse"
},
"example": {}
}
}
}
},
"x-codegen-request-body-name": "request-PlaceOrder"
}
}
},
"components": {
"schemas": {
"request-PlaceOrder": {
"type": "object",
"properties": {
"sku": {
"type": "string"
},
"quantity": {
"type": "integer"
}
},
"example": {
"sku": "string",
"quantity": 0
}
},
"PlaceOrderPathsInvokePost200ApplicationJsonResponse": {
"type": "object"
},
"PlaceOrderPathsInvokePost500ApplicationJsonResponse": {
"type": "object"
}
},
"securitySchemes": {
"apiKeyHeader": {
"type": "apiKey",
"name": "api-key",
"in": "header"
}
}
},
"x-original-swagger-version": "2.0"
}
15 changes: 15 additions & 0 deletions labs/openai-agents-tf/place-order-policy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<policies>
<inbound>
<base />
<set-backend-service backend-id="orderworflow-backend" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
25 changes: 25 additions & 0 deletions labs/openai-agents-tf/policy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<policies>
<inbound>
<base />
<authentication-managed-identity resource="https://cognitiveservices.azure.com" output-token-variable-name="managed-id-access-token" ignore-error="false" />
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["managed-id-access-token"])</value>
</set-header>
<set-backend-service backend-id="{backend-id}" />
<azure-openai-emit-token-metric namespace="openai">
<dimension name="Subscription ID" value="@(context.Subscription.Id)" />
<dimension name="Client IP" value="@(context.Request.IpAddress)" />
<dimension name="API ID" value="@(context.Api.Id)" />
<dimension name="User ID" value="@(context.Request.Headers.GetValueOrDefault("x-user-id", "N/A"))" />
</azure-openai-emit-token-metric>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
44 changes: 44 additions & 0 deletions labs/openai-agents-tf/product-catalog-mock-policy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<policies>
<inbound>
<base />
<return-response>
<set-status code="200" />
<set-body>@{
var random = new Random();
var names = new[] { "N/A" };
var skus = new[] { "SKU-1234", "SKU-5678", "SKU-4321", "SKU-8765" };
var storeLocations = new[] { "Lisbon", "Seattle", "London", "Madrid" };
var category = context.Request.MatchedParameters["category"];
switch (category.ToLower())
{
case "electronics":
names = new[] { "Smartphone", "Tablet", "Laptop", "Smartwatch" };
break;
case "appliances":
names = new[] { "Refrigerator", "Washing Machine", "Microwave", "Dishwasher" };
break;
case "clothing":
names = new[] { "T-shirt", "Jeans", "Jacket", "Sneakers" };
break;
}

return new JObject(
new JProperty("name", names[random.Next(names.Length)]),
new JProperty("category", category),
new JProperty("sku", skus[random.Next(skus.Length)]),
new JProperty("stock", random.Next(1, 100)),
new JProperty("store_location", storeLocations[random.Next(storeLocations.Length)])
).ToString();
}</set-body>
</return-response>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
113 changes: 113 additions & 0 deletions labs/openai-agents-tf/product-catalog-openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"openapi": "3.0.1",
"info": {
"title": "Product Catalog API",
"description": "API to retrieve the product catalog.",
"version": "1.0"
},
"servers": [{
"url": "https://replace-me.local/catalogservice"
}],
"paths": {
"/product": {
"get": {
"summary": "Get product details for a specified category",
"description": "Retrieves product details, for a specified category.",
"operationId": "get-product-details",
"parameters": [{
"name": "category",
"in": "query",
"description": "Name of the category to retrieve product information for",
"required": true,
"schema": {
"type": "string",
"example": "Electronics"
}
}],
"responses": {
"200": {
"description": "Product information for the specified category",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductInformation"
},
"examples": {
"Smartphone": {
"summary": "Example response for Smartphone",
"value": {
"name": "Smartphone",
"category": "Electronics",
"sku": "ELEC1234",
"stock": 50,
"store_location": "Lisbon"
}
},
"Laptop": {
"summary": "Example response for Laptop",
"value": {
"name": "Laptop",
"category": "Electronics",
"sku": "ELEC5678",
"stock": 30,
"store_location": "Seattle"
}
}
}
}
}
},
"400": {
"description": "Bad request"
},
"404": {
"description": "Category not found"
},
"500": {
"description": "Internal server error"
}
}
}
}
},
"components": {
"schemas": {
"ProductInformation": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Smartphone"
},
"category": {
"type": "string",
"example": "Electronics"
},
"sku": {
"type": "string",
"example": "ELEC1234"
},
"stock": {
"type": "number",
"format": "int32",
"example": 60
},
"store_location": {
"type": "string",
"example": "Lisbon"
}
}
}
},
"securitySchemes": {
"apiKeyHeader": {
"type": "apiKey",
"name": "api-key",
"in": "header"
}
}
},
"security": [{
"apiKeyHeader": []
}]
}
29 changes: 29 additions & 0 deletions labs/openai-agents-tf/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
terraform {

required_version = ">=1.8"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.16.0"
}
azapi = {
source = "Azure/azapi"
version = ">= 2.2.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.6.3"
}
}
}

provider "azurerm" {
features{}
}

provider "azapi" {
}

provider "random" {
}
16 changes: 16 additions & 0 deletions labs/openai-agents-tf/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

resource_group_name = "lab-openai-agents-tf"
resource_group_location = "uksouth"
apim_sku = "Basicv2"
openai_deployment_name = "gpt-4o-mini"
openai_model_name = "gpt-4o-mini"
openai_model_version = "2024-07-18"
openai_model_capacity = "8"
openai_api_version = "2024-10-21"
openai_config = {
openai-1 = {
name = "openai1",
location = "uksouth",
}
}

92 changes: 92 additions & 0 deletions labs/openai-agents-tf/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
variable "resource_group_name" {
type = string
description = "The name of the resource group."
}

variable "resource_group_location" {
type = string
description = "The location of the resource group."
default = "eastus"
}

variable "apim_resource_name" {
type = string
description = "The name of the API Management resource."
default = "apim"
}

variable "apim_sku" {
type = string
description = "The SKU of the API Management resource."
default = "Developer"
}

variable "openai_config" {
description = "Configuration for OpenAI accounts"
type = map(object({
location = string
name = string
}))
}

variable "openai_api_version" {
type = string
description = "The API version for OpenAI Cognitive Service."
default = "2024-10-21"

}

variable "openai_sku" {
type = string
description = "The SKU for OpenAI Cognitive Service."
default = "S0"
}

variable "openai_deployment_name" {
type = string
description = "The name of the OpenAI deployment."
}

variable "openai_model_name" {
type = string
description = "The name of the OpenAI model."
}

variable "openai_model_version" {
type = string
description = "The version of the OpenAI model."
}

variable "openai_model_capacity" {
type = number
description = "The capacity of the OpenAI model."
default = 1
}

variable "openai_api_spec_url" {
type = string
default = "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2024-10-21/inference.json"
}
variable "weather_api_path" {
type = string
description = "The path for the Weather API."
default = "weatherservice"
}

variable "place_order_api_path" {
type = string
description = "The path for the Place Order API."
default = "orderservice"
}

variable "product_catalog_api_path" {
type = string
description = "The path for the Product Catalog API."
default = "catalogservice"
}

variable "location" {
type = string
description = "The location for the resources."
default = "eastus"
}