diff --git a/tutorials/human_feedback/chatbot_with_human_feedback.ipynb b/tutorials/human_feedback/chatbot_with_human_feedback.ipynb
new file mode 100644
index 0000000000..878334b648
--- /dev/null
+++ b/tutorials/human_feedback/chatbot_with_human_feedback.ipynb
@@ -0,0 +1,263 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
\n",
+ " Docs\n",
+ " |\n",
+ " GitHub\n",
+ " |\n",
+ " Community\n",
+ "
\n",
+ "\n",
+ "Instrumenting a chatbot with human feedback
\n",
+ "\n",
+ "Phoenix provides endpoints to associate user-provided feedback directly with OpenInference spans as annotations.\n",
+ "\n",
+ "In this tutorial, we will create a manually-instrument chatbot with user-triggered \"👍\" and \"👎\" feedback buttons. We will have those buttons trigger a callback that sends the user feedback to Phoenix and is viewable alongside the span. Automating associating feedback with spans is a powerful way to quickly focus on traces of your application that are not behaving as expected."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!pip install \"phoenix\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import json\n",
+ "import os\n",
+ "import warnings\n",
+ "from typing import Any, Dict\n",
+ "from uuid import uuid4\n",
+ "\n",
+ "import httpx\n",
+ "import ipywidgets as widgets\n",
+ "import phoenix as px\n",
+ "from IPython.display import display\n",
+ "from openinference.semconv.trace import (\n",
+ " OpenInferenceMimeTypeValues,\n",
+ " OpenInferenceSpanKindValues,\n",
+ " SpanAttributes,\n",
+ ")\n",
+ "from opentelemetry import trace as trace_api\n",
+ "from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter\n",
+ "from opentelemetry.sdk import trace as trace_sdk\n",
+ "from opentelemetry.sdk.trace.export import SimpleSpanProcessor"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from getpass import getpass\n",
+ "\n",
+ "if not (openai_api_key := os.getenv(\"OPENAI_API_KEY\")):\n",
+ " openai_api_key = getpass(\"🔑 Enter your OpenAI API key: \")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Define endpoints and configure OpenTelemetry tracing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ENDPOINT = \"http://localhost:6006/v1\"\n",
+ "FEEDBACK_ENDPOINT = f\"{ENDPOINT}/span_annotations\"\n",
+ "OPENAI_API_URL = \"https://api.openai.com/v1/chat/completions\"\n",
+ "\n",
+ "tracer_provider = trace_sdk.TracerProvider()\n",
+ "tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(f\"{ENDPOINT}/traces\")))\n",
+ "trace_api.set_tracer_provider(tracer_provider)\n",
+ "TRACER = trace_api.get_tracer(__name__)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "px.launch_app()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Define and instrument chat service backend\n",
+ "\n",
+ "Here we define two functions:\n",
+ "\n",
+ "`generate_response` is a function that contains the chatbot logic for responding to a user query. `generate_response` is manually instrumented using the `OpenInference` semantic conventions. More information on how to manually instrument an application can be found [here](https://docs.arize.com/phoenix/tracing/how-to-tracing/manual-instrumentation). `generate_response` also returns the OpenTelemetry spanID, a hex-encoded string that is used to associate feedback with a specific trace.\n",
+ "\n",
+ "`send_feedback` is a function that sends user feedback to Phoenix via the `span_annotations` REST route."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "http_client = httpx.Client()\n",
+ "\n",
+ "\n",
+ "def generate_response(\n",
+ " input_text: str, model: str = \"gpt-4o\", temperature: float = 0.1\n",
+ ") -> Dict[str, Any]:\n",
+ " user_message = {\"role\": \"user\", \"content\": input_text, \"uuid\": str(uuid4())}\n",
+ " invocation_parameters = {\"temperature\": temperature}\n",
+ " payload = {\n",
+ " \"model\": model,\n",
+ " **invocation_parameters,\n",
+ " \"messages\": [user_message],\n",
+ " }\n",
+ " headers = {\n",
+ " \"Content-Type\": \"application/json\",\n",
+ " \"Authorization\": f\"Bearer {openai_api_key}\",\n",
+ " }\n",
+ " with TRACER.start_as_current_span(\"chatbot with feedback example\") as span:\n",
+ " span.set_attribute(\n",
+ " SpanAttributes.OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKindValues.LLM.value\n",
+ " )\n",
+ " span.set_attribute(SpanAttributes.LLM_MODEL_NAME, payload[\"model\"])\n",
+ " span.set_attribute(SpanAttributes.INPUT_VALUE, json.dumps(payload[\"messages\"][0]))\n",
+ " span.set_attribute(SpanAttributes.INPUT_MIME_TYPE, OpenInferenceMimeTypeValues.JSON.value)\n",
+ "\n",
+ " # get the active hex-encoded spanID\n",
+ " span_id = span.get_span_context().span_id.to_bytes(8, \"big\").hex()\n",
+ " print(span_id)\n",
+ "\n",
+ " response = http_client.post(OPENAI_API_URL, headers=headers, json=payload)\n",
+ "\n",
+ " if not (200 <= response.status_code < 300):\n",
+ " raise Exception(f\"Failed to call OpenAI API: {response.text}\")\n",
+ " response_json = response.json()\n",
+ "\n",
+ " span.set_attribute(SpanAttributes.OUTPUT_VALUE, json.dumps(response_json))\n",
+ " span.set_attribute(SpanAttributes.OUTPUT_MIME_TYPE, OpenInferenceMimeTypeValues.JSON.value)\n",
+ "\n",
+ " return response_json, span_id\n",
+ "\n",
+ "\n",
+ "def send_feedback(span_id: str, feedback: int) -> None:\n",
+ " label = \"👍\" if feedback == 1 else \"👎\"\n",
+ " request_body = {\n",
+ " \"data\": [\n",
+ " {\n",
+ " \"span_id\": span_id,\n",
+ " \"name\": \"user_feedback\",\n",
+ " \"annotator_kind\": \"HUMAN\",\n",
+ " \"result\": {\"label\": label, \"score\": feedback},\n",
+ " \"metadata\": {},\n",
+ " }\n",
+ " ]\n",
+ " }\n",
+ "\n",
+ " try:\n",
+ " response = http_client.post(FEEDBACK_ENDPOINT, json=request_body)\n",
+ " if not (200 <= response.status_code < 300):\n",
+ " raise Exception(f\"Failed to send feedback: {response.text}\")\n",
+ " print(f\"Feedback sent for span_id {span_id}: {label}\")\n",
+ " except httpx.ConnectError:\n",
+ " warnings.warn(\"Could not connect to feedback server.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Create Chat Widget\n",
+ "\n",
+ "We create a simple chat application using IPython widgets. Alongside the chatbot responses we provide feedback buttons that a user can click to provide feedback. These can be seen inside the Phoenix UI!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def send_message(_):\n",
+ " input_text = input_box.value\n",
+ "\n",
+ " # Send the message to the OpenAI API and get the response\n",
+ " response_data, span_id = generate_response(input_text)\n",
+ " assistant_content = response_data[\"choices\"][0][\"message\"][\"content\"]\n",
+ "\n",
+ " # Create thumbs up and thumbs down buttons\n",
+ " thumbs_up = widgets.Button(description=\"👍\", layout=widgets.Layout(width=\"30px\"))\n",
+ " thumbs_down = widgets.Button(description=\"👎\", layout=widgets.Layout(width=\"30px\"))\n",
+ "\n",
+ " # Set up the callbacks for the buttons\n",
+ " thumbs_up.on_click(lambda _: send_feedback(span_id, 1))\n",
+ " thumbs_down.on_click(lambda _: send_feedback(span_id, 0))\n",
+ "\n",
+ " # Create a horizontal box to hold the response and the buttons\n",
+ " response_box = widgets.HBox(\n",
+ " [widgets.Label(f\"Bot: {assistant_content}\"), thumbs_up, thumbs_down]\n",
+ " )\n",
+ "\n",
+ " # Add the user's message and the response to the chat history\n",
+ " chat_history.children += (widgets.Label(f\"You: {input_text}\"), response_box)\n",
+ "\n",
+ " # Clear the input box\n",
+ " input_box.value = \"\"\n",
+ "\n",
+ "\n",
+ "# Set up the chat interface\n",
+ "chat_history = widgets.VBox()\n",
+ "input_box = widgets.Text(placeholder=\"Type your message here...\")\n",
+ "send_button = widgets.Button(description=\"Send\")\n",
+ "send_button.on_click(send_message)\n",
+ "\n",
+ "# Display the chat interface\n",
+ "display(chat_history, input_box, send_button)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "phoenix",
+ "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.11.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}