{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fd14bd96",
   "metadata": {},
   "source": [
    "# Nomic Agents API Demo\n",
    "\n",
    "This notebook demonstrates the full lifecycle of the **Agents API**: discovering workflows,\n",
    "launching an agent with file inputs, polling for completion, and retrieving results.\n",
    "\n",
    "## Setup Step 1: Install required packages (run once)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4c0eb06",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install -q requests pygments"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "322af514",
   "metadata": {},
   "source": [
    "## Setup: Configure your instance and API key\n",
    "\n",
    "Fill in your Drive instance name (the subdomain from your Drive URL, e.g. `acme` for\n",
    "`acme.drive.nomic.ai`) and your API key. You can create an API key in the Drive UI under\n",
    "**Developer → API Keys** with the **developer** scope selected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9648766e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell and fill in the prompts.\n",
    "from getpass import getpass\n",
    "\n",
    "INSTANCE = input(\"Instance name (subdomain from your Drive URL): \").strip()\n",
    "API_KEY = getpass(\"API Key (npk_...): \").strip()\n",
    "\n",
    "if not INSTANCE:\n",
    "    raise SystemExit(\"Instance name is required.\")\n",
    "if not API_KEY.startswith(\"npk_\"):\n",
    "    raise SystemExit('API key should start with \"npk_\"')\n",
    "\n",
    "BASE_URL = f\"https://{INSTANCE}.drive.nomic.ai/api/v0\"\n",
    "HEADERS = {\"Authorization\": f\"Bearer {API_KEY}\"}\n",
    "\n",
    "print(f\"✔ Configured for {BASE_URL}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00dd3935",
   "metadata": {},
   "source": [
    "## Setup: Helper functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "009cbf25",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to define helper functions used throughout the notebook.\n",
    "import json\n",
    "import time\n",
    "\n",
    "import requests\n",
    "from pygments import highlight\n",
    "from pygments.lexers import JsonLexer\n",
    "from pygments.formatters import HtmlFormatter\n",
    "from IPython.display import HTML, display\n",
    "\n",
    "\n",
    "def print_json(obj, max_height=\"400px\"):\n",
    "    \"\"\"Pretty-print a JSON object with syntax highlighting.\"\"\"\n",
    "    formatter = HtmlFormatter()\n",
    "    colored = highlight(json.dumps(obj, indent=2), JsonLexer(), formatter)\n",
    "    display(\n",
    "        HTML(\n",
    "            f\"<style>{formatter.get_style_defs()}</style>\"\n",
    "            f'<div style=\"max-height: {max_height}; overflow-y: auto; '\n",
    "            f'border: 1px solid #ccc; border-radius: 4px; padding: 8px;\">'\n",
    "            f\"{colored}</div>\"\n",
    "        )\n",
    "    )\n",
    "\n",
    "\n",
    "def api(method, path, **kwargs):\n",
    "    \"\"\"Make an authenticated request to the Drive API.\n",
    "\n",
    "    Returns the parsed JSON response, or raises on HTTP errors.\n",
    "    \"\"\"\n",
    "    url = f\"{BASE_URL}{path}\"\n",
    "    resp = requests.request(method, url, headers=HEADERS, **kwargs)\n",
    "    if not resp.ok:\n",
    "        print(f\"ERROR {resp.status_code}: {resp.text[:500]}\")\n",
    "        resp.raise_for_status()\n",
    "    return resp.json()\n",
    "\n",
    "\n",
    "print(\"✔ Helpers defined\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1780334a",
   "metadata": {},
   "source": [
    "## Step 1: List available workflows\n",
    "\n",
    "Workflows are reusable agent templates. Each workflow declares **file input slots** — named\n",
    "parameters where you pass file IDs. The response tells you what slots exist, whether they're\n",
    "optional, and whether they accept multiple files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f2be3bf0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to list workflows on your instance.\n",
    "result = api(\"GET\", \"/workflows\")\n",
    "\n",
    "print(f\"Found {len(result['data'])} workflow(s):\\n\")\n",
    "for wf in result[\"data\"]:\n",
    "    slots = wf.get(\"requestedFiles\", [])\n",
    "    slot_names = \", \".join(s[\"name\"] for s in slots) if slots else \"(prompt-only)\"\n",
    "    print(f\"  • {wf['name']}\")\n",
    "    print(f\"    ID: {wf['id']}\")\n",
    "    print(f\"    Slots: {slot_names}\")\n",
    "    if wf.get(\"description\"):\n",
    "        print(f\"    Description: {wf['description'][:100]}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "654372f7",
   "metadata": {},
   "source": [
    "## Step 2: Select a workflow\n",
    "\n",
    "Pick a workflow from the dropdown. The slot definitions tell you what files the workflow\n",
    "expects as input."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "252f743b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell and select a workflow from the dropdown.\n",
    "import ipywidgets as widgets\n",
    "from IPython.display import display\n",
    "\n",
    "slot_workflows = [wf for wf in result[\"data\"] if wf.get(\"requestedFiles\")]\n",
    "if not slot_workflows:\n",
    "    raise SystemExit(\"No workflows with file-input slots found on this instance.\")\n",
    "\n",
    "_wf_options = {wf[\"name\"]: wf for wf in slot_workflows}\n",
    "_wf_dropdown = widgets.Dropdown(\n",
    "    options=list(_wf_options.keys()),\n",
    "    description=\"Workflow:\",\n",
    "    style={\"description_width\": \"80px\"},\n",
    "    layout=widgets.Layout(width=\"400px\"),\n",
    ")\n",
    "_slot_info = widgets.Output()\n",
    "\n",
    "\n",
    "def _show_slots(change):\n",
    "    _slot_info.clear_output()\n",
    "    wf = _wf_options[change[\"new\"]]\n",
    "    with _slot_info:\n",
    "        if wf.get(\"description\"):\n",
    "            print(f\"{wf['description']}\\n\")\n",
    "        print(\"This workflow needs the following file inputs:\")\n",
    "        print(\"-\" * 50)\n",
    "        for slot in wf[\"requestedFiles\"]:\n",
    "            flags = []\n",
    "            if slot[\"optional\"]:\n",
    "                flags.append(\"optional\")\n",
    "            if slot[\"multiple\"]:\n",
    "                flags.append(\"accepts multiple files\")\n",
    "            flag_str = f\" ({', '.join(flags)})\" if flags else \" (required)\"\n",
    "            print(f\"  • \\\"{slot['name']}\\\"{flag_str}\")\n",
    "            print(f\"    {slot['description']}\")\n",
    "            print()\n",
    "\n",
    "\n",
    "_wf_dropdown.observe(_show_slots, names=\"value\")\n",
    "display(_wf_dropdown, _slot_info)\n",
    "_show_slots({\"new\": _wf_dropdown.value})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2803ebf",
   "metadata": {},
   "source": [
    "## Step 3: Find your files\n",
    "\n",
    "Now that you know what files the workflow needs, search for them by name. You can re-run\n",
    "this cell with different queries until you've found all the files you need."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "379fa042",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to search for files by name.\n",
    "from urllib.parse import unquote\n",
    "\n",
    "search_query = input(\"Search for files: \").strip()\n",
    "\n",
    "results = api(\"GET\", \"/files/search\", params={\"query\": search_query, \"limit\": 10})\n",
    "\n",
    "print(f\"\\nFiles matching \\\"{search_query}\\\" ({len(results['data'])} results):\\n\")\n",
    "from IPython.display import HTML\n",
    "html = \"\"\n",
    "for f in results[\"data\"]:\n",
    "    html += f'<div style=\"margin-bottom: 8px;\">'\n",
    "    html += f'<code style=\"user-select: all; cursor: pointer; background: #f0f0f0; padding: 2px 4px; border-radius: 3px;\">{f[\"id\"]}</code>'\n",
    "    html += f' {f[\"name\"]}'\n",
    "    html += f'</div>'\n",
    "display(HTML(html))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca94c8c2",
   "metadata": {},
   "source": [
    "## Step 4: Provide file inputs and launch\n",
    "\n",
    "Paste the file IDs from Step 3 into the fields below, then run the **Launch** cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7995ba7d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to show the file input fields for the selected workflow.\n",
    "\n",
    "selected = _wf_options[_wf_dropdown.value]\n",
    "WORKFLOW_ID = selected[\"id\"]\n",
    "\n",
    "_slot_inputs = {}  # name -> (widget, slot_metadata)\n",
    "_input_children = []\n",
    "for slot in selected[\"requestedFiles\"]:\n",
    "    flags = []\n",
    "    if slot[\"optional\"]:\n",
    "        flags.append(\"optional\")\n",
    "    if slot[\"multiple\"]:\n",
    "        flags.append(\"one file ID per line\")\n",
    "    flag_str = f\" ({', '.join(flags)})\" if flags else \"\"\n",
    "\n",
    "    label = widgets.HTML(\n",
    "        f\"<b>{slot['name']}</b>{flag_str}<br/>\"\n",
    "        f\"<small>{slot['description']}</small>\"\n",
    "    )\n",
    "    if slot[\"multiple\"]:\n",
    "        field = widgets.Textarea(\n",
    "            placeholder=\"paste one UUID per line\",\n",
    "            layout=widgets.Layout(width=\"450px\", height=\"60px\"),\n",
    "        )\n",
    "    else:\n",
    "        field = widgets.Text(\n",
    "            placeholder=\"paste file UUID here\",\n",
    "            layout=widgets.Layout(width=\"450px\"),\n",
    "        )\n",
    "    _slot_inputs[slot[\"name\"]] = (field, slot)\n",
    "    _input_children.append(widgets.VBox([label, field], layout=widgets.Layout(margin=\"0 0 12px 0\")))\n",
    "\n",
    "display(widgets.VBox(_input_children))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84679c44",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to launch the agent.\n",
    "\n",
    "workflow_inputs = {}\n",
    "for slot_name, (widget, slot) in _slot_inputs.items():\n",
    "    raw = widget.value.strip()\n",
    "    if slot[\"multiple\"]:\n",
    "        file_ids = [line.strip() for line in raw.splitlines() if line.strip()]\n",
    "        workflow_inputs[slot_name] = file_ids if file_ids else None\n",
    "    else:\n",
    "        workflow_inputs[slot_name] = raw if raw else None\n",
    "\n",
    "has_any = any(v is not None for v in workflow_inputs.values())\n",
    "if not has_any:\n",
    "    raise SystemExit(\"Please paste at least one file ID into the fields above.\")\n",
    "\n",
    "launch_body = {\n",
    "    \"workflowId\": WORKFLOW_ID,\n",
    "    \"workflowInputs\": workflow_inputs,\n",
    "}\n",
    "\n",
    "print(f\"Launching agent with workflow: {selected['name']}\")\n",
    "print_json(launch_body)\n",
    "\n",
    "agent = api(\"POST\", \"/agents\", json=launch_body)\n",
    "\n",
    "AGENT_ID = agent[\"data\"][\"id\"]\n",
    "print(f\"\\n✔ Agent launched!\")\n",
    "print(f\"  ID: {AGENT_ID}\")\n",
    "print(f\"  Status: {agent['data']['status']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9140b39",
   "metadata": {},
   "source": [
    "## Step 5: Poll until complete\n",
    "\n",
    "Agents run asynchronously. We poll `GET /agents/{id}` every few seconds until the status\n",
    "reaches a terminal state (`finished`, `stopped`, or `failed`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c72e4c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to poll the agent until it finishes.\n",
    "TERMINAL_STATUSES = {\"finished\", \"stopped\", \"failed\"}\n",
    "POLL_INTERVAL = 5  # seconds\n",
    "\n",
    "print(f\"Polling agent {AGENT_ID}...\")\n",
    "prev_status = None\n",
    "\n",
    "while True:\n",
    "    agent = api(\"GET\", f\"/agents/{AGENT_ID}\")\n",
    "    status = agent[\"data\"][\"status\"]\n",
    "\n",
    "    if status != prev_status:\n",
    "        print(f\"  Status: {status}\")\n",
    "        prev_status = status\n",
    "\n",
    "    if status in TERMINAL_STATUSES:\n",
    "        break\n",
    "\n",
    "    time.sleep(POLL_INTERVAL)\n",
    "\n",
    "print(f\"\\n✔ Agent reached terminal state: {status}\")\n",
    "if agent[\"data\"].get(\"artifacts\"):\n",
    "    print(f\"  Artifacts produced: {len(agent['data']['artifacts'])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ae41ac7",
   "metadata": {},
   "source": [
    "## Step 6: Get conversation\n",
    "\n",
    "Retrieve the agent's conversation history — the messages exchanged between the user prompt,\n",
    "the agent's reasoning, and its final response. Pass `?include=steps` to also see thinking\n",
    "and tool-use events."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a74f8958",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to retrieve the agent's conversation.\n",
    "conversation = api(\"GET\", f\"/agents/{AGENT_ID}/conversation\", params={\"include\": \"steps\"})\n",
    "\n",
    "messages = conversation[\"data\"][\"messages\"]\n",
    "print(f\"Conversation has {len(messages)} message(s):\\n\")\n",
    "\n",
    "for msg in messages:\n",
    "    msg_type = msg[\"type\"]\n",
    "    if msg_type == \"user\":\n",
    "        print(f\"[USER] {msg['text'][:200]}\")\n",
    "    elif msg_type == \"assistant\":\n",
    "        print(f\"[ASSISTANT] {msg['text'][:500]}\")\n",
    "    elif msg_type == \"tool\":\n",
    "        status_icon = {\"started\": \"⏳\", \"completed\": \"✔\", \"error\": \"✘\"}.get(msg[\"status\"], \"?\")\n",
    "        print(f\"[TOOL {status_icon}] {msg['toolName']}\")\n",
    "    elif msg_type == \"thinking\":\n",
    "        label = \"(redacted)\" if msg.get(\"redacted\") else msg.get(\"text\", \"\")[:100]\n",
    "        print(f\"[THINKING] {label}\")\n",
    "    elif msg_type == \"error\":\n",
    "        print(f\"[ERROR] {msg['text']}\")\n",
    "    elif msg_type == \"compaction\":\n",
    "        print(\"[COMPACTION] context window compacted\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1dbb00a9",
   "metadata": {},
   "source": [
    "## Step 7: Get artifacts\n",
    "\n",
    "Artifacts are the structured outputs produced by the agent — documents (markdown) or tables.\n",
    "Each artifact listed on the agent detail can be fetched individually."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7570f6cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to fetch all artifacts produced by the agent.\n",
    "from IPython.display import Markdown\n",
    "\n",
    "artifacts = agent[\"data\"].get(\"artifacts\", [])\n",
    "\n",
    "if not artifacts:\n",
    "    print(\"No artifacts were produced by this agent.\")\n",
    "else:\n",
    "    print(f\"Fetching {len(artifacts)} artifact(s)...\\n\")\n",
    "    for ref in artifacts:\n",
    "        print(f\"{'='*60}\")\n",
    "        print(f\"Artifact: {ref['name']} (type: {ref['type']})\")\n",
    "        print(f\"ID: {ref['id']}\")\n",
    "        print(f\"{'='*60}\")\n",
    "\n",
    "        artifact = api(\"GET\", f\"/agents/{AGENT_ID}/artifacts/{ref['id']}\")\n",
    "        content = artifact[\"data\"][\"content\"]\n",
    "\n",
    "        if isinstance(content, str):\n",
    "            # Markdown document — render it\n",
    "            display(Markdown(content[:3000]))\n",
    "            if len(content) > 3000:\n",
    "                print(f\"\\n  ... (truncated, {len(content)} chars total)\")\n",
    "        else:\n",
    "            # Table or structured data — pretty-print as JSON\n",
    "            print_json(content)\n",
    "        print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf4c5d45",
   "metadata": {},
   "source": [
    "## Step 8: Follow-up (multi-turn)\n",
    "\n",
    "Once an agent has finished, you can send a follow-up prompt to continue the conversation.\n",
    "This is useful for asking clarifying questions or requesting revisions to the output."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b4fe0434",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Type your follow-up prompt, then run the next cell.\n",
    "_followup_input = widgets.Textarea(\n",
    "    placeholder=\"e.g. Summarize the key findings in 3 bullet points.\",\n",
    "    layout=widgets.Layout(width=\"100%\", height=\"60px\"),\n",
    ")\n",
    "display(_followup_input)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eedbe76f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# <-- Run this cell to send the follow-up.\n",
    "FOLLOWUP_PROMPT = _followup_input.value.strip()\n",
    "if not FOLLOWUP_PROMPT:\n",
    "    raise SystemExit(\"Please type a follow-up prompt above first.\")\n",
    "\n",
    "print(f\"Sending follow-up: {FOLLOWUP_PROMPT!r}\\n\")\n",
    "\n",
    "followup = api(\"POST\", f\"/agents/{AGENT_ID}/followup\", json={\n",
    "    \"prompt\": {\"text\": FOLLOWUP_PROMPT}\n",
    "})\n",
    "\n",
    "print(f\"✔ Follow-up accepted (status: {followup['data']['status']})\")\n",
    "print(\"  Polling until complete...\")\n",
    "\n",
    "prev_status = None\n",
    "while True:\n",
    "    agent = api(\"GET\", f\"/agents/{AGENT_ID}\")\n",
    "    status = agent[\"data\"][\"status\"]\n",
    "    if status != prev_status:\n",
    "        print(f\"  Status: {status}\")\n",
    "        prev_status = status\n",
    "    if status in TERMINAL_STATUSES:\n",
    "        break\n",
    "    time.sleep(POLL_INTERVAL)\n",
    "\n",
    "# Show the new conversation messages\n",
    "conversation = api(\"GET\", f\"/agents/{AGENT_ID}/conversation\")\n",
    "messages = conversation[\"data\"][\"messages\"]\n",
    "\n",
    "# Print just the last assistant message\n",
    "assistant_msgs = [m for m in messages if m[\"type\"] == \"assistant\"]\n",
    "if assistant_msgs:\n",
    "    print(f\"\\n{'='*60}\")\n",
    "    print(\"Latest response:\")\n",
    "    print(f\"{'='*60}\")\n",
    "    print(assistant_msgs[-1][\"text\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1479774e",
   "metadata": {},
   "source": [
    "## API Reference\n",
    "\n",
    "| Method | Endpoint | Description |\n",
    "|--------|----------|-------------|\n",
    "| `GET` | `/files/search` | Search files by name |\n",
    "| `GET` | `/workflows` | List available workflow templates |\n",
    "| `POST` | `/agents` | Launch a new agent |\n",
    "| `GET` | `/agents/{id}` | Get agent status and metadata |\n",
    "| `GET` | `/agents/{id}/conversation` | Get conversation messages |\n",
    "| `GET` | `/agents/{id}/artifacts/{artifactId}` | Get artifact content |\n",
    "| `POST` | `/agents/{id}/followup` | Continue a finished agent |\n",
    "| `POST` | `/agents/{id}/stop` | Cancel a running agent |\n",
    "\n",
    "All endpoints authenticate via `Authorization: Bearer npk_...` and are scoped under `/api/v0`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.14.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
