Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions cookbook/conversation-tags.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: Conversation tags
description: Attach key-value metadata to a conversation with tags and read it back from AppConversation.tags.
icon: tags
---

{/* GENERATED from OpenHands/enterprise-cookbook@f00d8684783a6f687ee209454155e98556b4d0bd (conversation-tags/README.md). Edit the source, not this file. */}

<Card title="View source on GitHub" icon="github" href="https://github.com/OpenHands/enterprise-cookbook/tree/f00d8684783a6f687ee209454155e98556b4d0bd/conversation-tags" horizontal />

Stash your own key-value metadata on an OpenHands conversation — for example an
external `environment_url` or `environment_conversation_id` — and read it back
later from your own tooling. Conversations expose a free-form **`tags`** map for
exactly this.

This is the supported replacement for adding a bespoke field (e.g. a custom
`environment_url` column) to the conversation model: use `tags` instead.

## The two-server split

OpenHands has a **Cloud app server** (manages accounts, sandboxes, and
conversations) and, for each sandbox, an **agent server** (the runtime that owns
the conversation). Tags live on the agent-side conversation, and their values
surface on the Cloud's `AppConversation.tags` field.

| Step | Server | Call |
| ----------------------- | --------- | ------------------------------------------------- |
| Start a conversation | Cloud | `POST /api/v1/app-conversations` |
| Resolve agent URL + key | Cloud | `GET /api/v1/app-conversations?ids=<id>` |
| **Write tags** | **Agent** | `PATCH {conversation_url}` with `{"tags": {...}}` |
| Read tags back | Cloud | `GET /api/v1/app-conversations?ids=<id>` → `tags` |

Auth uses `X-Session-API-Key` on both servers, but with **different keys**:

- Cloud app server → your `OH_API_KEY`
- Agent server → the per-conversation `session_api_key` returned by the Cloud

`conversation_url` from the Cloud is already the full agent resource URL
`https://<agent-host>/api/conversations/<id>`, so you `PATCH` it directly.

**Consistency:** the agent server is authoritative and reflects a `PATCH`
immediately (`GET {conversation_url}` → `tags`). The Cloud's
`AppConversation.tags` view is **eventually consistent** — it typically catches
up within a few seconds — so this example confirms the write on the agent server
and then *polls* the Cloud read instead of reading once.

> Why not set tags on the Cloud create call? The Cloud
> `POST/PATCH /api/v1/app-conversations` payloads do not expose `tags` today —
> the agent server is the authoritative place to write them, and the Cloud
> reflects the result. The agent `POST /api/conversations` also accepts `tags`
> at creation time if you provision the sandbox yourself (see
> [`clone-and-attach`](https://github.com/OpenHands/enterprise-cookbook/tree/f00d8684783a6f687ee209454155e98556b4d0bd/clone-and-attach)).

## Tag rules

The agent server enforces:

- **keys** must be **lowercase alphanumeric** — no `_` or `-`
(use `environmenturl`, not `environment_url`; an invalid key is rejected)
- **values** are arbitrary strings, **≤ 256 characters**
- `PATCH` **replaces all** tags — so this example does a read-modify-write to
merge instead of clobbering existing tags

Need to store something structured or longer than 256 chars? Put a JSON string
into a single tag value (within the limit), or split across multiple keys.

## Run it

```bash
export OH_API_KEY=... # your https://app.all-hands.dev API key
pip install requests

# Zero-config: starts a conversation, sets two demo tags, reads them back,
# then deletes the conversation + sandbox.
python tag_conversation.py
```

Sample output:

```text
=== start conversation ===
start-task status: STARTING_CONVERSATION
start-task status: READY
conversation: b07894c6643c453e9091414056ba4828
sandbox status: RUNNING
agent conversation_url: https://qplbjkyptdumixsu.prod-runtime.all-hands.dev/api/conversations/b07894c6643c453e9091414056ba4828

=== set tags (agent server) ===
existing tags: {}
setting tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}
agent tags (authoritative): {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}

=== read tags back (cloud server, eventually consistent) ===
AppConversation.tags: {'environmenturl': 'https://env.example.com/session/abc123', 'environmentconversationid': 'ext-0001'}

round-trip OK: True

=== cleanup ===
deleted conversation b07894c6643c453e9091414056ba4828
deleted sandbox 3NjFZz5JDyIVUdvxNsXi0R
```

## Set your own tags

Pass `--tag KEY=VALUE` (repeatable), and `--keep` to leave the conversation open
so you can inspect the tags in the UI:

```bash
python tag_conversation.py \
--tag environmenturl=https://env.example.com/abc \
--tag environmentconversationid=ext-42 \
--keep
```

| Flag | Env var | Default | Purpose |
| ---------------- | ----------------- | --------------------------- | ------------------------------------- |
| `--api-key` | `OH_API_KEY` | — (required) | Cloud API key |
| `--base-url` | `OH_API_BASE` | `https://app.all-hands.dev` | Cloud app server |
| `--tag` | — | two demo tags | `KEY=VALUE`, repeatable |
| `--message` | `INITIAL_MESSAGE` | a hello prompt | First message to the agent |
| `--sandbox-id` | `SANDBOX_ID` | none | Reuse a RUNNING sandbox |
| `--keep` | — | off | Don't delete the conversation/sandbox |
| `--poll-timeout` | `POLL_TIMEOUT` | `240` | Seconds to wait for readiness |

## API endpoints used

| Endpoint | Server | Purpose |
| ------------------------------------------------ | ------ | ---------------------------------------------------------- |
| `POST /api/v1/app-conversations` | Cloud | Start a conversation |
| `GET /api/v1/app-conversations/start-tasks?ids=` | Cloud | Poll for the conversation id |
| `GET /api/v1/app-conversations?ids=` | Cloud | Resolve `conversation_url`, `session_api_key`, read `tags` |
| `GET {conversation_url}` | Agent | Read current tags before merging |
| `PATCH {conversation_url}` | Agent | Set the (merged) tags |
| `DELETE /api/v1/app-conversations/{id}` | Cloud | Clean up the conversation |
| `DELETE /api/v1/sandboxes/{id}?sandbox_id=` | Cloud | Clean up the sandbox |
20 changes: 20 additions & 0 deletions cookbook/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Cookbook
description: Runnable examples for building on the OpenHands API.
---

{/* GENERATED from OpenHands/enterprise-cookbook@f00d8684783a6f687ee209454155e98556b4d0bd (cookbook.yaml). Edit the source, not this file. */}

Standalone, runnable examples for the OpenHands API. Each page is generated from an
example in [OpenHands/enterprise-cookbook](https://github.com/OpenHands/enterprise-cookbook),
where you will find the full source.

## Conversation monitoring & reacting

Observe conversations and react to their state.

<CardGroup cols={2}>
<Card title="Conversation tags" icon="tags" href="/cookbook/conversation-tags">
Attach key-value metadata to a conversation with tags and read it back from AppConversation.tags.
</Card>
</CardGroup>
17 changes: 17 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,23 @@
]
}
]
},
{
"tab": "Cookbook",
"groups": [
{
"group": "Overview",
"pages": [
"cookbook/index"
]
},
{
"group": "Conversation monitoring & reacting",
"pages": [
"cookbook/conversation-tags"
]
}
]
}
]
},
Expand Down
Loading