Human Signals
Data-driven dashboard for human-in-the-loop metrics — track interventions, escalations, sentiment, and operational patterns across your AI agents.
Why Use Human Signals?
Automated evaluation scores tell you how well your AI performs. Human Signals tells you what happens when humans step in — the overrides, escalations, sentiment shifts, and learnings that only surface through real operator interactions.
Intervention Tracking
See when and why humans override AI decisions — correction, escalation, edge case, or full manual takeover.
Dynamic KPIs
Auto-generated KPI cards with sparklines. Click any KPI to expand its weekly trend chart.
Config-Driven Charts
Bar, donut, stacked bar, horizontal bar, ranked list, and single stat charts — all driven by your display config.
Full Conversation View
Drill into any case to see the complete chat thread, signal values, learnings, and feature requests.
Quick Start
Get human signals data loaded in under two minutes:
Navigate to Human Signals
Click Human Signals in the left sidebar. If no data is loaded, you'll see the upload screen.
Upload CSV or Connect Database
Drag a CSV file into the upload zone, or configure config/human_signals_db.yaml for automatic database import. The expected format uses metric_name, dataset_id, signals, conversation, and timestamp columns.
Explore Your Dashboard
Once data loads, the dashboard populates with KPI cards, trend charts, distribution charts, and a case table. Use the time range selector and filters to slice by source, environment, or metric values.
human_signals_db.yaml with auto_load: true. The sync engine will pull data from your PostgreSQL database automatically on page load, using the DuckDB pipeline for fast queries.
Page Anatomy
Here's how the Human Signals dashboard is organized, with every major section labeled:
Human Signals
Human-in-the-loop insights and operational metrics
Signal Trends Over Time
What AI Did Well
Outcome Analysis
match_value), numeric aggregations, and aggregates.table_badge_columns in the YAML config.KPI Strip
The KPI strip auto-generates cards from your display_config.kpi_strip configuration. Each card shows a metric icon, a large value, an optional sparkline, and a descriptive label.
AI Success Rate — Weekly Trend
×KPI card types:
- Aggregate KPIs —
total_cases(count). No sparkline. - Boolean rate KPIs — percentage of cases where a boolean signal is true (e.g., resolution rate, escalation rate). Includes a sparkline and is clickable to expand a trend chart.
- Match value KPIs — percentage of cases where a string signal equals a specific value. Set
match_valuein the config (e.g.,match_value: "success"to count cases whereanalysis_mode == "success"). - Numeric aggregation KPIs — aggregated numeric signals using
aggregation(mean,median,sum,min,max,p95). Supportsdurationandcompactformats.
signals_metrics.yaml under the kpi_strip key. Each entry specifies label, icon, metric, signal, format (percent, number, duration, compact), and optionally match_value, aggregation, and highlight.
Filters & Time Range
The filter bar sits at the top of the dashboard with the time range selector inline on the right. Click the bar to expand the filter panel, which shows a responsive grid of dropdown selectors.
Two filter categories:
- Source filters — filter by
source_componentandenvironment(single-select). Only appear if your data contains those fields. - Metric filters — filter by signal values like override type, sentiment, escalation type (multi-select with checkboxes). Only appear for signals with 8 or fewer unique values.
The active filter count badge shows how many filters are currently applied. The Clear button resets all source and metric filters. The time range selector offers presets (7d, 30d, 90d, 6mo, 1yr) and a custom date range picker.
Chart Section
The chart section contains two areas: the Signal Trends time-series chart and the config-driven chart sections with various visualization types. All sections are collapsible and start collapsed by default to keep the page compact.
Signal Trends Over Time
A collapsible multi-line chart plotting signal rates as weekly percentages. Each line represents a KPI that has format: percent in the display config. The chart automatically extracts trend signals from your KPI configuration. Click the section header to expand.
Chart Types
Below the trend chart, chart sections render in configurable grid layouts (full, grid_2, or grid_3). Each section has a collapsible title with a chevron toggle. The available chart types are:
Intervention Type (Bar)
Sentiment (Donut)
Failed Step (Horizontal Bar)
Override Breakdown (Stacked Bar)
Top Learning Categories (Ranked List)
Actionable Feedback (Single Stat)
| Chart Type | Config Value | Best For |
|---|---|---|
| Bar | bar | Comparing category counts (intervention types, failed steps) |
| Donut | donut | Proportional breakdowns (sentiment distribution) |
| Horizontal Bar | horizontal_bar | Ranked categories with long labels |
| Stacked Bar | stacked_bar | Part-of-whole comparisons |
| Ranked List | ranked_list | Top-N frequency lists (learning categories, feature requests) |
| Single Stat | single_stat | Boolean rate as a single large number (actionable feedback %) |
Case Table
The full paginated case table appears below the charts. Each row represents one aggregated case (one dataset_id) with all its flattened signal values.
| Case ID | Source ↕ | Intervention ↕ | Sentiment | Failed Step | Messages | Timestamp ↕ | |
|---|---|---|---|---|---|---|---|
| CS-001-abc | alpha_bot | correction | positive | retrieval | 12 | Jan 28, 2026 | 👁 |
| CS-002-def | alpha_bot | escalation | negative | generation | 8 | Jan 27, 2026 | 👁 |
| CS-003-ghi | beta_bot | edge_case | neutral | — | 5 | Jan 26, 2026 | 👁 |
| CS-004-jkl | alpha_bot | manual | positive | reasoning | 15 | Jan 25, 2026 | 👁 |
Table features:
- Column Picker — click the "Columns" button to show/hide columns. Defaults to 7 of the most important columns.
- Sortable columns — click any column header with ↕ to sort ascending, descending, or reset. Uses lexicographic comparison with numeric awareness.
- Color-coded badges — signal values render as colored badges using
color_mapsfrom your display config. Control which columns show badges with thetable_badge_columnslist insignals_metrics.yaml. Omit the list to badge all mapped columns. - Pagination — configurable page size (10, 25, 50, 100). Page pills with a 5-page sliding window around the current page.
- Eye icon — click to open the Case Detail Modal.
Case Detail Modal
Click the eye icon on any case row to open a full detail view. The modal shows every signal value, metadata, the complete conversation thread, learnings, and feature requests.
Modal sections (top to bottom):
- Header — Case ID with source badge and close button
- Slack Link — purple button linking to the original Slack thread (if
message_urlis present inadditional_input) - Status Badges — color-coded badges for intervention type, sentiment, resolution status, priority, etc. Colors come from
color_maps - Metadata Row — timestamp, message count, business name, agent name
- Metric Sections — grouped by metric name, with simple signals in a 2-column grid and complex/structured signals rendered full-width with nested key-value displays
- Suggested Action — highlighted panel when actionable feedback is detected
- Learnings — expandable section with numbered learning items and category tags
- Feature Requests — expandable section with numbered feature request items
- Full Conversation — expandable chat view with assistant (left) and user (right) message bubbles, Slack markdown parsing
Data Format
Human Signals uses a long format where each row is one metric observation for one case. Cases are grouped by dataset_id and signals are flattened as {metric_name}__{signal_key}.
Required Columns
dataset_id,metric_name,signals,source_name,timestamp
Full CSV Schema
dataset_id,metric_name,metric_score,metric_category,signals,conversation,conversation_stats,additional_input,source_name,source_component,environment,timestamp
| Column | Required | Description |
|---|---|---|
dataset_id | Yes | Unique case identifier. All metric rows with the same ID are grouped into one case. |
metric_name | Yes | Name of the metric (e.g., intervention_type, sentiment_category) |
signals | Yes | JSON dict of signal key-value pairs (e.g., {"intervention_type": "correction", "has_intervention": true}) |
source_name | Yes | Source system name (e.g., alpha_bot). Used for filtering and display. |
timestamp | No | ISO datetime. Used for time-range filtering and trend charts. |
conversation | No | JSON with {"messages": [{"role": "...", "content": "..."}]} |
conversation_stats | No | JSON with turn counts (e.g., {"turn_count": 12}) |
additional_input | No | JSON with extra metadata (e.g., {"message_url": "...", "sender": "..."}) |
source_component | No | Component within source (used for filters) |
environment | No | Deployment environment (used for filters) |
How Flattening Works
The backend groups rows by dataset_id, then for each metric row flattens all signal keys as {metric_name}__{signal_key}. For example:
# Input row:
metric_name: "intervention_type"
signals: {"intervention_type": "correction", "has_intervention": true}
# Becomes flattened case fields:
intervention_type__intervention_type: "correction"
intervention_type__has_intervention: true
signals column must contain valid JSON (or Python dict syntax). The backend tries JSON parsing first, then falls back to ast.literal_eval. Malformed signals will be silently skipped.
Configuration
The display layout is auto-generated from the metric schema but can be customized via config/signals_metrics.yaml. The backend generates a display_config that controls KPIs, charts, filters, table columns, and color maps.
Display Config Structure
{
"kpi_strip": [
{ "label": "Total Cases", "aggregate": "total_cases", "icon": "zap", "highlight": true },
{ "label": "Intervention Rate", "metric": "intervention_type", "signal": "has_intervention",
"format": "percent", "icon": "alert-triangle" }
],
"chart_sections": [
{
"title": "Outcome Distribution",
"layout": "grid_2",
"charts": [
{ "metric": "intervention_type", "signal": "intervention_type",
"type": "donut", "title": "Intervention Type" }
]
}
],
"filters": [
{ "type": "source", "field": "source_component", "label": "Component" },
{ "type": "metric", "metric": "sentiment_category", "signal": "sentiment",
"label": "Sentiment", "options": ["positive", "neutral", "negative"] }
],
"table_columns": [
{ "key": "Case_ID", "label": "Case ID", "sortable": true },
{ "key": "intervention_type__intervention_type", "label": "Intervention", "sortable": true }
],
"color_maps": {
"intervention_type__intervention_type": {
"correction": "#4f46e5", "escalation": "#ca8a04", "edge_case": "#16a34a"
}
}
}
Database Configuration
To auto-import from a PostgreSQL database, create config/human_signals_db.yaml from the example template:
human_signals_db:
enabled: true
auto_load: true
url: "postgresql://user:pass@host:5432/db"
dataset_query: |
SELECT dataset_id, conversation, additional_input, timestamp
FROM hitl_cases WHERE created_at > NOW() - INTERVAL '30 days'
results_query: |
SELECT dataset_id, metric_name, signals, source_name, timestamp
FROM hitl_results WHERE timestamp > NOW() - INTERVAL '30 days'
query_timeout: 60
row_limit: 10000
visible_metrics: [] # Empty = show all
visible_metrics to limit which metrics appear on the dashboard. This is useful when your database contains many metric types but you only want to surface a curated subset.