Repository: tensorzero/tensorzero (language: rust)

Issue:
Deduplicate feedback in the generation of variant performance charts
The query in [this function](https://github.com/tensorzero/tensorzero/blob/8c88242c5a9464b419ed9333cd64d13d317ea11f/ui/app/utils/clickhouse/function.ts#L15) needs to be rewritten like the ones in `curation.ts` so that we correctly account for overwriting


Discussion / hints:

Fix patch:
diff --git a/ui/app/utils/clickhouse/function.ts b/ui/app/utils/clickhouse/function.ts
index e6d48f5f6..23a09f23c 100644
--- a/ui/app/utils/clickhouse/function.ts
+++ b/ui/app/utils/clickhouse/function.ts
@@ -115,11 +115,16 @@ WITH sub AS (
         i.episode_id AS episode_id,
         any(f.value) AS value_per_episode
     FROM ${inference_table_name} i
-    JOIN ${metric_table_name} f
-        ON i.episode_id = f.target_id
+    JOIN (
+        SELECT
+            target_id,
+            value,
+            ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as rn
+        FROM ${metric_table_name}
+        WHERE metric_name = {metric_name:String}
+    ) f ON i.episode_id = f.target_id AND f.rn = 1
     WHERE
-        f.metric_name = {metric_name:String}
-        AND i.function_name = {function_name:String}${variantFilter}
+        i.function_name = {function_name:String}${variantFilter}
     GROUP BY
         variant_name,
         episode_id
@@ -162,14 +167,16 @@ WITH sub AS (
         */
         any(f.value) AS value_per_episode
     FROM ${inference_table_name} i
-    JOIN ${metric_table_name} f
-        ON i.episode_id = f.target_id
+    JOIN (
+        SELECT
+            target_id,
+            value,
+            ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as rn
+        FROM ${metric_table_name}
+        WHERE metric_name = {metric_name:String}
+    ) f ON i.episode_id = f.target_id AND f.rn = 1
     WHERE
-        /* Filter for the metric you're interested in. */
-        f.metric_name = {metric_name:String}
-
-        /* Filter to only inferences with the correct function_name. */
-        AND i.function_name = {function_name:String}${variantFilter}
+        i.function_name = {function_name:String}${variantFilter}
     GROUP BY
         period_start,
         variant_name,
@@ -254,11 +261,16 @@ SELECT
     stddevSamp(f.value) AS stdev,
     1.96 * (stddevSamp(f.value) / sqrt(count())) AS ci_error
 FROM ${inference_table_name} i
-JOIN ${metric_table_name} f
-    ON i.id = f.target_id
+JOIN (
+    SELECT
+        target_id,
+        value,
+        ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as rn
+    FROM ${metric_table_name}
+    WHERE metric_name = {metric_name:String}
+) f ON i.id = f.target_id AND f.rn = 1
 WHERE
-    f.metric_name = {metric_name:String}
-    AND i.function_name = {function_name:String}${variantFilter}
+    i.function_name = {function_name:String}${variantFilter}
 GROUP BY
     variant_name
 ORDER BY
@@ -273,11 +285,16 @@ SELECT
     stddevSamp(f.value) AS stdev,
     1.96 * (stddevSamp(f.value) / sqrt(count())) AS ci_error
 FROM ${inference_table_name} i
-JOIN ${metric_table_name} f
-    ON i.id = f.target_id
+JOIN (
+    SELECT
+        target_id,
+        value,
+        ROW_NUMBER() OVER (PARTITION BY target_id ORDER BY timestamp DESC) as rn
+    FROM ${metric_table_name}
+    WHERE metric_name = {metric_name:String}
+) f ON i.id = f.target_id AND f.rn = 1
 WHERE
-    f.metric_name = {metric_name:String}
-    AND i.function_name = {function_name:String}${variantFilter}
+    i.function_name = {function_name:String}${variantFilter}
 GROUP BY
     period_start,
     variant_name
diff --git a/ui/fixtures/boolean_metric_feedback_examples.csv b/ui/fixtures/boolean_metric_feedback_examples.csv
index 6fae0a616..091ffe9ff 100644
--- a/ui/fixtures/boolean_metric_feedback_examples.csv
+++ b/ui/fixtures/boolean_metric_feedback_examples.csv
@@ -757,3 +757,4 @@ id,target_id,metric_name,value,tags
 01942e27-fb34-7011-85d7-3abead27d374,01942e26-4c26-7182-bdb1-3097f9609dfb,solved,False,{}
 01942e28-0089-74a0-ad1c-7eaa9d929d94,01942e26-4d94-79f1-bfa9-7970eaf8266e,solved,False,{}
 01944375-1308-7a02-a74f-16580aaed027,01942e26-4693-7e80-8591-47b98e25d721,solved,True,{}
+01943375-1308-7a02-a74f-16580aaed027,01942e26-4693-7e80-8591-47b98e25d721,solved,False,{}
diff --git a/ui/fixtures/demonstration_feedback_examples.csv b/ui/fixtures/demonstration_feedback_examples.csv
index 8dcda8a42..8940c09ea 100644
--- a/ui/fixtures/demonstration_feedback_examples.csv
+++ b/ui/fixtures/demonstration_feedback_examples.csv
@@ -592,3 +592,4 @@ id,inference_id,value,tags
 0193fbb4-c37a-7711-8679-965c1ed22cdc,01943162-6e90-70a0-9722-6ff65a8a48c0,"[{""type"":""text"",""text"":""Thinking about well-being, it's a broad topic that encompasses physical, mental, and emotional health. Hyperbole often involves exaggeration for emphasis. I need to weave that in somehow. Let's try this:\n\nIn peace I subsist,  \nImmortal vigor in me—  \nLife's boundless embrace.  ""}]",{}
 0193fbb4-c749-72f3-b1ca-bb689184b52f,01941399-4780-7e71-909f-a52c84612217,"[{""type"":""text"",""text"":""Okay, let's think about the concept of attendance. It's about showing up, being present, or maybe even the lack of presence. To include hyperbole, I'll exaggerate the importance or the consequences of attendance.\n\nFirst, I need a line with 5 syllables. \n\nMaybe: \n\""Never missed a day\"" \n\nNow, a 7-syllable line:\n\n\""Halls echo empty with loss\""\n\nAnd finally, another 5-syllable line:\n\n\""Lone planet absent\""\n\nHere's the haiku:\n\nNever missed a day  \nHalls echo empty with loss  \nLone planet absent""}]",{}
 0193fbb4-c568-7c53-a8ce-ec66988b1ab9,01940bfe-6cf8-7e51-af48-90120849a8b9,"[{""type"":""text"",""text"":""Alright, let's dive into the concept of virtue. Virtue often suggests moral excellence, goodness, and righteousness. Hyperbole could be utilized to emphasize the grandness or the celestial quality of virtue.\n\nNow, to construct a haiku:\n- I'll start with a 5-syllable line capturing a core element of virtue.\n- Then, I'll think of a 7-syllable line that incorporates hyperbole.\n- Lastly, I'll wrap it up with another 5-syllable line relating to virtue.\n\nHere is the haiku:\n\nHeart of purest gold,  \nMountains bow to its great light,  \nGuiding stars above.""}]",{}
+0193fbb4-c568-5c53-a8ce-ec66988b1ab9,01940bfe-6cf8-7e51-af48-90120849a8b9,"[{""type"":""text"",""text"":""Alright, let's dive into the concept of virtue. Virtue often suggests moral excellence, goodness, and righteousness. Hyperbole could be utilized to emphasize the grandness or the celestial quality of virtue.\n\nNow, to construct a haiku:\n- I'll start with a 5-syllable line capturing a core element of virtue.\n- Then, I'll think of a 7-syllable line that incorporates hyperbole.\n- Lastly, I'll wrap it up with another 5-syllable line relating to virtue.\n\nHere is the haiku:\n\nHeart of purest gold,  \nMountains bow to its great height,  \nGuiding stars above.""}]",{}
diff --git a/ui/fixtures/float_metric_feedback_examples.csv b/ui/fixtures/float_metric_feedback_examples.csv
index 686ca1976..fe6352e51 100644
--- a/ui/fixtures/float_metric_feedback_examples.csv
+++ b/ui/fixtures/float_metric_feedback_examples.csv
@@ -949,3 +949,4 @@ id,target_id,metric_name,value,tags
 01940a91-9864-70c2-99fe-b143fa977fa9,0193da46-650d-7740-bd69-587924cb6eae,jaccard_similarity_episode,0.25,{}
 01940a91-9864-70c2-99fe-b270780f6595,0193da46-5d99-7a02-bdc4-4a00ebfe2305,jaccard_similarity_episode,0.6666667,{}
 01940a91-9864-70c2-99fe-ae6f2d6b69ef,0193da46-5d8f-71b1-bebe-830fef4ed65c,jaccard_similarity_episode,0.5,{}
+01940a91-9864-60c2-99fe-ae6f2d6b69ef,0193da46-5d8f-71b1-bebe-830fef4ed65c,jaccard_similarity_episode,-134.434,{}


Test patch:
diff --git a/ui/app/utils/clickhouse/feedback.test.ts b/ui/app/utils/clickhouse/feedback.test.ts
index f2cf40751..8e4922a46 100644
--- a/ui/app/utils/clickhouse/feedback.test.ts
+++ b/ui/app/utils/clickhouse/feedback.test.ts
@@ -8,7 +8,6 @@ import {
   type BooleanMetricFeedbackRow,
   type CommentFeedbackRow,
   type FeedbackRow,
-  type FloatMetricFeedbackRow,
 } from "./feedback";
 
 test("queryCommentFeedbackByTargetId", async () => {
@@ -150,7 +149,7 @@ test("queryFeedbackByTargetId", async () => {
     before: secondPage[secondPage.length - 1].id,
     page_size,
   });
-  expect(thirdPage).toHaveLength(8);
+  expect(thirdPage).toHaveLength(9);
 
   // Check that all pages are sorted by id in descending order
   const checkSorting = (feedback: FeedbackRow[]) => {
@@ -163,10 +162,12 @@ test("queryFeedbackByTargetId", async () => {
   checkSorting(secondPage);
   checkSorting(thirdPage);
 
-  // The last element should be a float metric
-  const lastElement = thirdPage[thirdPage.length - 1] as FloatMetricFeedbackRow;
-  expect(lastElement.metric_name).toBe("num_questions");
-  expect(lastElement.value).toBe(5);
+  // The last element should be a boolean metric (as it is the one that was overwritten)
+  const lastElement = thirdPage[
+    thirdPage.length - 1
+  ] as BooleanMetricFeedbackRow;
+  expect(lastElement.metric_name).toBe("solved");
+  expect(lastElement.value).toBe(false);
 
   // The first element should be a boolean metric
   const firstElement = firstPage[0] as BooleanMetricFeedbackRow;
@@ -174,7 +175,7 @@ test("queryFeedbackByTargetId", async () => {
   expect(firstElement.value).toBe(true);
 
   // Check total number of items
-  expect(firstPage.length + secondPage.length + thirdPage.length).toBe(28);
+  expect(firstPage.length + secondPage.length + thirdPage.length).toBe(29);
 
   const emptyFeedback = await queryFeedbackByTargetId({
     target_id: "01942e26-4693-7e80-8591-47b98e25d711",
@@ -203,7 +204,7 @@ test("queryFeedbackByTargetId", async () => {
     before: secondBackwardPage[secondBackwardPage.length - 1].id,
     page_size,
   });
-  expect(thirdBackwardPage).toHaveLength(7);
+  expect(thirdBackwardPage).toHaveLength(8);
 
   // Check that backward pages are sorted by id in descending order
   checkSorting(firstBackwardPage);
@@ -215,7 +216,7 @@ test("queryFeedbackByTargetId", async () => {
     firstBackwardPage.length +
       secondBackwardPage.length +
       thirdBackwardPage.length,
-  ).toBe(27);
+  ).toBe(28);
 });
 
 describe("queryMetricsWithFeedback", () => {