fix: eval templates removal (#12595)

1. This PR removes Evaluation templates from pipeline module
2. removes LLM evaluation node from pipeline editor 
3. remove LLM evaluation creation tab from schema details

---------

Co-authored-by: Ashish Kolhe <35160958+oasisk@users.noreply.github.com>
Co-authored-by: udev <uddhav@openobserve.ai>
This commit is contained in:
Sai Nikhil
2026-06-11 12:45:33 +05:30
committed by GitHub
co-authored by Ashish Kolhe udev
parent 6ea4b39141
commit ff8a579bdc
13 changed files with 191 additions and 451 deletions
+9 -31
View File
@@ -137,7 +137,7 @@ pub async fn save_pipeline(
}
let pipeline_id = pipeline.id.to_string();
let pipeline_name = pipeline.name.clone();
match pipeline::save_pipeline(pipeline).await {
match pipeline::save_user_pipeline(pipeline).await {
Ok(()) => MetaHttpResponse::json(
MetaHttpResponse::message(StatusCode::OK, "Pipeline created successfully")
.with_id(pipeline_id)
@@ -206,7 +206,7 @@ pub async fn list_pipelines(
// Get List of allowed objects ends
}
let pipelines = match pipeline::list_pipelines(&org_id, _permitted).await {
let pipelines = match pipeline::list_user_pipelines(&org_id, _permitted).await {
Ok(pipelines) => pipelines,
Err(e) => return e.into(),
};
@@ -271,15 +271,7 @@ pub async fn list_pipelines(
)
)]
pub async fn get_pipeline(Path((org_id, pipeline_id)): Path<(String, String)>) -> Response {
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
.await
.as_deref()
!= Some(org_id.as_str())
{
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
}
let meta_pipeline = match crate::service::db::pipeline::get_by_id(&pipeline_id).await {
let meta_pipeline = match pipeline::get_user_pipeline(&org_id, &pipeline_id).await {
Ok(pipeline) => pipeline,
Err(e) => return e.into(),
};
@@ -378,15 +370,7 @@ pub async fn list_streams_with_pipeline(Path(org_id): Path<String>) -> Response
)
)]
pub async fn delete_pipeline(Path((org_id, pipeline_id)): Path<(String, String)>) -> Response {
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
.await
.as_deref()
!= Some(org_id.as_str())
{
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
}
match pipeline::delete_pipeline(&pipeline_id).await {
match pipeline::delete_user_pipeline(&org_id, &pipeline_id).await {
Ok(()) => MetaHttpResponse::json(MetaHttpResponse::message(
StatusCode::OK,
"Pipeline deleted successfully",
@@ -451,7 +435,7 @@ pub async fn delete_pipeline_bulk(
let mut err = None;
for id in req.ids {
match pipeline::delete_pipeline(&id).await {
match pipeline::delete_user_pipeline(&org_id, &id).await {
Ok(()) => {
successful.push(id);
}
@@ -504,7 +488,8 @@ pub async fn update_pipeline(
Json(mut pipeline): Json<Pipeline>,
) -> Response {
pipeline.org = org_id;
match pipeline::update_pipeline(pipeline).await {
let org_id = pipeline.org.clone();
match pipeline::update_user_pipeline(&org_id, pipeline).await {
Ok(()) => MetaHttpResponse::json(MetaHttpResponse::message(
StatusCode::OK,
"Pipeline updated successfully",
@@ -545,13 +530,6 @@ pub async fn enable_pipeline(
Path((org_id, pipeline_id)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> Response {
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
.await
.as_deref()
!= Some(org_id.as_str())
{
return MetaHttpResponse::not_found(format!("Pipeline not found: {pipeline_id}"));
}
let enable = query
.get("value")
.and_then(|v| v.parse::<bool>().ok())
@@ -562,7 +540,7 @@ pub async fn enable_pipeline(
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or_default();
match pipeline::enable_pipeline(&org_id, &pipeline_id, enable, starts_from_now).await {
match pipeline::enable_user_pipeline(&org_id, &pipeline_id, enable, starts_from_now).await {
Ok(()) => {
let resp_msg = format!(
"Pipeline successfully {}",
@@ -644,7 +622,7 @@ pub async fn enable_pipeline_bulk(
let mut err = None;
for id in req.ids {
match pipeline::enable_pipeline(&org_id, &id, enable, starts_from_now).await {
match pipeline::enable_user_pipeline(&org_id, &id, enable, starts_from_now).await {
Ok(()) => {
successful.push(id);
}
+26 -6
View File
@@ -26,6 +26,14 @@ use crate::common::meta::http::HttpResponse as MetaHttpResponse;
#[cfg(feature = "enterprise")]
use crate::service::alerts::backfill;
#[cfg(feature = "enterprise")]
async fn ensure_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<(), Response> {
crate::service::pipeline::get_user_pipeline(org_id, pipeline_id)
.await
.map(|_| ())
.map_err(Into::into)
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct BackfillRequest {
/// Start time in microseconds
@@ -83,12 +91,8 @@ pub async fn create_backfill(
Path((org_id, pipeline_id)): Path<(String, String)>,
Json(req): Json<BackfillRequest>,
) -> Response {
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
.await
.as_deref()
!= Some(org_id.as_str())
{
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
return response;
}
log::info!(
@@ -244,6 +248,10 @@ pub async fn list_backfills(Path(_org_id): Path<String>) -> Response {
pub async fn get_backfill(
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
) -> Response {
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
return response;
}
match backfill::get_backfill_job(&org_id, &job_id).await {
Ok(job) => {
// Verify the job belongs to the specified pipeline
@@ -330,6 +338,10 @@ pub async fn enable_backfill(
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
Query(query): Query<std::collections::HashMap<String, String>>,
) -> Response {
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
return response;
}
let enable = query
.get("value")
.and_then(|v| v.parse::<bool>().ok())
@@ -441,6 +453,10 @@ pub async fn enable_backfill(
pub async fn delete_backfill(
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
) -> Response {
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
return response;
}
// Verify the job belongs to the specified pipeline
match backfill::get_backfill_job(&org_id, &job_id).await {
Ok(job) => {
@@ -554,6 +570,10 @@ pub async fn update_backfill(
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
Json(req): Json<BackfillRequest>,
) -> Response {
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
return response;
}
// Verify the job belongs to the specified pipeline
match backfill::get_backfill_job(&org_id, &job_id).await {
Ok(job) => {
@@ -44,7 +44,7 @@ use crate::{
},
handler::http::{extractors::Headers, request::alerts::history::escape_like},
service::{
db::pipeline::list_by_org as list_pipelines,
pipeline::list_user_pipelines,
search::{self as SearchService},
},
};
@@ -562,7 +562,7 @@ pub async fn get_pipeline_history(
/// Helper function to get all pipeline IDs for an organization
async fn get_organization_pipeline_ids(org_id: &str) -> Result<Vec<String>, anyhow::Error> {
// Get all pipelines for the organization
let pipelines = list_pipelines(org_id).await?;
let pipelines = list_user_pipelines(org_id, None).await?;
// Extract pipeline IDs
let ids: Vec<String> = pipelines.into_iter().map(|pipeline| pipeline.id).collect();
+3 -1
View File
@@ -152,7 +152,9 @@ pub async fn get_summary(org_id: &str) -> OrgSummary {
.collect::<Vec<_>>()
};
let pipelines = db::pipeline::list_by_org(org_id).await.unwrap_or_default();
let pipelines = super::pipeline::list_user_pipelines(org_id, None)
.await
.unwrap_or_default();
let pipeline_summary = PipelineSummary {
num_realtime: pipelines
.iter()
+149 -1
View File
@@ -15,7 +15,7 @@
use config::meta::{
pipeline::{
Pipeline,
Pipeline, PipelineKind,
components::{NodeData, PipelineSource},
},
search::SearchEventType,
@@ -115,6 +115,18 @@ pub async fn save_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError>
Ok(())
}
#[tracing::instrument(skip(pipeline))]
pub async fn save_user_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError> {
if let Ok(existing_pipeline) = pipeline::get_by_id(&pipeline.id).await
&& (existing_pipeline.org != pipeline.org || !existing_pipeline.is_user())
{
return Err(PipelineError::NotFound(pipeline.id));
}
pipeline.kind = PipelineKind::User;
save_pipeline(pipeline).await
}
#[tracing::instrument(skip(pipeline))]
pub async fn update_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError> {
let Ok(existing_pipeline) = pipeline::get_by_id(&pipeline.id).await else {
@@ -212,6 +224,16 @@ pub async fn update_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError
Ok(())
}
#[tracing::instrument(skip(pipeline))]
pub async fn update_user_pipeline(
org_id: &str,
mut pipeline: Pipeline,
) -> Result<(), PipelineError> {
get_user_pipeline(org_id, &pipeline.id).await?;
pipeline.kind = PipelineKind::User;
update_pipeline(pipeline).await
}
#[tracing::instrument]
pub async fn list_pipelines(
org_id: &str,
@@ -234,6 +256,45 @@ pub async fn list_pipelines(
.collect())
}
#[tracing::instrument]
pub async fn list_user_pipelines(
org_id: &str,
permitted: Option<Vec<String>>,
) -> Result<Vec<Pipeline>, PipelineError> {
Ok(pipeline::list_by_org(org_id)
.await?
.into_iter()
.filter(|pipeline| is_user_pipeline_visible(pipeline, org_id, permitted.as_ref()))
.collect())
}
fn is_user_pipeline_visible(
pipeline: &Pipeline,
org_id: &str,
permitted: Option<&Vec<String>>,
) -> bool {
if !pipeline.is_user() {
return false;
}
match permitted {
Some(permitted) => {
permitted.contains(&format!("pipeline:{}", pipeline.id))
|| permitted.contains(&format!("pipeline:_all_{org_id}"))
}
None => true,
}
}
#[tracing::instrument]
pub async fn get_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<Pipeline, PipelineError> {
let pipeline = pipeline::get_by_id(pipeline_id).await?;
if pipeline.org != org_id || !pipeline.is_user() {
return Err(PipelineError::NotFound(pipeline_id.to_string()));
}
Ok(pipeline)
}
#[tracing::instrument]
pub async fn list_pipeline_triggers(org_id: &str) -> Result<Vec<Trigger>, PipelineError> {
Ok(
@@ -294,6 +355,17 @@ pub async fn enable_pipeline(
Ok(())
}
#[tracing::instrument]
pub async fn enable_user_pipeline(
org_id: &str,
pipeline_id: &str,
enable: bool,
starts_from_now: bool,
) -> Result<(), PipelineError> {
get_user_pipeline(org_id, pipeline_id).await?;
enable_pipeline(org_id, pipeline_id, enable, starts_from_now).await
}
#[tracing::instrument]
pub async fn delete_pipeline(pipeline_id: &str) -> Result<(), PipelineError> {
let Ok(existing_pipeline) = pipeline::get_by_id(pipeline_id).await else {
@@ -337,3 +409,79 @@ pub async fn delete_pipeline(pipeline_id: &str) -> Result<(), PipelineError> {
.await;
Ok(())
}
#[tracing::instrument]
pub async fn delete_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<(), PipelineError> {
get_user_pipeline(org_id, pipeline_id).await?;
delete_pipeline(pipeline_id).await
}
#[cfg(test)]
mod tests {
use config::meta::{
pipeline::{
Pipeline, PipelineKind,
components::{DerivedStream, PipelineSource},
},
stream::StreamParams,
};
use super::is_user_pipeline_visible;
fn test_pipeline(id: &str, kind: PipelineKind) -> Pipeline {
Pipeline {
id: id.to_string(),
version: 1,
enabled: true,
org: "test_org".to_string(),
name: id.to_string(),
description: String::new(),
source: PipelineSource::Realtime(StreamParams::default()),
nodes: vec![],
edges: vec![],
kind,
}
}
#[test]
fn test_is_user_pipeline_visible_excludes_evaluation_pipelines() {
let user_pipeline = test_pipeline("user_pipeline", PipelineKind::User);
let evaluation_pipeline = test_pipeline("evaluation_pipeline", PipelineKind::Evaluation);
assert!(is_user_pipeline_visible(&user_pipeline, "test_org", None));
assert!(!is_user_pipeline_visible(
&evaluation_pipeline,
"test_org",
None
));
}
#[test]
fn test_is_user_pipeline_visible_applies_permissions() {
let pipeline = test_pipeline("user_pipeline", PipelineKind::User);
assert!(is_user_pipeline_visible(
&pipeline,
"test_org",
Some(&vec!["pipeline:user_pipeline".to_string()])
));
assert!(is_user_pipeline_visible(
&pipeline,
"test_org",
Some(&vec!["pipeline:_all_test_org".to_string()])
));
assert!(!is_user_pipeline_visible(
&pipeline,
"test_org",
Some(&vec!["pipeline:other_pipeline".to_string()])
));
}
#[test]
fn test_is_user_pipeline_visible_keeps_scheduled_user_pipelines() {
let mut pipeline = test_pipeline("scheduled_pipeline", PipelineKind::User);
pipeline.source = PipelineSource::Scheduled(DerivedStream::default());
assert!(is_user_pipeline_visible(&pipeline, "test_org", None));
}
}
@@ -2098,22 +2098,6 @@ describe("Schema Component Tests", () => {
expect(names.length).toBe(3);
});
it("should compute llmEvalStreamFields", () => {
const fields = wrapper.vm.llmEvalStreamFields;
expect(Array.isArray(fields)).toBe(true);
expect(fields.length).toBe(3);
expect(fields[0]).toHaveProperty("label");
expect(fields[0]).toHaveProperty("value");
});
it("should mark llmEvalFormDirty to false when tab changes from llmEvaluation", () => {
wrapper.vm.llmEvalFormDirty = true;
wrapper.vm.updateActiveMainTab("llmEvaluation");
// llmEvalFormDirty stays true for llm tab
wrapper.vm.updateActiveMainTab("schemaSettings");
expect(wrapper.vm.llmEvalFormDirty).toBe(false);
});
it("should compute hasUDSFieldInSelection", async () => {
wrapper.vm.activeTab = "allFields";
wrapper.vm.selectedFields = [{ name: "message", isUserDefined: true }];
@@ -2408,12 +2392,6 @@ describe("Schema Component Tests", () => {
afterEach(() => { w.unmount(); });
it("should expose llmEvalStreamFields for traces streams", () => {
const fields = w.vm.llmEvalStreamFields;
expect(Array.isArray(fields)).toBe(true);
expect(fields.length).toBeGreaterThan(0);
});
it("should show cross-linking tab when enable_cross_linking is true", async () => {
store.state.zoConfig.enable_cross_linking = true;
w.vm.updateActiveMainTab("crossLinking");
+1 -61
View File
@@ -333,19 +333,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
label="Configuration"
data-test="schema-configuration-tab"
/>
<!-- LLM Evaluation Tab (enterprise + ai_enabled + traces only) -->
<OTab
v-if="
config.isEnterprise == 'true' &&
store.state.zoConfig.ai_enabled &&
indexData.stream_type === 'traces'
"
name="llmEvaluation"
icon="psychology"
:label="t('pipeline.llmEvaluation')"
data-test="stream-llm-evaluation-tab"
/>
<!-- Cross-Linking Tab -->
<OTab
v-if="
@@ -773,15 +760,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<!-- LLM Evaluation tab -->
<LlmEvaluationSettings
v-if="activeMainTab == 'llmEvaluation'"
ref="llmEvalSettingsRef"
:stream-name="indexData.name"
:stream-fields="llmEvalStreamFields"
@dirty="llmEvalFormDirty = true"
/>
<!-- cross-linking tab -->
<div v-if="activeMainTab == 'crossLinking'">
<div class="tw:p-4">
@@ -810,32 +788,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<!-- floating footer for the table -->
<div class="floating-buttons tw:flex-shrink-0 tw:px-2 tw:py-1">
<!-- LLM Evaluation tab footer -->
<div
v-if="activeMainTab === 'llmEvaluation'"
class="tw:flex tw:items-center tw:justify-end tw:gap-2"
>
<OButton
data-test="schema-cancel-button"
variant="outline"
size="sm-action"
@click="llmEvalFormDirty = false; $emit('close')"
>
{{ t("logStream.cancel") }}
</OButton>
<OButton
:disabled="!llmEvalFormDirty"
data-test="schema-update-settings-button"
variant="primary"
size="sm-action"
@click="llmEvalSettingsRef?.save()"
>
{{ t("logStream.updateSettings") }}
</OButton>
</div>
<div
v-else-if="indexData.schema.length > 0"
v-if="indexData.schema.length > 0"
class="tw:flex tw:items-center tw:justify-between"
>
<div class="tw:flex tw:items-center tw:gap-2">
@@ -1032,7 +986,6 @@ import DateTime from "@/components/DateTime.vue";
import AssociatedRegexPatterns from "./AssociatedRegexPatterns.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import PerformanceFieldsDialog from "./PerformanceFieldsDialog.vue";
import LlmEvaluationSettings from "./LlmEvaluationSettings.vue";
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
@@ -1082,7 +1035,6 @@ export default defineComponent({
AssociatedRegexPatterns,
ODrawer,
PerformanceFieldsDialog,
LlmEvaluationSettings,
CrossLinkManager,
OButton,
OIcon,
@@ -1137,14 +1089,6 @@ export default defineComponent({
let previousSchemaVersion: any = null;
const approxPartition = ref(false);
const llmEvalStreamFields = computed(() =>
(indexData.value.schema || []).map((f: any) => ({
label: f.name,
value: f.name,
})),
);
const llmEvalSettingsRef = ref<any>(null);
const llmEvalFormDirty = ref(false);
const streamCrossLinks = ref<any[]>([]);
const orgCrossLinks = computed(
() =>
@@ -2112,7 +2056,6 @@ export default defineComponent({
const updateActiveMainTab = (tab) => {
activeMainTab.value = tab;
if (tab !== "llmEvaluation") llmEvalFormDirty.value = false;
};
// Function to get missing FTS and Secondary Index fields
@@ -2770,9 +2713,6 @@ export default defineComponent({
quickModeIcon,
getConfigIcon,
getTimelineIcon,
llmEvalStreamFields,
llmEvalSettingsRef,
llmEvalFormDirty,
};
},
created() {
@@ -112,11 +112,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:open="true"
@cancel:hideform="resetDialog"
/>
<LlmEvaluation
v-if="pipelineObj.dialog.name === 'llm_evaluation'"
:open="true"
@cancel:hideform="resetDialog"
/>
<ODrawer data-test="pipeline-editor-json-editor-drawer"
v-model:open="showJsonEditorDialog"
:width="70"
@@ -191,7 +186,6 @@ import useDragAndDrop from "@/plugins/pipelines/useDnD";
import StreamNode from "@/components/pipeline/NodeForm/Stream.vue";
import QueryForm from "@/components/pipeline/NodeForm/Query.vue";
import ConditionForm from "@/components/pipeline/NodeForm/Condition.vue";
import LlmEvaluation from "@/components/pipeline/NodeForm/LlmEvaluation.vue";
import { MarkerType, useVueFlow } from "@vue-flow/core";
import ExternalDestination from "./NodeForm/ExternalDestination.vue";
import { contextRegistry, createPipelinesContextProvider } from "@/composables/contextProviders";
@@ -206,7 +200,6 @@ const externalOutputImage = getImageURL("images/pipeline/output_remote.png");
const streamRouteImage = getImageURL("images/pipeline/route.svg");
const conditionImage = getImageURL("images/pipeline/transform_condition.png");
const queryImage = getImageURL("images/pipeline/input_query.png");
const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
import useStreams from "@/composables/useStreams";
import usePipelines from "@/composables/usePipelines";
@@ -494,22 +487,6 @@ watch(
);
onBeforeMount(() => {
if (config.isEnterprise == "true" && store.state.zoConfig.ai_enabled) {
// Insert LLM Evaluation node before the "Destination" section header
const destIdx = nodeTypes.findIndex(
(n) => n.isSectionHeader && n.label === "Destination",
);
if (destIdx !== -1) {
nodeTypes.splice(destIdx, 0, {
label: "LLM Evaluation",
subtype: "llm_evaluation",
io_type: "default",
icon: "img:" + llmEvaluationImage,
tooltip: "LLM Evaluation Node",
isSectionHeader: false,
});
}
}
if (config.isEnterprise == "true") {
nodeTypes.push({
label: "Remote",
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!--
PipelineSectionTabs — the L2 section switcher (Stream Pipelines / Functions /
Enrichment Tables / Eval Templates) rendered as tabs next to a page title.
Enrichment Tables) rendered as tabs next to a page title.
Drop it into an AppPageHeader's #tabs slot on any pipeline section page; the
active tab is derived from the current route and clicking a tab navigates.
This mirrors the breadcrumb section dropdown as faster inline navigation.
@@ -57,7 +57,6 @@ import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import { useRouter, type RouteLocationRaw } from "vue-router";
import config from "@/aws-exports";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OTab from "@/lib/navigation/Tabs/OTab.vue";
@@ -81,8 +80,6 @@ const routeToSection: Record<string, string> = {
pipelineBackfill: "streamPipelines",
functionList: "functions",
enrichmentTables: "enrichmentTables",
evalTemplates: "evalTemplates",
evalTemplatesAdd: "evalTemplates",
};
const activeSectionKey = computed(() => {
@@ -127,13 +124,6 @@ const sections = computed<Section[]>(() => {
to: { name: "enrichmentTables", query: q },
visible: true,
},
{
key: "evalTemplates",
label: t("pipeline.evalTemplates"),
icon: "fact-check",
to: { name: "evalTemplates", query: q },
visible: config.isEnterprise == "true",
},
];
});
@@ -51,8 +51,6 @@ import DropzoneBackground from "@/plugins/pipelines/DropzoneBackground.vue";
import { ControlButton, Controls } from '@vue-flow/controls'
import { VueFlow } from "@vue-flow/core";
import { ref, onMounted, nextTick } from "vue";
import { useStore } from "vuex";
import config from "@/aws-exports";
import CustomNode from '@/plugins/pipelines/CustomNode.vue';
import CustomEdge from "@/plugins/pipelines/CustomEdge.vue";
/* import the required styles */
@@ -65,7 +63,6 @@ const streamOutputImage = getImageURL("images/pipeline/output_stream.png");
const externalOutputImage = getImageURL("images/pipeline/output_remote.png");
const conditionImage = getImageURL("images/pipeline/transform_condition.png");
const queryImage = getImageURL("images/pipeline/input_query.png");
const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
export default defineComponent({
@@ -74,7 +71,6 @@ const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
},
components: { VueFlow, CustomNode, DropzoneBackground, CustomEdge, ControlButton, Controls },
setup(props) {
const store = useStore();
const {
pipelineObj,
} = useDragAndDrop();
@@ -167,22 +163,6 @@ const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
},
];
if (config.isEnterprise == "true" && store.state.zoConfig.ai_enabled) {
const destIdx = pipelineObj.nodeTypes.findIndex(
(n) => n.isSectionHeader && n.label === "Destination",
);
if (destIdx !== -1) {
pipelineObj.nodeTypes.splice(destIdx, 0, {
label: "LLM Evaluation",
subtype: "llm_evaluation",
io_type: "default",
icon: "img:" + llmEvaluationImage,
tooltip: "LLM Evaluation Node",
isSectionHeader: false,
});
}
}
})
// Watch for pipeline prop changes to update error information
-210
View File
@@ -286,16 +286,6 @@ size="sm">
/></template>
Thread
</OToggleGroupItem>
<OToggleGroupItem
v-if="hasLLMSpans && evalPipelineExists && evalData.length > 0"
value="evaluations"
size="sm"
>
<template #icon-left
><OIcon name="assignment-turned-in" size="xs" class="tw:shrink-0"
/></template>
Evaluations
</OToggleGroupItem>
</OToggleGroup>
</div>
@@ -737,21 +727,6 @@ size="xs"
</div>
</div>
<!-- Evaluations View - only for LLM traces with evaluation data -->
<div
v-if="
hasLLMSpans && evalPipelineExists && activeTab === 'evaluations'
"
style="display: flex; flex: 1; min-height: 0; overflow-y: auto"
class="tw:w-full tw:bg-[var(--o2-card-bg)]!"
>
<TraceEvaluationsView
ref="traceEvaluationsViewRef"
data-test="trace-details-evaluations"
:eval-data="evalData"
:is-loading="isLoadingEvalData"
/>
</div>
</div>
</div>
</div>
@@ -868,7 +843,6 @@ import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from '@/lib/overlay/Drawer/ODrawer.vue';
import OIcon from "@/lib/core/Icon/OIcon.vue";
import pipelineService from "@/services/pipelines";
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
@@ -891,11 +865,6 @@ const FlameGraphView = defineAsyncComponent(
() => import("@/components/traces/FlameGraphView.vue"),
);
// Import TraceEvaluationsView
const TraceEvaluationsView = defineAsyncComponent(
() => import("./TraceEvaluationsView.vue"),
);
// Import ThreadView (LLM Thread tab)
const ThreadView = defineAsyncComponent(
() => import("./ThreadView.vue"),
@@ -983,7 +952,6 @@ export default defineComponent({
TraceDAG,
TraceHeader,
FlameGraphView,
TraceEvaluationsView,
OToggleGroup,
OToggleGroupItem,
OButton,
@@ -1370,13 +1338,6 @@ export default defineComponent({
return spans.some((span: any) => isLLMTrace(span));
});
// Evaluation pipeline state
const evalPipelineExists = ref(false);
const evalPipelineStreamName = ref<string | null>(null);
const evalData = ref<any[]>([]);
const isLoadingEvalData = ref(false);
const traceEvaluationsViewRef = ref<any>(null);
// Computed properties for new header
const errorSpansCount = computed(() => {
const spans = effectiveSpanList.value;
@@ -1412,14 +1373,6 @@ export default defineComponent({
if (hasLLMSpans.value) {
tabs.push({ label: "Thread", value: "thread" });
}
// Conditionally add Evaluations tab for LLM traces with evaluation data
if (
hasLLMSpans.value &&
evalPipelineExists.value &&
evalData.value.length > 0
) {
tabs.push({ label: "Evaluations", value: "evaluations" });
}
return tabs;
});
@@ -1498,37 +1451,6 @@ export default defineComponent({
},
);
// Watch for stream and LLM spans to fetch evaluation pipeline
watch(
() => [currentTraceStreamName.value, hasLLMSpans.value],
async ([streamName, hasLlm]) => {
if (hasLlm && streamName) {
await fetchEvalPipeline();
} else {
evalPipelineExists.value = false;
evalPipelineStreamName.value = null;
}
},
{ immediate: true },
);
// Watch for trace ID changes to fetch evaluation data
watch(
() => [
effectiveTraceId.value,
evalPipelineExists.value,
evalPipelineStreamName.value,
],
async ([traceId, pipelineExists, streamName]) => {
if (pipelineExists && streamName && traceId) {
await fetchEvalData();
} else {
evalData.value = [];
}
},
{ immediate: true },
);
const updateActiveTab = (tab: string) => {
activeTab.value = tab;
if(tab === 'map') {
@@ -2657,130 +2579,6 @@ export default defineComponent({
await setupTraceDetails();
};
/**
* Fetch LLM evaluation pipeline for the current trace stream
*/
const fetchEvalPipeline = async () => {
if (!currentTraceStreamName.value) {
evalPipelineExists.value = false;
return;
}
try {
const orgId = store.state.selectedOrganization.identifier;
const res = await pipelineService.getPipelines(orgId);
const pipelines: any[] = res.data?.list || [];
console.log(
"[TraceEval] Fetched pipelines:",
pipelines.length,
"Stream:",
currentTraceStreamName.value,
);
// Find pipeline with matching source stream and llm_evaluation node
const evalPipeline = pipelines.find(
(p: any) =>
p.source?.stream_name === currentTraceStreamName.value &&
p.source?.stream_type === "traces" &&
p.nodes?.some((n: any) => n.data?.node_type === "llm_evaluation"),
);
if (evalPipeline) {
evalPipelineExists.value = true;
// Get the evaluation output stream name
const evalOutputNode = evalPipeline.nodes.find(
(n: any) =>
n.io_type === "output" && n.data?.stream_type === "logs",
);
evalPipelineStreamName.value =
evalOutputNode?.data?.stream_name ||
`${currentTraceStreamName.value}_evaluations`;
console.log(
"[TraceEval] Found eval pipeline, stream:",
evalPipelineStreamName.value,
);
} else {
console.log(
"[TraceEval] No eval pipeline found for stream:",
currentTraceStreamName.value,
);
evalPipelineExists.value = false;
evalPipelineStreamName.value = null;
}
} catch (error) {
console.error("Error fetching evaluation pipeline:", error);
evalPipelineExists.value = false;
evalPipelineStreamName.value = null;
}
};
/**
* Fetch evaluation data for the current trace
*/
const fetchEvalData = async () => {
if (
!evalPipelineExists.value ||
!evalPipelineStreamName.value ||
!effectiveTraceId.value
) {
evalData.value = [];
return;
}
isLoadingEvalData.value = true;
try {
console.log(
"[TraceEval] Fetching data from stream:",
evalPipelineStreamName.value,
"traceId:",
effectiveTraceId.value,
);
const req = {
query: {
sql: `SELECT * FROM "${evalPipelineStreamName.value}" WHERE trace_id = '${sanitizeTraceId(effectiveTraceId.value)}' ORDER BY _timestamp ASC`,
start_time: effectiveTimeRange.value.from - 60000000,
end_time: effectiveTimeRange.value.to + 60000000,
from: 0,
size: 100,
},
};
const res = await searchService.search(
{
org_identifier:
(router.currentRoute.value.query?.org_identifier as string) ||
store.state.selectedOrganization.identifier,
query: req,
page_type: "logs",
},
"ui",
);
evalData.value = res.data?.hits || [];
console.log(
"[TraceEval] Fetched evaluation records:",
evalData.value.length,
);
// Load templates for the current org
if (
traceEvaluationsViewRef.value?.loadTemplates &&
effectiveOrgIdentifier.value
) {
await traceEvaluationsViewRef.value.loadTemplates(
effectiveOrgIdentifier.value,
);
}
} catch (error) {
console.error("Error fetching evaluation data:", error);
evalData.value = [];
} finally {
isLoadingEvalData.value = false;
}
};
return {
router,
t,
@@ -2905,14 +2703,6 @@ export default defineComponent({
// FlameGraph data
flatSpans,
traceMetadata,
// Evaluation data
evalPipelineExists,
evalPipelineStreamName,
evalData,
isLoadingEvalData,
traceEvaluationsViewRef,
fetchEvalPipeline,
fetchEvalData,
formatLargeNumber,
updateActiveTab
};
-22
View File
@@ -358,28 +358,6 @@ describe("Functions.vue", () => {
expect((wrapper.vm as any).isDetailView).toBe(false);
});
it("should compute pipelineSections excluding hidden pipelines", async () => {
wrapper = await createWrapper(
{ zoConfig: { custom_hide_menus: "pipelines" } },
"pipelines",
);
const sections = (wrapper.vm as any).pipelineSections;
const streamPipelinesSection = sections.find(
(s: any) => s.key === "streamPipelines",
);
expect(streamPipelinesSection?.visible).toBe(false);
});
it("should compute pipelineSections with streamPipelines present when not hidden", async () => {
wrapper = await createWrapper({}, "pipelines");
const sections = (wrapper.vm as any).pipelineSections;
const streamPipelinesSection = sections.find(
(s: any) => s.key === "streamPipelines",
);
// When not hidden, visible is not explicitly set to false
expect(streamPipelinesSection).toBeDefined();
expect(streamPipelinesSection?.to?.name).toBe("pipelines");
});
});
describe("Store Integration", () => {
-41
View File
@@ -144,7 +144,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import AppPageHeader from "@/components/common/AppPageHeader.vue";
import PipelineSectionTabs from "@/components/pipeline/PipelineSectionTabs.vue";
import type { SectionHubItem } from "@/components/common/SectionHub.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
@@ -193,9 +192,6 @@ export default defineComponent({
pipelineBackfill: "streamPipelines",
functionList: "functions",
enrichmentTables: "enrichmentTables",
evalTemplates: "evalTemplates",
evalTemplatesAdd: "evalTemplates",
evalTemplatesEdit: "evalTemplates",
};
const routeName = computed(() => router.currentRoute.value.name as string);
@@ -211,42 +207,6 @@ export default defineComponent({
() => store.state.selectedOrganization.identifier,
);
// L2 sections for the switcher dropdown.
const pipelineSections = computed<SectionHubItem[]>(() => {
const q = { org_identifier: orgIdentifier.value };
const hideStreamPipelines = store.state.zoConfig?.custom_hide_menus
?.split(",")
.includes("pipelines");
return [
{
key: "streamPipelines",
label: t("function.streamPipeline"),
icon: "lan",
to: { name: "pipelines", query: q },
visible: !hideStreamPipelines,
},
{
key: "functions",
label: t("function.header"),
icon: "function",
to: { name: "functionList", query: q },
},
{
key: "enrichmentTables",
label: t("function.enrichmentTables"),
icon: "dataset",
to: { name: "enrichmentTables", query: q },
},
{
key: "evalTemplates",
label: t("pipeline.evalTemplates"),
icon: "fact-check",
to: { name: "evalTemplates", query: q },
visible: config.isEnterprise == "true",
},
];
});
// ── Level 3 detail crumb ────────────────────────────────────────────────
const detailLabels: Record<string, () => string> = {
pipelineEditor: () =>
@@ -345,7 +305,6 @@ export default defineComponent({
config,
orgIdentifier,
activeTab,
pipelineSections,
isDetailView,
breadcrumbLabel,
detailBack,