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", + " \"phoenix\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 +}