pierrejeambrun commented on code in PR #74334:
URL: https://github.com/apache/airflow/pull/74334#discussion_r4206879802


##########
providers/common/ai/src/airflow/providers/common/ai/plugins/www/src/components/ModelPanel.tsx:
##########
@@ -0,0 +1,137 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Badge, Box, Flex, HStack, Heading, SimpleGrid, Spinner, Text } from 
"@chakra-ui/react";
+import type { FC, ReactNode } from "react";
+import { LuInfo } from "react-icons/lu";
+
+import { NoModelInfo } from "src/components/NoModelInfo";
+import { Tooltip } from "src/components/Tooltip";
+import { useModelInfo } from "src/hooks/useModelInfo";
+
+interface ModelPanelProps {
+  dagId: string;
+  runId: string;
+  taskId: string;
+  mapIndex: number;
+}
+
+// Pydantic-ai prices a response via genai-prices' per-token rate lookup (see
+// durable/replay_usage.py::fill_replayed_cost), not from provider-reported 
billing --
+// a model or provider the lookup doesn't recognise is left unpriced rather 
than guessed.
+const COST_TOOLTIP =
+  "Estimated in USD from a per-token price lookup (genai-prices), not 
provider-reported " +
+  "billing -- best-effort, and may be stale or unavailable for some 
models/providers.";
+
+// The XCom keeps full precision (see utils/logging.py's 
format_usage_for_xcom) so a very
+// cheap run doesn't look free in the logs; round to cents here for a readable 
display.
+function formatCost(cost: string | null): string {
+  return cost === null ? "—" : `$${Number(cost).toFixed(2)}`;
+}
+
+const StatBox: FC<{ label: string; tooltip?: ReactNode; value: number | string 
}> = ({
+  label,
+  tooltip,
+  value,
+}) => (
+  <Box bg="bg.subtle" borderRadius="lg" borderWidth="1px" p={4}>
+    <HStack gap={0.5}>
+      <Text color="fg.muted" fontSize="xs">
+        {label}
+      </Text>
+      {tooltip !== undefined && (
+        <Tooltip content={tooltip} portalled>
+          <Box aria-label={`About ${label}`} as="button" color="fg.muted" 
p={0.5}>
+            <LuInfo />
+          </Box>
+        </Tooltip>
+      )}
+    </HStack>
+    <Text fontSize="lg" fontWeight="semibold">
+      {value}
+    </Text>
+  </Box>
+);
+
+export const ModelPanel: FC<ModelPanelProps> = ({ dagId, runId, taskId, 
mapIndex }) => {
+  const { modelName, usage, loading, error } = useModelInfo(dagId, runId, 
taskId, mapIndex);
+
+  if (loading) {
+    return (
+      <Flex align="center" gap={2} p={2}>
+        <Spinner colorPalette="brand" size="sm" />
+        <Text color="fg.muted" fontSize="sm">
+          Loading model info...
+        </Text>
+      </Flex>
+    );
+  }
+
+  if (!modelName && !usage) {
+    return <NoModelInfo error={error} />;
+  }
+
+  return (
+    <Box p={2}>
+      <Box borderBottomWidth="1px" mb={4} pb={4}>
+        <Heading size="sm">AI Model</Heading>
+        <HStack color="fg.muted" fontSize="sm" gap={3} mt={1}>
+          <Text as="span">
+            <Text as="b">Task:</Text> {taskId}
+          </Text>
+          <Text as="span">
+            <Text as="b">DAG:</Text> {dagId}
+          </Text>
+        </HStack>
+      </Box>
+
+      {error && (

Review Comment:
   Fixed



##########
providers/common/ai/tests/unit/common/ai/plugins/test_model_panel.py:
##########
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+from tests_common.test_utils.version_compat import AIRFLOW_V_3_1_PLUS
+
+if not AIRFLOW_V_3_1_PLUS:
+    pytest.skip("The AI Model panel is only compatible with Airflow >= 3.1.0", 
allow_module_level=True)
+
+from airflow.providers.common.ai.plugins.model_panel import ModelPanelPlugin, 
_get_base_url_path
+
+from tests_common.test_utils.config import conf_vars
+
+
+class TestGetBaseUrlPath:
+    def test_default_base_url(self):
+        with conf_vars({("api", "base_url"): "/"}):
+            assert _get_base_url_path("/ai-model") == "/ai-model"
+
+    def test_http_base_url_extracts_path(self):
+        with conf_vars({("api", "base_url"): "http://example.com/airflow/"}):
+            assert _get_base_url_path("/ai-model") == "/airflow/ai-model"
+
+
+class TestModelPanelPlugin:
+    def test_plugin_name(self):
+        assert ModelPanelPlugin.name == "ai_model_panel"
+
+    def test_fastapi_apps_registered(self):
+        assert len(ModelPanelPlugin.fastapi_apps) == 1
+        assert ModelPanelPlugin.fastapi_apps[0]["name"] == "ai-model-panel"
+        assert "url_prefix" in ModelPanelPlugin.fastapi_apps[0]
+
+    def test_react_apps_registered(self):
+        assert len(ModelPanelPlugin.react_apps) == 1
+        app = ModelPanelPlugin.react_apps[0]
+        assert app["name"] == "Model"
+        assert app["url_route"] == "ai-model"
+        assert app["destination"] == "task_instance"
+        assert "model.umd.cjs" in app["bundle_url"]
+
+    def test_applies_to_scopes_to_operators_that_publish_model_name(self):
+        app = ModelPanelPlugin.react_apps[0]
+        operator_names = app["applies_to"]["operator_names"]
+        assert set(operator_names) == {"LLMOperator", "AgentOperator", 
"@task.llm", "@task.agent"}

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to