|
| 1 | +--- |
| 2 | +title: Conversation tags |
| 3 | +description: Attach key-value metadata to a conversation with tags and read it back from AppConversation.tags. |
| 4 | +icon: tags |
| 5 | +--- |
| 6 | + |
| 7 | +{/* GENERATED from OpenHands/enterprise-cookbook@f00d8684783a6f687ee209454155e98556b4d0bd (conversation-tags/README.md). Edit the source, not this file. */} |
| 8 | + |
| 9 | +<Card title="View source on GitHub" icon="github" href="https://github.com/OpenHands/enterprise-cookbook/tree/f00d8684783a6f687ee209454155e98556b4d0bd/conversation-tags" horizontal /> |
| 10 | + |
| 11 | +Stash your own key-value metadata on an OpenHands conversation — for example an |
| 12 | +external `environment_url` or `environment_conversation_id` — and read it back |
| 13 | +later from your own tooling. Conversations expose a free-form **`tags`** map for |
| 14 | +exactly this. |
| 15 | + |
| 16 | +This is the supported replacement for adding a bespoke field (e.g. a custom |
| 17 | +`environment_url` column) to the conversation model: use `tags` instead. |
| 18 | + |
| 19 | +## The two-server split |
| 20 | + |
| 21 | +OpenHands has a **Cloud app server** (manages accounts, sandboxes, and |
| 22 | +conversations) and, for each sandbox, an **agent server** (the runtime that owns |
| 23 | +the conversation). Tags live on the agent-side conversation, and their values |
| 24 | +surface on the Cloud's `AppConversation.tags` field. |
| 25 | + |
| 26 | +| Step | Server | Call | |
| 27 | +| ----------------------- | --------- | ------------------------------------------------- | |
| 28 | +| Start a conversation | Cloud | `POST /api/v1/app-conversations` | |
| 29 | +| Resolve agent URL + key | Cloud | `GET /api/v1/app-conversations?ids=<id>` | |
| 30 | +| **Write tags** | **Agent** | `PATCH {conversation_url}` with `{"tags": {...}}` | |
| 31 | +| Read tags back | Cloud | `GET /api/v1/app-conversations?ids=<id>` → `tags` | |
| 32 | + |
| 33 | +Auth uses `X-Session-API-Key` on both servers, but with **different keys**: |
| 34 | + |
| 35 | +- Cloud app server → your `OH_API_KEY` |
| 36 | +- Agent server → the per-conversation `session_api_key` returned by the Cloud |
| 37 | + |
| 38 | +`conversation_url` from the Cloud is already the full agent resource URL |
| 39 | +`https://<agent-host>/api/conversations/<id>`, so you `PATCH` it directly. |
| 40 | + |
| 41 | +**Consistency:** the agent server is authoritative and reflects a `PATCH` |
| 42 | +immediately (`GET {conversation_url}` → `tags`). The Cloud's |
| 43 | +`AppConversation.tags` view is **eventually consistent** — it typically catches |
| 44 | +up within a few seconds — so this example confirms the write on the agent server |
| 45 | +and then *polls* the Cloud read instead of reading once. |
| 46 | + |
| 47 | +> Why not set tags on the Cloud create call? The Cloud |
| 48 | +> `POST/PATCH /api/v1/app-conversations` payloads do not expose `tags` today — |
| 49 | +> the agent server is the authoritative place to write them, and the Cloud |
| 50 | +> reflects the result. The agent `POST /api/conversations` also accepts `tags` |
| 51 | +> at creation time if you provision the sandbox yourself (see |
| 52 | +> [`clone-and-attach`](https://github.com/OpenHands/enterprise-cookbook/tree/f00d8684783a6f687ee209454155e98556b4d0bd/clone-and-attach)). |
| 53 | +
|
| 54 | +## Tag rules |
| 55 | + |
| 56 | +The agent server enforces: |
| 57 | + |
| 58 | +- **keys** must be **lowercase alphanumeric** — no `_` or `-` |
| 59 | + (use `environmenturl`, not `environment_url`; an invalid key is rejected) |
| 60 | +- **values** are arbitrary strings, **≤ 256 characters** |
| 61 | +- `PATCH` **replaces all** tags — so this example does a read-modify-write to |
| 62 | + merge instead of clobbering existing tags |
| 63 | + |
| 64 | +Need to store something structured or longer than 256 chars? Put a JSON string |
| 65 | +into a single tag value (within the limit), or split across multiple keys. |
| 66 | + |
| 67 | +## Run it |
| 68 | + |
| 69 | +```bash |
| 70 | +export OH_API_KEY=... # your https://app.all-hands.dev API key |
| 71 | +pip install requests |
| 72 | + |
| 73 | +# Zero-config: starts a conversation, sets two demo tags, reads them back, |
| 74 | +# then deletes the conversation + sandbox. |
| 75 | +python tag_conversation.py |
| 76 | +``` |
| 77 | + |
| 78 | +Sample output: |
| 79 | + |
| 80 | +```text |
| 81 | +=== start conversation === |
| 82 | + start-task status: STARTING_CONVERSATION |
| 83 | + start-task status: READY |
| 84 | +conversation: b07894c6643c453e9091414056ba4828 |
| 85 | + sandbox status: RUNNING |
| 86 | +agent conversation_url: https://qplbjkyptdumixsu.prod-runtime.all-hands.dev/api/conversations/b07894c6643c453e9091414056ba4828 |
| 87 | +
|
| 88 | +=== set tags (agent server) === |
| 89 | + existing tags: {} |
| 90 | + setting tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'} |
| 91 | + agent tags (authoritative): {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'} |
| 92 | +
|
| 93 | +=== read tags back (cloud server, eventually consistent) === |
| 94 | + AppConversation.tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'} |
| 95 | +
|
| 96 | +round-trip OK: True |
| 97 | +
|
| 98 | +=== cleanup === |
| 99 | + deleted conversation b07894c6643c453e9091414056ba4828 |
| 100 | + deleted sandbox 3NjFZz5JDyIVUdvxNsXi0R |
| 101 | +``` |
| 102 | + |
| 103 | +## Set your own tags |
| 104 | + |
| 105 | +Pass `--tag KEY=VALUE` (repeatable), and `--keep` to leave the conversation open |
| 106 | +so you can inspect the tags in the UI: |
| 107 | + |
| 108 | +```bash |
| 109 | +python tag_conversation.py \ |
| 110 | + --tag environmenturl=https://env.example.com/abc \ |
| 111 | + --tag environmentconversationid=ext-42 \ |
| 112 | + --keep |
| 113 | +``` |
| 114 | + |
| 115 | +| Flag | Env var | Default | Purpose | |
| 116 | +| ---------------- | ----------------- | --------------------------- | ------------------------------------- | |
| 117 | +| `--api-key` | `OH_API_KEY` | — (required) | Cloud API key | |
| 118 | +| `--base-url` | `OH_API_BASE` | `https://app.all-hands.dev` | Cloud app server | |
| 119 | +| `--tag` | — | two demo tags | `KEY=VALUE`, repeatable | |
| 120 | +| `--message` | `INITIAL_MESSAGE` | a hello prompt | First message to the agent | |
| 121 | +| `--sandbox-id` | `SANDBOX_ID` | none | Reuse a RUNNING sandbox | |
| 122 | +| `--keep` | — | off | Don't delete the conversation/sandbox | |
| 123 | +| `--poll-timeout` | `POLL_TIMEOUT` | `240` | Seconds to wait for readiness | |
| 124 | + |
| 125 | +## API endpoints used |
| 126 | + |
| 127 | +| Endpoint | Server | Purpose | |
| 128 | +| ------------------------------------------------ | ------ | ---------------------------------------------------------- | |
| 129 | +| `POST /api/v1/app-conversations` | Cloud | Start a conversation | |
| 130 | +| `GET /api/v1/app-conversations/start-tasks?ids=` | Cloud | Poll for the conversation id | |
| 131 | +| `GET /api/v1/app-conversations?ids=` | Cloud | Resolve `conversation_url`, `session_api_key`, read `tags` | |
| 132 | +| `GET {conversation_url}` | Agent | Read current tags before merging | |
| 133 | +| `PATCH {conversation_url}` | Agent | Set the (merged) tags | |
| 134 | +| `DELETE /api/v1/app-conversations/{id}` | Cloud | Clean up the conversation | |
| 135 | +| `DELETE /api/v1/sandboxes/{id}?sandbox_id=` | Cloud | Clean up the sandbox | |
0 commit comments