Data Sources¶
AXIS supports two data ingestion patterns: CSV upload through the UI (the default) and Postgres auto-load via YAML configuration. You can use both at the same time -- each AXIS page (Evaluate, Monitoring, Human Signals) has its own data pipeline.
At a Glance¶
| CSV Upload | Postgres Auto-Load | |
|---|---|---|
| Setup effort | None | YAML config file per database |
| Data freshness | Manual -- upload when ready | Automatic on startup + periodic incremental sync |
| Best for | Quick exploration, demos, ad-hoc analysis | Production pipelines, recurring data |
| Pages supported | Evaluate, Monitoring, Human Signals | Evaluate, Monitoring, Human Signals |
| Analytics engine | In-memory (per session) | DuckDB embedded store (persistent) |
Option 1: CSV Upload (Default)¶
CSV upload requires no configuration. Start the backend and frontend, then drag-and-drop a CSV file through the UI.
How it works¶
- Navigate to the target page (Evaluate, Monitoring, or Human Signals).
- Use the upload area to select or drag a CSV file.
- AXIS parses, validates, and normalizes the columns automatically.
- Data is held in memory for the duration of the session.
Column normalization¶
AXIS auto-maps common column name variations to its internal schema. For example:
| Your column name | AXIS maps it to |
|---|---|
id, record_id |
dataset_id |
input, prompt, question |
query |
output, response, completion |
actual_output |
expected, ground_truth, reference |
expected_output |
time, created_at |
timestamp |
model, agent |
model_name |
score, metric_value |
metric_score |
metric |
metric_name |
No pre-processing needed
In most cases you can export directly from your evaluation framework or observability tool and upload without renaming columns.
Option 2: Postgres Auto-Load¶
Postgres auto-load executes SQL queries on app startup, syncs data into a local DuckDB analytics store, and populates the dashboard automatically. Each page has its own YAML config file with split queries — one for the dataset/records table and one for the metrics/results table.
| Page | Config file | Top-level key |
|---|---|---|
| Evaluate / Analytics | custom/config/eval_db.yaml |
eval_db |
| Monitoring | custom/config/monitoring_db.yaml |
monitoring_db |
| Human Signals | custom/config/human_signals_db.yaml |
human_signals_db |
| Production / KPIs | custom/config/kpi_db.yaml |
kpi_db |
Step 1 -- Copy the template¶
# Copy the template you need from backend/config/ to custom/config/
cp backend/config/eval_db.yaml.example custom/config/eval_db.yaml # for Evaluate
cp backend/config/monitoring_db.yaml.example custom/config/monitoring_db.yaml # for Monitoring
cp backend/config/human_signals_db.yaml.example custom/config/human_signals_db.yaml # for Human Signals
Or run make setup to copy all templates at once.
Step 2 -- Configure the connection¶
Open the YAML file and set your connection details. You have two options:
Step 3 -- Write split queries¶
Each config uses two SQL queries: dataset_query (records/traces) and results_query (metrics/scores). Both must include a dataset_id column — AXIS joins them in DuckDB.
eval_db:
enabled: true
auto_load: true
url: "postgresql://axis_reader:${DB_PASSWORD}@db.example.com:5432/evals"
dataset_query: |
SELECT
e.id AS dataset_id,
e.experiment_name AS evaluation_name,
e.input AS query,
e.output AS actual_output,
e.expected AS expected_output,
e.metadata AS data_metadata
FROM evaluations e
WHERE e.created_at > NOW() - INTERVAL '7 days'
results_query: |
SELECT
m.eval_id AS dataset_id,
m.metric_name,
m.score AS metric_score
FROM metrics m
JOIN evaluations e ON e.id = m.eval_id
WHERE e.created_at > NOW() - INTERVAL '7 days'
query_timeout: 60
row_limit: 10000
eval_db:
enabled: true
auto_load: true
url: "postgresql://axis_reader:${DB_PASSWORD}@db.example.com:5432/evals"
columns:
my_id_field: dataset_id
user_prompt: query
llm_response: actual_output
gold_answer: expected_output
run_name: evaluation_name
eval_metric: metric_name
eval_score: metric_score
dataset_query: "SELECT * FROM my_evaluations WHERE created_at > NOW() - INTERVAL '7 days'"
results_query: "SELECT * FROM my_metrics WHERE created_at > NOW() - INTERVAL '7 days'"
query_timeout: 60
row_limit: 10000
Both queries must include dataset_id
The dataset_query and results_query are joined on dataset_id in DuckDB. Make sure both queries alias or include a column named dataset_id.
Step 4 -- Restart the backend¶
On startup you will see a log line confirming the auto-load:
Safety limits
AXIS enforces hard caps on query execution:
query_timeout: clamped to a maximum of 120 secondsrow_limit: clamped to a maximum of 50,000 rows
Values above these caps are silently reduced to the maximum.
Monitoring Data Formats¶
The Monitoring module accepts two CSV formats. AXIS auto-detects which format you're using.
Long Format (recommended for production)¶
Each row represents a single metric observation. Used when metrics are stored in a normalized database schema.
dataset_id,query,actual_output,metric_name,metric_score,timestamp,environment,source_name
01KFX...,What is...,The answer...,Faithfulness,0.85,2024-01-15T10:30:00,production,alpha_bot
01KFX...,What is...,The answer...,Relevance,0.92,2024-01-15T10:30:00,production,alpha_bot
Key columns: metric_name (name of the metric) and metric_score (numeric score value).
Wide Format¶
Each row contains all metrics as separate columns. Common when exporting from evaluation runs.
dataset_id,query,actual_output,faithfulness_score,relevance_score,timestamp
01KFX...,What is...,The answer...,0.85,0.92,2024-01-15T10:30:00
Columns ending in _score are auto-detected as metrics.
Format detection logic
If the data contains both metric_name and metric_score columns, AXIS uses
long format. Otherwise, it scans for columns ending in _score (wide format).
Metric Categories¶
The metric_category column in monitoring data controls how metrics are displayed in the Monitoring dashboard. Three categories are supported:
| Category | Description | Example values | UI rendering |
|---|---|---|---|
SCORE |
Numeric scores (default) | 0.85, 0.92 |
Time-series charts, pass/fail thresholds, sparklines |
CLASSIFICATION |
Categorical labels | "POSITIVE", "HALLUCINATION" |
Category breakdowns, stacked bar charts, distribution panels |
ANALYSIS |
Structured insights/reasoning | {"issues": [...]} |
Detail views with formatted JSON, paginated insights table |
How to use¶
Add metric_category to your monitoring data or SQL query:
If metric_category is omitted, all metrics default to SCORE.
Monitoring tabs
The Monitoring page shows separate tabs for each category type:
- Score tab — numeric metric trends, pass rates, latency distribution
- Classification tab — category value counts and trends (only appears when CLASSIFICATION metrics exist)
- Analysis tab — paginated structured insights (only appears when ANALYSIS metrics exist)
Mixing Both Approaches¶
You can auto-load data from Postgres for one page and use CSV upload for another. For example:
- Monitoring: auto-load from your observability database via
monitoring_db.yaml - Evaluate: upload experiment CSVs manually through the UI
- Human Signals: auto-load HITL cases from your support database via
human_signals_db.yaml
Each page's data source is independent.
DuckDB Analytics Store¶
When using Postgres auto-load, data is synced into an embedded DuckDB analytics store that acts as a local cache. This provides fast analytical queries without hitting your source database on every request.
How it works¶
- On startup, each configured database runs its
dataset_queryandresults_queryconcurrently against Postgres - Results are written to internal DuckDB tables (e.g.,
monitoring_dataset,monitoring_results) - A JOIN view is created (e.g.,
monitoring_data) that combines both tables ondataset_id - Subsequent API requests read from DuckDB, not Postgres
Incremental sync¶
Add incremental_column to your YAML config to enable watermark-based incremental sync — only new rows are appended on each refresh:
monitoring_db:
incremental_column: created_at
refresh_interval_minutes: 15 # auto-sync every 15 minutes
Learn more¶
- DuckDB Configuration -- store settings, sync workers, concurrency
- DuckDB Architecture -- technical deep-dive into the sync engine
- DuckDB Sync Runbook -- production sync patterns
Env-Var Fallback¶
If you prefer environment variables over YAML files, you can configure database connections entirely through backend/.env. However, YAML files offer additional features not available via env vars:
| Feature | YAML | Env vars |
|---|---|---|
| Split queries | dataset_query + results_query |
Not available |
| Column mappings | columns map |
Not available |
| Incremental sync | incremental_column field |
Not available |
| Periodic refresh | refresh_interval_minutes |
Not available |
| Row limit / timeout | Per-file | Per-database (eval only) |
| Auto-load with query | All three databases | Eval DB only |
Note
If a YAML file exists for a database, its env vars are ignored entirely. Delete or rename the YAML file to fall back to env vars.
See Environment Variables for the complete env var reference and YAML Configs for the full YAML schema.
Troubleshooting¶
My data does not appear after startup
Check the backend logs for connection errors. Common issues:
enabled: false-- the master switch is offauto_load: false-- the data is configured but not loading automatically- Incorrect
urlor host/port/database values - Firewall blocking the database port
${DB_PASSWORD}placeholder without the env var set
Columns are not mapping correctly
- Verify your
columnsmapping in the YAML matches your actual database column names - Alternatively, use
ASaliases in your SQL query to rename columns - Check the AXIS schema for the target column names in YAML Configs
Query is timing out
- Increase
query_timeout(max 120 seconds) - Add a
WHEREclause to limit the date range - Reduce
row_limitto fetch fewer rows - Ensure the database has appropriate indexes on filtered columns
- Add
partition_columnto enable parallel COPY reads for large tables
DuckDB sync is slow or stuck
- Check sync status:
curl http://localhost:8500/api/store/status - Force a full rebuild:
curl -X POST http://localhost:8500/api/store/sync/monitoring?full=true - Reset watermarks:
curl -X POST http://localhost:8500/api/store/sync/monitoring/reset-watermark - See the DuckDB Sync Runbook for troubleshooting details