Skip to content

Conversation Management

Arbitex organises chat sessions into conversations. Each conversation has a title, an operating mode (Single / Compare / Summarize), a snapshot of the models it was created with, and optional organisational attributes (folder, pin, archive, project).

When a new conversation’s first message is sent, Arbitex automatically generates a concise title in a background task. The auto-title service runs independently of the response stream so it never adds latency to the chat.

  1. The first user role message is sent to a lightweight LLM (default: claude-haiku-4-5-20251001 via the Anthropic provider). The assistant’s first reply is included as context for better title accuracy.
  2. The model receives the system prompt: “Generate a concise 3-5 word title for this conversation. Respond with only the title, no quotes or punctuation.”
  3. Only the first 500 characters of the user message are forwarded to the title model (token cost optimisation). max_tokens=30, temperature=0.3. If the assistant has already replied, its first response (also truncated to 500 characters) is included in the context.
  4. The raw response is sanitised: surrounding quotes stripped, trailing punctuation removed, truncated to 100 characters.
  5. On success, the conversation’s auto_titled flag is set to true. The title generation includes retry logic for transient LLM failures.

If the LLM is disabled, unavailable, or returns an empty response, a fallback title is derived from the first 50 characters of the message text, truncated at the nearest word boundary with "..." appended.

Examples:

First message (excerpt) Generated title
"How do I configure SCIM in Okta?" Configure SCIM Okta
"Write a Python function that..." Python Function Writing
(LLM unavailable) "Explain the difference between..." Explain the difference...

Auto-titling is controlled by two SystemConfig keys:

Key Description Default
auto_title_enabled Enable/disable auto-titling globally true
auto_title_model Model to use for title generation, format "provider/model_id" "anthropic/claude-haiku-4-5-20251001"

Admins can change these via PUT /api/v1/admin/config/{key}.

  • If a conversation already has a manually-set title (even an empty string set intentionally), the auto-title service returns immediately without overwriting it.
  • If auto_title_enabled is false in SystemConfig, the first-N-chars fallback title is used instead.

Users can rename any conversation they own at any time.

PATCH /api/conversations/{conversation_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "SCIM Okta Integration Questions"
}

Response 200 OK — the full conversation object with the updated title.

The new title replaces the existing one and persists. After a manual rename, the auto-title service will not overwrite it (it only acts when title is null or empty at the time of the first message).

Setting title to an empty string "" is allowed and results in the conversation appearing as “Untitled Conversation” in the UI.

Users can request a new auto-generated title for any conversation, even one that has been manually renamed.

POST /api/conversations/{conversation_id}/regenerate-title
Authorization: Bearer <token>

Response 200 OK — the updated conversation object with the new title and auto_titled: true.

The regeneration uses the same LLM and system prompt as initial auto-titling, but includes both the first user message and the assistant’s reply for better context. This is useful when the initial auto-title was too generic or when the conversation topic evolved.

Field Type Description
id UUID Unique conversation identifier
title string | null Conversation title (null until auto-titled)
auto_titled bool true if the current title was set by the auto-title service; false after manual rename
mode "single" | "compare" | "summarize" Operating mode
model_config_snapshot JSONB Snapshot of models selected at creation
model_instructions string | null Custom system prompt for this conversation
is_pinned bool Pinned to top of sidebar
is_archived bool Soft-archived (hidden from default view)
folder_id UUID | null Folder assignment
project_id UUID | null Project cost-tagging
share_token string | null Token for public share link
parent_conversation_id UUID | null Fork source (null if not a fork)
created_at ISO-8601 Creation timestamp
updated_at ISO-8601 Last modification timestamp

Archived conversations are hidden from the default sidebar list but remain accessible via the archive view or direct URL.

PATCH /api/conversations/{conversation_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"is_archived": true
}

To unarchive, send "is_archived": false.

The PATCH endpoint accepts both title and is_archived in the same request.

Pinned conversations float to the top of the sidebar regardless of recency. Pin state is toggled via the same PATCH endpoint:

PATCH /api/conversations/{conversation_id}
Content-Type: application/json
{
"is_pinned": true
}

Each message in a conversation carries a model_id field. The frontend renders a model badge alongside assistant messages to identify which model produced each response. In Compare and Summarize modes (multiple models active), each response column displays the appropriate model badge.

  • Single mode: one badge, matching the model selected for the conversation.
  • Compare mode: N badges (one per model column), rendered side-by-side.
  • Summarize mode: individual model responses are labelled with their model_id; the synthesis message carries the synthesizer model badge.

The model_config_snapshot on the conversation records which models were active at the time the conversation was created. If models are later renamed or deprecated, the badge still shows the original model_id string from the snapshot.

Each Message in the API response includes:

{
"role": "assistant",
"content": "Here is your answer...",
"model_id": "gpt-4o",
"created_at": "2026-03-12T14:22:00Z"
}

For summarizer synthesis messages, role is "summary" and model_id is the configured synthesizer model.

The conversation list endpoint returns conversations ordered by most recently updated, with pagination support.

GET /api/conversations?page=1&page_size=20&search=SCIM
Authorization: Bearer <token>

Query parameters:

Parameter Type Default Description
page int 1 Page number (1-indexed)
page_size int 20 Results per page
search string Full-text search against title and message content (case-insensitive ILIKE)
is_archived bool false Include/exclude archived conversations
folder_id UUID Filter to a specific folder

Search behaviour: The search parameter performs a PostgreSQL ILIKE match against the conversation title column and any message content in the conversation (CAST(content AS text)). This is a substring match — no tokenisation or stemming.

A fork creates an independent copy of a conversation from a chosen message point. The fork title is automatically prefixed with (Fork).

POST /api/conversations/{conversation_id}/fork
Authorization: Bearer <token>
Content-Type: application/json
{
"fork_message_id": "msg-uuid-here"
}

The forked conversation has parent_conversation_id set to the source conversation’s ID and fork_message_id set to the branch point. Messages up to and including the fork point are copied.

Conversations are soft-deleted: the deleted_at timestamp is set. Soft-deleted conversations do not appear in list or search results.

DELETE /api/conversations/{conversation_id}
Authorization: Bearer <token>

Response 204 No Content

Permanent deletion is not available via the user API. Admins can perform hard deletes via GDPR erasure endpoints (see Conversation Export).