Feedback Result Persistence#
Feedback evaluation results in TruLens flow through three layers: the schema (in-memory Pydantic models), the ORM (SQLAlchemy mappings), and the database (trulens_feedbacks table). The dashboard reads back this data via get_records_and_feedback and renders per-call metadata through a dedicated detail view.
Schema: FeedbackResult and FeedbackCall#
The two core serializable types live in schema/feedback.py:
-
FeedbackResultβ top-level result for oneFeedbackinstance against one record. Key fields:feedback_result_idβ content-addressed ID derived viaobj_id_of_objon amodel_dump()record_id,feedback_definition_idβ foreign keys to records and feedback definitionsstatusβFeedbackResultStatusenum:NONE,RUNNING,DONE,FAILED,SKIPPEDcallsβ list ofFeedbackCallobjects (one per input combination)resultβ final aggregated float;errorβ traceback string on failure;multi_resultβ JSON-encoded dict for multi-valued feedbacks
-
FeedbackCallβ records a single invocation of the feedback function withargs(selector-extracted inputs),ret(float or list), andmeta(arbitrary dict for extra data such as reasoning).
A single feedback evaluation can produce multiple FeedbackCall records when selectors match more than one value (controlled by FeedbackCombinations: ZIP or PRODUCT). The agg function then reduces these to the scalar result.
Database Table: trulens_feedbacks#
The ORM mapping is in database/orm.py. The table name is trulens_feedbacks (prefix + feedbacks). Columns:
| Column | Type | Notes |
|---|---|---|
feedback_result_id | VARCHAR(256) PK | Content-addressed ID |
record_id | VARCHAR(256) FK β trulens_records | |
feedback_definition_id | VARCHAR(256) FK β trulens_feedback_defs | |
last_ts | Float (Unix timestamp) | |
status | Text (enum value) | NONE / RUNNING / DONE / FAILED / SKIPPED |
error | Text | Traceback on failure |
calls_json | Text | JSON of {"calls": [...FeedbackCall dicts...]} |
result | Float | Aggregated score, NULL on failure |
name | Text | Display name |
cost_json | Text | JSON-serialized Cost |
multi_result | Text | JSON dict for multi-valued outputs |
The ORM.FeedbackResult.parse() classmethod does the serialization: all FeedbackCall objects are packed together into calls_json via json_str_of_obj(dict(calls=obj.calls)) . All JSON columns use SQLAlchemy Text (not native JSON) for cross-database compatibility .
Write Path: run_and_log#
Feedback.run_and_log() is the main entry point for executing and persisting a feedback result:
- A placeholder
FeedbackResultwithstatus=RUNNINGis immediately written viadb.insert_feedback(). Feedback.run()executes the implementation, buildsFeedbackCallobjects for each input combination, aggregates them withself.agg, and returns aFeedbackResultwithstatus=DONE(orFAILED).- The final result is upserted via
db.insert_feedback(feedback_result).
SQLAlchemyDB.insert_feedback() uses session.merge() for thread-safe upsert semantics. A Snowflake-specific workaround inserts -1 for nullable floats first, then updates to NULL, because the Snowflake connector cannot bind None to numeric parameters in the same statement .
For deferred mode, Feedback.evaluate_deferred() queries rows with status in [NONE, FAILED, RUNNING] and dispatches them to a thread pool. RUNNING rows are only retried after TruSession.RETRY_RUNNING_SECONDS has elapsed .
The DBConnector.add_feedback() method is the public API used externally (e.g., for logging human feedback). It accepts a FeedbackResult, a Future[FeedbackResult], or raw kwargs.
Dashboard: Retrieval and Record Limits#
The Records page (pages/Records.py) fetches data through get_records_and_feedback() in dashboard_utils.py, which is decorated with @st.cache_data (15-minute TTL). This in turn calls SQLAlchemyDB.get_records_and_feedback(), which joins trulens_records, trulens_feedbacks, and trulens_apps, ordering by timestamp descending.
Record limit: The UI defaults to RECORDS_LIMIT = 1000 , stored in Streamlit session state under the key ST_RECORDS_LIMIT = "records_limit" . When the result set hits the limit, a banner prompts the user with a "Show all" button that sets the limit to None . Feedback score columns are identified by feedback_col_names returned alongside the DataFrame.
OTel vs. legacy: If OTel tracing is enabled, get_records_and_feedback delegates to _get_records_and_feedback_otel(), which reconstructs records from the trulens_events table using RECORD_ROOT, EVAL, and EVAL_ROOT span types .
get_feedback_defs() is called separately to populate feedback_directions (the higher_is_better per-feedback flag), which drives color-coding in the grid.
Dashboard: Detailed Feedback View#
When a user selects a record in the grid, _render_trace() renders a detail panel. The "Feedback Results" section uses two helpers from records_utils.py:
-
_render_feedback_pills()β renders a pill/selectbox with score and icon for each feedback column that has a non-null result. Icons come fromCATEGORY.of_score()based on direction. -
_render_feedback_call()β reads{feedback_col}_callsfrom the row (the deserializedcalls_json), then callsdisplay_feedback_call()which:- Separates
EVAL_ROOTandEVALspans (for OTel) from legacy{args, ret, meta}dicts - For OTel: filters to the most recent
eval_root_idto avoid duplicate evaluations - Builds a DataFrame with
args,score(fromret), and flattenedmetacolumns - For groundedness feedbacks: expands the
reasons/explanationfield into per-statement rows viaexpand_groundedness_df() - Applies row-level color highlighting via
highlight()
- Separates
The meta dict on each FeedbackCall is the vehicle for surfacing per-call context β such as reasoning text from LLM-graded feedback β directly in the UI. It is stored inside calls_json in the database and expanded into DataFrame columns at display time .