# Streamlit + Snowflake Cortex Agent: How It Works

This document explains how the Streamlit chat app (`app.py`) connects to and interacts with the Snowflake Cortex Agent REST API.

---

## 0. Running the App

### Install dependencies

```bash
pip install -e .
```

### Run

```bash
streamlit run app.py
```

Streamlit will open the app in your browser automatically. On first load, a browser popup will appear for Snowflake SSO authentication.

### Prerequisites

- `.streamlit/secrets.toml` must exist with valid Snowflake credentials (see section 1)
- `config.toml` must exist in the project root (see section 9)

---

## 1. Authentication

The app uses `snowflake-connector-python` with `externalbrowser` (SSO) authentication. Credentials are stored in `.streamlit/secrets.toml`:

```toml
[connections.snowflake]
account = "sme-orchard"
user = "rbomberg@sonymusic-pde.com"
authenticator = "externalbrowser"
role = "FACTS_DEV_ML_READWRITE"
database = "DEV_ENGINEERING"
schema = "DEV_RBOMBERG"
```

On first request, the Snowflake connector opens a browser window for SSO login. The connection is cached via `@st.cache_resource` so authentication only happens once per app session.

The REST API calls require a Snowflake session token, extracted from the connector:

```python
conn = get_snowflake_connection()
token = conn.rest.token
host = conn.host  # e.g. "sme-orchard.snowflakecomputing.com"
```

All API requests use the header:

```
Authorization: Snowflake Token="<token>"
```

---

## 2. Conversation Threading

The app uses Snowflake's server-side Threads API to maintain conversation context, rather than re-sending the full message history with each request.

### Creating a thread

On the first user message, the app creates a thread:

```
POST https://{host}/api/v2/cortex/threads
```

This returns a `thread_id` which is stored in `st.session_state.thread_id`.

### Multi-turn conversation

Each subsequent message is sent with:

- `thread_id` — identifies the conversation
- `parent_message_id` — the `message_id` of the previous assistant response (extracted from SSE `metadata` events)

The server maintains full conversation history, so the payload only contains the single new user message:

```json
{
  "messages": [
    {"role": "user", "content": [{"type": "text", "text": "..."}]}
  ],
  "thread_id": "abc-123",
  "parent_message_id": 42
}
```

### Resetting a conversation

The sidebar "New Conversation" button clears `thread_id`, `parent_message_id`, and the local message history. The next message creates a fresh thread.

---

## 3. Calling the Agent

The agent is invoked via:

```
POST https://{host}/api/v2/databases/{AGENT_DATABASE}/schemas/{AGENT_SCHEMA}/agents/{AGENT_NAME}:run
```

The database, schema, and agent name are read from `config.toml` at startup (see section 9).

The response is a **Server-Sent Events (SSE)** stream. Each SSE frame has two lines:

```
event: <event_type>
data: <json_payload>
```

The app reads lines from the stream and dispatches based on `event_type`.

---

## 4. SSE Event Types

The agent emits several event types during a response. Here is how each is handled:

### `metadata`

Sent at the start of the assistant's response. Contains the assistant's `message_id`, which becomes the `parent_message_id` for the next turn.

```json
{
  "metadata": {
    "role": "assistant",
    "message_id": 42
  }
}
```

**App behavior:** Extracts and stores `message_id`.

### `response.text.delta`

Incremental text chunks streamed as the agent generates its answer.

```json
{
  "text": "The top 10 songs globally are"
}
```

**App behavior:** Appends each chunk to a buffer and updates an `st.empty()` placeholder with the accumulated text plus a `▌` cursor. This gives the user a real-time streaming effect.

### `response.chart`

Emitted when the agent produces a visualization. Contains a Vega-Lite chart specification as a JSON string.

```json
{
  "chart_spec": "{\"$schema\":\"https://vega.github.io/schema/vega-lite/v5.json\", ...}"
}
```

**App behavior:** Parses the `chart_spec` JSON string into a dict, collects it, and renders it with `st.vega_lite_chart(spec, use_container_width=True)` after all text streaming is complete.

### `response`

A final summary event. Used as a fallback to extract `assistant_message_id` if the `metadata` event didn't provide it.

**App behavior:** Checks for `assistant_message_id` in the payload only if one hasn't already been captured.

### `done`

Signals the end of the SSE stream. Has no meaningful data payload.

**App behavior:** Breaks out of the stream-reading loop. At this point the app finalizes the text display (removes the cursor), renders any collected charts, and saves the full response to session state.

---

## 5. Rendering Flow

1. **Chat history** — On each Streamlit rerun, all previous messages are rendered from `st.session_state.messages`. Each message stores its `content` (text) and `charts` (list of Vega-Lite specs). Text is rendered with `st.markdown`, charts with `st.vega_lite_chart`.

2. **Live streaming** — When a new message is sent, the assistant response area uses `st.empty()` as a placeholder that gets updated with each text delta. Charts are collected during streaming and rendered after text is complete.

3. **Suggestion chips** — When the conversation is empty, pre-built suggestion pills are shown. Clicking one injects the corresponding question as a user message.

---

## 6. Session State

| Key | Type | Purpose |
|-----|------|---------|
| `messages` | `list[dict]` | Full local chat history. Each dict has `role`, `content`, and optionally `charts`. |
| `thread_id` | `str \| None` | Server-side thread identifier. Created on first message, reset on "New Conversation". |
| `parent_message_id` | `int` | The `message_id` from the last assistant response. Sent with each request so the server knows which message to continue from. |

---

## 7. Dependencies

```
snowflake-connector-python>=3.3.0   # Snowflake auth + token extraction
streamlit[snowflake]>=1.54.0        # Chat UI, vega_lite_chart, session state
requests                            # HTTP calls to the Agent and Threads APIs
tomli>=2.0.0                        # TOML config parsing (Python < 3.11 only; stdlib tomllib used on 3.11+)
```

---

## 8. Key Design Decisions

- **Server-side threads over client-side history** — The Threads API lets the server manage conversation context. Each request sends only the new message, keeping payloads small and avoiding duplicated context.
- **Generator-based streaming** — `call_agent()` is a Python generator that yields typed event dicts (`text`, `chart`, `done`). This lets the UI loop handle each event type incrementally without buffering the entire response.
- **Manual `st.empty()` over `st.write_stream`** — `st.write_stream` only supports text. Using `st.empty()` with manual updates allows interleaving streamed text and chart rendering in the same response.
- **`verify=False` on HTTPS requests** — Used to avoid SSL certificate issues in local development. Should be removed or replaced with proper cert handling for production.

---

## 9. Configuration (`config.toml`)

Agent identity and all UI strings are driven by `config.toml` in the project root. Edit this file to point the app at a different Cortex agent or change the UI copy without touching `app.py`.

```toml
[agent]
database = "DEV_ENGINEERING"   # Snowflake database containing the agent
schema   = "DEV_RBOMBERG"      # Schema containing the agent
name     = "CHATBOT_AGENT"     # Agent object name

[ui]
title            = "Music Analytics Chat"
caption          = "..."
chat_placeholder = "..."

[[ui.suggestions]]
label  = ":blue[:material/music_note:] Top 10 songs globally"
prompt = "What are the top 10 songs globally by streams in the last 7 days?"
# Add or remove [[ui.suggestions]] blocks to change the suggestion chips
```
