diff --git a/authors.yaml b/authors.yaml index 92b89168e9..4e14d57d64 100644 --- a/authors.yaml +++ b/authors.yaml @@ -547,3 +547,8 @@ derrickchoi-openai: name: "Derrick Choi" website: "https://www.linkedin.com/in/derrickchoi/" avatar: "https://avatars.githubusercontent.com/u/211427900" + +kevinv-openai: + name: "Kevin Verdieck" + website: "https://www.linkedin.com/in/kevinverdieck/" + avatar: "https://avatars.githubusercontent.com/u/197816265?v=4" diff --git a/examples/chatgpt/compliance_api/logs_platform.ipynb b/examples/chatgpt/compliance_api/logs_platform.ipynb new file mode 100644 index 0000000000..d51eb731c4 --- /dev/null +++ b/examples/chatgpt/compliance_api/logs_platform.ipynb @@ -0,0 +1,409 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OpenAI Compliance Logs Platform quickstart\n", + "\n", + "Use this notebook to get started using the OpenAI Compliance Logs Platform. The examples focus on downloading log files so you can ingest them into your SIEM or data lake.\n", + "\n", + "- [Help Center Overview](https://help.openai.com/en/articles/9261474-compliance-api-for-chatgpt-enterprise-edu-and-chatgpt-for-teachers)\n", + "- [API Reference](https://chatgpt.com/admin/api-reference#tag/Compliance-API-Logs-Platform)\n" + ], + "id": "3c9bde9be51b8b78" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- An Enterprise Compliance API key exported as `COMPLIANCE_API_KEY`.\n", + "- The ChatGPT account ID or the API Platform Org ID for the principal in question.\n", + "- Specific requirements for your environment" + ], + "id": "18f32c1126f4e3ce" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quickstart Scripts\n", + "\n", + "Provided below are functionally identical scripts - one for Unix-based and one for Windows-based environments.\n", + "These scripts give an example of how one could build an integration with the Compliance API to retrieve and process\n", + "log data for given event types and time ranges.\n", + "These scripts handle listing and paging through the available log files and downloading them - writing the output to stdout.\n", + "\n", + "Example invocations of these scripts are embedded in their help blocks - execute them with no arguments to see them." + ], + "id": "4388a0474fcabd7" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Option 1: Unix-based\n", + "\n", + "Prerequisites:\n", + "- Save the script locally as `download_compliance_files.sh` and mark it executable\n", + "- Make sure you have up-to-date `bash`, `curl`, `sed`, and `date` installed.\n", + "- Format the date you want to get every log `after` as an ISO 8601 string including timezone.\n", + "\n", + "Run the script akin to `./download_compliance_files.sh `" + ], + "id": "e6999916958ccbba" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "```bash\n", + "#!/usr/bin/env bash\n", + "set -euo pipefail\n", + "\n", + "usage() {\n", + " echo \"Usage: $0 \" >&2\n", + " echo >&2\n", + " echo 'Examples: ' >&2\n", + " echo 'COMPLIANCE_API_KEY= ./download_compliance_files.sh f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n", + " echo 'COMPLIANCE_API_KEY= ./download_compliance_files.sh org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n", + "}\n", + "\n", + "if [[ $# -ne 4 ]]; then\n", + " usage\n", + " exit 2\n", + "fi\n", + "\n", + "PRINCIPAL_ID=\"$1\"\n", + "EVENT_TYPE=\"$2\"\n", + "LIMIT=\"$3\"\n", + "INITIAL_AFTER=\"$4\"\n", + "\n", + "# Require COMPLIANCE_API_KEY to be present and non-empty before using it\n", + "if [[ -z \"${COMPLIANCE_API_KEY:-}\" ]]; then\n", + " echo \"COMPLIANCE_API_KEY environment variable is required. e.g.:\" >&2\n", + " echo \"COMPLIANCE_API_KEY= $0 \" >&2\n", + " exit 2\n", + "fi\n", + "\n", + "API_BASE=\"https://api.chatgpt.com/v1/compliance\"\n", + "AUTH_HEADER=(\"-H\" \"Authorization: Bearer ${COMPLIANCE_API_KEY}\")\n", + "\n", + "# Determine whether the first arg is a workspace ID or an org ID.\n", + "# If it starts with \"org-\" treat it as an organization ID and switch the path segment accordingly.\n", + "SCOPE_SEGMENT=\"workspaces\"\n", + "if [[ \"${PRINCIPAL_ID}\" == org-* ]]; then\n", + " SCOPE_SEGMENT=\"organizations\"\n", + "fi\n", + "\n", + "# Perform a curl request and fail fast on HTTP errors, logging context to stderr.\n", + "# Usage: perform_curl \"description of action\" \n", + "perform_curl() {\n", + " local description=\"$1\"\n", + " shift\n", + " # Capture body and HTTP status code, keeping body on stdout-like var\n", + " # We append a newline before the status to reliably split even if body has no trailing newline.\n", + " local combined\n", + " if ! combined=$(curl -sS -w \"\\n%{http_code}\" \"$@\"); then\n", + " echo \"Network/transport error while ${description}\" >&2\n", + " exit 1\n", + " fi\n", + " local http_code\n", + " http_code=\"${combined##*$'\\n'}\"\n", + " local body\n", + " body=\"${combined%$'\\n'*}\"\n", + "\n", + " if [[ ! \"${http_code}\" =~ ^2[0-9][0-9]$ ]]; then\n", + " echo \"HTTP error ${http_code} while ${description}:\" >&2\n", + " if [[ -n \"${body}\" ]]; then\n", + " # Print the body to stderr so it doesn't corrupt stdout stream\n", + " echo \"${body}\" | jq . >&2\n", + " fi\n", + " exit 1\n", + " fi\n", + "\n", + " # On success, emit body to stdout for callers to consume\n", + " echo \"${body}\"\n", + "}\n", + "\n", + "list_logs() {\n", + " local after=\"$1\"\n", + " perform_curl \"listing logs (after=${after}, event_type=${EVENT_TYPE}, limit=${LIMIT})\" \\\n", + " -G \\\n", + " \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs\" \\\n", + " \"${AUTH_HEADER[@]}\" \\\n", + " --data-urlencode \"limit=${LIMIT}\" \\\n", + " --data-urlencode \"event_type=${EVENT_TYPE}\" \\\n", + " --data-urlencode \"after=${after}\"\n", + "}\n", + "\n", + "download_log() {\n", + " local id=\"$1\"\n", + " echo \"Fetching logs for ID: ${id}\" >&2\n", + " perform_curl \"downloading log id=${id}\" \\\n", + " -G -L \\\n", + " \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs/${id}\" \\\n", + " \"${AUTH_HEADER[@]}\"\n", + "}\n", + "\n", + "to_local_human() {\n", + " local iso=\"$1\"\n", + " if [[ -z \"${iso}\" || \"${iso}\" == \"null\" ]]; then\n", + " echo \"\"\n", + " return 0\n", + " fi\n", + "\n", + " local iso_norm\n", + " iso_norm=$(echo -n \"${iso}\" \\\n", + " | sed -E 's/\\.[0-9]+(Z|[+-][0-9:]+)$/\\1/' \\\n", + " | sed -E 's/([+-]00:00)$/Z/')\n", + "\n", + " # macOS/BSD date: parse UTC to epoch then format in local timezone\n", + " local epoch\n", + " epoch=$(date -j -u -f \"%Y-%m-%dT%H:%M:%SZ\" \"${iso_norm}\" +%s 2>/dev/null) || true\n", + " if [[ -n \"${epoch}\" ]]; then\n", + " date -r \"${epoch}\" \"+%Y-%m-%d %H:%M:%S %Z\" 2>/dev/null && return 0\n", + " fi\n", + "\n", + " # Fallback to original if parsing failed\n", + " echo \"${iso}\"\n", + "}\n", + "\n", + "current_after=\"${INITIAL_AFTER}\"\n", + "page=1\n", + "total_downloaded=0\n", + "while true; do\n", + " echo \"Fetching page ${page} with after='${current_after}' (local: $(to_local_human \"${current_after}\"))\" >&2\n", + " response_json=\"$(list_logs \"${current_after}\")\"\n", + "\n", + " # Count and download each ID from the current page (if any)\n", + " page_count=\"$(echo \"${response_json}\" | jq '.data | length')\"\n", + " if [[ \"${page_count}\" -gt 0 ]]; then\n", + " echo \"${response_json}\" | jq -r '.data[].id' | while read -r id; do\n", + " download_log \"${id}\"\n", + " done\n", + " total_downloaded=$((total_downloaded + page_count))\n", + " fi\n", + "\n", + " has_more=\"$(echo \"${response_json}\" | jq -r '.has_more')\"\n", + " current_after=\"$(echo \"${response_json}\" | jq -r '.last_end_time')\"\n", + " if [[ \"${has_more}\" == \"true\" ]]; then\n", + " page=$((page + 1))\n", + " else\n", + " break\n", + " fi\n", + "done\n", + "\n", + "if [[ \"${total_downloaded}\" -eq 0 && ( -z \"${current_after}\" || \"${current_after}\" == \"null\" ) ]]; then\n", + " echo \"No results found for event_type ${EVENT_TYPE} after ${INITIAL_AFTER}\" >&2\n", + "else\n", + " echo \"Completed downloading ${total_downloaded} log files up to ${current_after} (local: $(to_local_human \"${current_after}\"))\" >&2\n", + "fi\n", + "```" + ], + "id": "e6ed22a253b67850" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Option 2: Windows-based\n", + "\n", + "Prerequisites:\n", + "- Save the script locally as `download_compliance_files.ps1`\n", + "- Open PowerShell (Version 5.1+) and navigate to the directory where the script is saved.\n", + "\n", + "Run the script akin to `.\\download_compliance_files.ps1 `" + ], + "id": "206e977bd1b7a441" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "```ps\n", + "#!/usr/bin/env pwsh\n", + "#Requires -Version 5.1\n", + "\n", + "Set-StrictMode -Version Latest\n", + "$ErrorActionPreference = 'Stop'\n", + "\n", + "Add-Type -AssemblyName System.Web\n", + "\n", + "function Show-Usage {\n", + " [Console]::Error.WriteLine(@\"\n", + "Usage: .\\download_compliance_files.ps1 \n", + "\n", + "Example:\n", + " `$env:COMPLIANCE_API_KEY = ''\n", + " .\\download_compliance_files.ps1 f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n", + " Out-File -Encoding utf8 output.jsonl\n", + "\n", + "Example (org id):\n", + " `$env:COMPLIANCE_API_KEY = ''\n", + " .\\download_compliance_files.ps1 org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n", + " Out-File -Encoding utf8 output.jsonl\n", + "\"@)\n", + "}\n", + "\n", + "if ($args.Count -ne 4) {\n", + " Show-Usage\n", + " exit 2\n", + "}\n", + "\n", + "if (-not $env:COMPLIANCE_API_KEY) {\n", + " [Console]::Error.WriteLine('COMPLIANCE_API_KEY environment variable must be set.')\n", + " exit 2\n", + "}\n", + "\n", + "$PrincipalId = $args[0]\n", + "$EventType = $args[1]\n", + "$Limit = $args[2]\n", + "$InitialAfter = $args[3]\n", + "\n", + "$ApiBase = 'https://api.chatgpt.com/v1/compliance'\n", + "\n", + "if ($PrincipalId.StartsWith('org-')) {\n", + " $ScopeSegment = 'organizations'\n", + "} else {\n", + " $ScopeSegment = 'workspaces'\n", + "}\n", + "\n", + "$handler = [System.Net.Http.HttpClientHandler]::new()\n", + "$client = [System.Net.Http.HttpClient]::new($handler)\n", + "$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $env:COMPLIANCE_API_KEY)\n", + "\n", + "function Invoke-ComplianceRequest {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $Description,\n", + " [Parameter(Mandatory = $true)] [string] $Path,\n", + " [hashtable] $Query = @{}\n", + " )\n", + "\n", + " $builder = [System.UriBuilder]::new(\"$ApiBase/$ScopeSegment/$PrincipalId/$Path\")\n", + " $queryString = [System.Web.HttpUtility]::ParseQueryString($builder.Query)\n", + " foreach ($key in $Query.Keys) {\n", + " $queryString[$key] = $Query[$key]\n", + " }\n", + " $builder.Query = $queryString.ToString()\n", + "\n", + " try {\n", + " $response = $client.GetAsync($builder.Uri).GetAwaiter().GetResult()\n", + " } catch {\n", + " [Console]::Error.WriteLine(\"Network/transport error while $Description\")\n", + " exit 1\n", + " }\n", + "\n", + " $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()\n", + " if (-not $response.IsSuccessStatusCode) {\n", + " [Console]::Error.WriteLine(\"HTTP error $($response.StatusCode.value__) while ${Description}:\")\n", + " if ($body) {\n", + " try {\n", + " $parsed = $body | ConvertFrom-Json\n", + " $parsed | ConvertTo-Json -Depth 10 | Write-Error\n", + " } catch {\n", + " [Console]::Error.WriteLine($body)\n", + " }\n", + " }\n", + " exit 1\n", + " }\n", + "\n", + " Write-Output $body\n", + "}\n", + "\n", + "function List-Logs {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $After\n", + " )\n", + "\n", + " Invoke-ComplianceRequest -Description \"listing logs (after=$After, event_type=$EventType, limit=$Limit)\" -Path 'logs' -Query @{\n", + " limit = $Limit\n", + " event_type = $EventType\n", + " after = $After\n", + " }\n", + "}\n", + "\n", + "function Download-Log {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $Id\n", + " )\n", + "\n", + " [Console]::Error.WriteLine(\"Fetching logs for ID: $Id\")\n", + " Invoke-ComplianceRequest -Description \"downloading log id=$Id\" -Path \"logs/$Id\"\n", + "}\n", + "\n", + "function ConvertTo-LocalHuman {\n", + " param(\n", + " [string] $Iso\n", + " )\n", + "\n", + " if (-not $Iso -or $Iso -eq 'null') {\n", + " return ''\n", + " }\n", + "\n", + " try {\n", + " $dt = [datetimeoffset]::Parse($Iso)\n", + " return $dt.ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss zzz')\n", + " } catch {\n", + " return $Iso\n", + " }\n", + "}\n", + "\n", + "$currentAfter = $InitialAfter\n", + "$page = 1\n", + "$totalDownloaded = 0\n", + "while ($true) {\n", + " [Console]::Error.WriteLine(\"Fetching page $page with after='$currentAfter' (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n", + " $responseJson = List-Logs -After $currentAfter\n", + " $responseObj = $responseJson | ConvertFrom-Json\n", + "\n", + " $pageCount = $responseObj.data.Count\n", + " if ($pageCount -gt 0) {\n", + " foreach ($entry in $responseObj.data) {\n", + " Download-Log -Id $entry.id\n", + " }\n", + " $totalDownloaded += $pageCount\n", + " }\n", + "\n", + " $hasMore = $false\n", + " if ($null -ne $responseObj.has_more) {\n", + " $hasMore = [System.Convert]::ToBoolean($responseObj.has_more)\n", + " }\n", + "\n", + " $currentAfter = $responseObj.last_end_time\n", + " if ($hasMore) {\n", + " $page += 1\n", + " } else {\n", + " break\n", + " }\n", + "}\n", + "\n", + "if ($totalDownloaded -eq 0 -and ([string]::IsNullOrEmpty($currentAfter) -or $currentAfter -eq 'null')) {\n", + " [Console]::Error.WriteLine(\"No results found for event_type $EventType after $InitialAfter\")\n", + "} else {\n", + " [Console]::Error.WriteLine(\"Completed downloading $totalDownloaded log files up to $currentAfter (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n", + "}\n", + "\n", + "$client.Dispose()\n", + "$handler.Dispose()\n", + "```" + ], + "id": "2a26163e0617856a" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/registry.yaml b/registry.yaml index 049a10df0e..7c080a01ad 100644 --- a/registry.yaml +++ b/registry.yaml @@ -2681,11 +2681,23 @@ tags: - codex +- title: OpenAI Compliance Logs Platform quickstart + path: examples/chatgpt/compliance_api/logs_platform.ipynb + date: 2025-12-11 + authors: + - kevinv-openai + tags: + - chatgpt + - chatgpt-data + - chatgpt-and-api + - compliance + - enterprise + - title: GPT-5.2 Prompting Guide path: examples/gpt-5/gpt-5-2_prompting_guide.ipynb date: 2025-12-11 authors: - msingh-openai - - emre-openai + - emre-openai tags: - gpt-5.2