Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gateway/src/routes/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ pub fn build_internal_non_otel_enabled_routes() -> Router<AppStateData> {
"/internal/functions/{function_name}/inference-stats/{metric_name}",
get(endpoints::internal::inference_stats::get_inference_with_feedback_stats_handler),
)
.route(
"/internal/feedback/{target_id}/latest-id-by-metric",
get(endpoints::feedback::internal::get_latest_feedback_id_by_metric_handler),
)
.route(
"/internal/model_inferences/{inference_id}",
get(endpoints::internal::model_inferences::get_model_inferences_handler),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type LatestFeedbackIdByMetricResponse = {
feedback_id_by_metric: { [key in string]?: string };
};
1 change: 1 addition & 0 deletions internal/tensorzero-node/lib/bindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export * from "./LLMJudgeIncludeConfig";
export * from "./LLMJudgeInputFormat";
export * from "./LLMJudgeOptimize";
export * from "./LLMJudgeOutputType";
export * from "./LatestFeedbackIdByMetricResponse";
export * from "./LaunchOptimizationParams";
export * from "./LaunchOptimizationWorkflowParams";
export * from "./ListDatapointsRequest";
Expand Down
67 changes: 66 additions & 1 deletion tensorzero-core/src/db/clickhouse/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
feedback::{
BooleanMetricFeedbackRow, CommentFeedbackRow, CumulativeFeedbackTimeSeriesPoint,
DemonstrationFeedbackRow, FeedbackBounds, FeedbackBoundsByType, FeedbackByVariant,
FeedbackRow, FloatMetricFeedbackRow, MetricWithFeedback,
FeedbackRow, FloatMetricFeedbackRow, LatestFeedbackRow, MetricWithFeedback,
},
},
error::{Error, ErrorDetails},
Expand Down Expand Up @@ -491,6 +491,35 @@ fn build_metrics_with_feedback_query(
(query, query_params)
}

/// Builds the SQL query for getting the latest feedback ID for each metric for a target
fn build_latest_feedback_id_by_metric_query(target_id: Uuid) -> (String, HashMap<String, String>) {
let mut query_params = HashMap::new();
query_params.insert("target_id".to_string(), target_id.to_string());

let query = r"
SELECT
metric_name,
argMax(id, toUInt128(id)) as latest_id
FROM BooleanMetricFeedbackByTargetId
WHERE target_id = {target_id:String}
GROUP BY metric_name

UNION ALL

SELECT
metric_name,
argMax(id, toUInt128(id)) as latest_id
FROM FloatMetricFeedbackByTargetId
WHERE target_id = {target_id:String}
GROUP BY metric_name

ORDER BY metric_name
FORMAT JSONEachRow"
.to_string();

(query, query_params)
}

// Implementation of FeedbackQueries trait
#[async_trait]
impl FeedbackQueries for ClickHouseConnectionInfo {
Expand Down Expand Up @@ -940,6 +969,22 @@ impl FeedbackQueries for ClickHouseConnectionInfo {

parse_json_rows(response.response.as_str())
}

async fn query_latest_feedback_id_by_metric(
&self,
target_id: Uuid,
) -> Result<Vec<LatestFeedbackRow>, Error> {
let (query, params_owned) = build_latest_feedback_id_by_metric_query(target_id);

let query_params: HashMap<&str, &str> = params_owned
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();

let response = self.run_query_synchronous(query, &query_params).await?;

parse_json_rows(response.response.as_str())
}
}

fn parse_table_bounds(response: &str) -> Result<TableBounds, Error> {
Expand Down Expand Up @@ -1952,4 +1997,24 @@ mod tests {
Some(&"test_variant".to_string())
);
}

#[test]
fn test_build_latest_feedback_id_by_metric_query() {
let target_id = Uuid::now_v7();
let (query, query_params) = build_latest_feedback_id_by_metric_query(target_id);

// Verify the query structure
assert_query_contains(&query, "SELECT");
assert_query_contains(&query, "FROM BooleanMetricFeedbackByTargetId");
assert_query_contains(&query, "FROM FloatMetricFeedbackByTargetId");
assert_query_contains(&query, "argMax(id, toUInt128(id)) as latest_id");
assert_query_contains(&query, "WHERE target_id = {target_id:String}");
assert_query_contains(&query, "UNION ALL");
assert_query_contains(&query, "GROUP BY metric_name");
assert_query_contains(&query, "ORDER BY metric_name");
assert_query_contains(&query, "FORMAT JSONEachRow");

// Verify params
assert_eq!(query_params.get("target_id"), Some(&target_id.to_string()));
}
}
16 changes: 16 additions & 0 deletions tensorzero-core/src/db/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[cfg(test)]
use mockall::automock;

use super::TableBounds;
use crate::serde_util::deserialize_u64;

#[async_trait]
#[cfg_attr(test, automock)]
pub trait FeedbackQueries {
/// Retrieves cumulative feedback statistics for a given metric and function, optionally filtered by variant names.
async fn get_feedback_by_variant(
Expand Down Expand Up @@ -91,6 +95,12 @@ pub trait FeedbackQueries {
inference_table: &str,
variant_name: Option<&str>,
) -> Result<Vec<MetricWithFeedback>, Error>;

/// Query the latest feedback ID for each metric for a given target
async fn query_latest_feedback_id_by_metric(
&self,
target_id: Uuid,
) -> Result<Vec<LatestFeedbackRow>, Error>;
}

#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
Expand Down Expand Up @@ -239,3 +249,9 @@ pub enum MetricType {
Float,
Demonstration,
}

#[derive(Debug, Deserialize)]
pub struct LatestFeedbackRow {
pub metric_name: String,
pub latest_id: String,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Feedback endpoint for querying feedback-related information

use std::collections::HashMap;

use axum::extract::{Path, State};
use axum::{Json, debug_handler};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use uuid::Uuid;

use crate::db::feedback::FeedbackQueries;
use crate::error::Error;
use crate::utils::gateway::{AppState, AppStateData};

#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct LatestFeedbackIdByMetricResponse {
pub feedback_id_by_metric: HashMap<String, String>,
}

/// HTTP handler for getting the latest feedback ID for each metric for a target
#[debug_handler(state = AppStateData)]
#[instrument(
name = "get_latest_feedback_id_by_metric_handler",
skip_all,
fields(
target_id = %target_id,
)
)]
pub async fn get_latest_feedback_id_by_metric_handler(
State(app_state): AppState,
Path(target_id): Path<Uuid>,
) -> Result<Json<LatestFeedbackIdByMetricResponse>, Error> {
let response =
get_latest_feedback_id_by_metric(&app_state.clickhouse_connection_info, target_id).await?;
Ok(Json(response))
}

/// Core business logic for getting the latest feedback ID for each metric
pub async fn get_latest_feedback_id_by_metric(
clickhouse: &impl FeedbackQueries,
target_id: Uuid,
) -> Result<LatestFeedbackIdByMetricResponse, Error> {
let latest_feedback_rows = clickhouse
.query_latest_feedback_id_by_metric(target_id)
.await?;

let feedback_id_by_metric = latest_feedback_rows
.into_iter()
.map(|row| (row.metric_name, row.latest_id))
.collect();

Ok(LatestFeedbackIdByMetricResponse {
feedback_id_by_metric,
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::db::feedback::{LatestFeedbackRow, MockFeedbackQueries};
use std::collections::HashMap;

#[tokio::test]
async fn test_get_latest_feedback_id_by_metric_calls_clickhouse() {
let mut mock_clickhouse = MockFeedbackQueries::new();

let target_id = Uuid::now_v7();
let accuracy_id = Uuid::now_v7().to_string();
let quality_id = Uuid::now_v7().to_string();

let mut expected_map = HashMap::new();
expected_map.insert("accuracy".to_string(), accuracy_id.clone());
expected_map.insert("quality".to_string(), quality_id.clone());

mock_clickhouse
.expect_query_latest_feedback_id_by_metric()
.withf(move |id| *id == target_id)
.times(1)
.returning({
let accuracy_id = accuracy_id.clone();
let quality_id = quality_id.clone();
move |_| {
let rows = vec![
LatestFeedbackRow {
metric_name: "accuracy".to_string(),
latest_id: accuracy_id.clone(),
},
LatestFeedbackRow {
metric_name: "quality".to_string(),
latest_id: quality_id.clone(),
},
];
Box::pin(async move { Ok(rows) })
}
});

let result = get_latest_feedback_id_by_metric(&mock_clickhouse, target_id)
.await
.unwrap();

assert_eq!(result.feedback_id_by_metric, expected_map);
}

#[tokio::test]
async fn test_get_latest_feedback_id_by_metric_empty_result() {
let mut mock_clickhouse = MockFeedbackQueries::new();

let target_id = Uuid::now_v7();
let expected_map = HashMap::new();

mock_clickhouse
.expect_query_latest_feedback_id_by_metric()
.withf(move |id| *id == target_id)
.times(1)
.returning(move |_| Box::pin(async move { Ok(vec![]) }));

let result = get_latest_feedback_id_by_metric(&mock_clickhouse, target_id)
.await
.unwrap();

assert_eq!(result.feedback_id_by_metric, expected_map);
assert!(result.feedback_id_by_metric.is_empty());
}
}
3 changes: 3 additions & 0 deletions tensorzero-core/src/endpoints/feedback/internal/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod latest_feedback_by_metric;

pub use latest_feedback_by_metric::*;
2 changes: 2 additions & 0 deletions tensorzero-core/src/endpoints/feedback/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ use tensorzero_auth::middleware::RequestApiKeyExtension;
use tracing_opentelemetry::OpenTelemetrySpanExt;

use super::validate_tags;

pub mod human_feedback;
pub mod internal;

/// There is a potential issue here where if we write an inference and then immediately write feedback for it,
/// we might not be able to find the inference in the database because it hasn't been written yet.
Expand Down
Loading
Loading