-
Notifications
You must be signed in to change notification settings - Fork 702
UN-3991 [FIX] Show model names on Prompt Studio tiles for shared users #2240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c33ca2e
b30385d
ca031c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| from unstract.sdk1.adapters.base import Adapter | ||
| from unstract.sdk1.adapters.x2text.constants import X2TextConstants | ||
| from unstract.sdk1.constants import AdapterTypes | ||
| from unstract.sdk1.constants import Common as common | ||
| from unstract.sdk1.embedding import EmbeddingCompat | ||
| from unstract.sdk1.exceptions import SdkError | ||
| from unstract.sdk1.llm import LLM | ||
|
|
@@ -28,6 +29,8 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| UNAVAILABLE_ADAPTER_ICON = "⚠️" | ||
|
|
||
| try: | ||
| from plugins.subscription.time_trials.subscription_adapter import add_unstract_key | ||
| except ImportError: | ||
|
|
@@ -109,6 +112,31 @@ def get_adapter_data_with_key(adapter_id: str, key_value: str) -> Any: | |
| raise InValidAdapterId() | ||
| return updated_adapters[0].get(key_value) | ||
|
|
||
| @staticmethod | ||
| def get_display_info(adapter: AdapterInstance) -> tuple[str, str]: | ||
| """Icon and model label for an adapter, as (icon, model). | ||
|
|
||
| Display data only, no credentials. Never raises - falls back to the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 — three claims in this docstring aren't quite what the code does. (Separate from the icon-fallback thread above — this is about the contract text itself.) "Never raises" — true today, but only by accident. "Display data only, no credentials" — accurate about the return value, but it reads as "this function doesn't touch credentials", which is the opposite of what line 134 does: "falls back to … the adapter id's provider prefix" — frames the normal path as an error path. The prefix is returned whenever metadata has no Related, on the log message at line 132: |
||
| warning icon and the adapter id's provider prefix. | ||
| """ | ||
| icon = UNAVAILABLE_ADAPTER_ICON | ||
| if adapter.is_available: | ||
| try: | ||
| icon = ( | ||
| AdapterProcessor.get_adapter_data_with_key( | ||
| adapter.adapter_id, common.ICON | ||
| ) | ||
| or UNAVAILABLE_ADAPTER_ICON | ||
| ) | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| except Exception as e: | ||
| logger.warning(f"No icon for adapter {adapter.adapter_id}: {e}") | ||
| try: | ||
| model = adapter.metadata.get("model") | ||
| except Exception as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — this
Caught here, it becomes a The same handler also absorbs several unrelated failures, all reachable, none of which "No metadata for adapter" describes:
Suggested fix — let the outage propagate, narrow the rest, and log at try:
model = adapter.metadata.get("model")
except InvalidEncryptionKey:
raise # platform-wide credential outage; must not degrade to a log line
except (TypeError, ValueError, AttributeError) as e:
logger.error(
"Unreadable metadata for adapter %s (%s): %s",
adapter.id, adapter.adapter_id, e,
)
model = NoneIf a 403 on the profile list is considered too disruptive for a display path, that's a defensible call — but then it should be a deliberate one, with the log at |
||
| logger.warning(f"No metadata for adapter {adapter.adapter_id}: {e}") | ||
| model = None | ||
| return icon, model or adapter.adapter_id.split("|")[0] | ||
|
|
||
| @staticmethod | ||
| def test_adapter(adapter_id: str, adapter_metadata: dict[str, Any]) -> bool: | ||
| try: | ||
|
|
@@ -214,27 +242,6 @@ def set_default_triad(default_triad: dict[str, str], user: User) -> None: | |
| else: | ||
| raise InternalServiceError() | ||
|
|
||
| @staticmethod | ||
| def get_adapter_instance_by_id(adapter_instance_id: str) -> Adapter: | ||
| """Get the adapter instance by its ID. | ||
|
|
||
| Parameters: | ||
| - adapter_instance_id (str): The ID of the adapter instance. | ||
|
|
||
| Returns: | ||
| - Adapter: The adapter instance with the specified ID. | ||
|
|
||
| Raises: | ||
| - Exception: If there is an error while fetching the adapter instance. | ||
| """ | ||
| try: | ||
| adapter = AdapterInstance.objects.get(id=adapter_instance_id) | ||
| except Exception as e: | ||
| logger.error(f"Unable to fetch adapter: {e}") | ||
| if not adapter: | ||
| logger.error("Unable to fetch adapter") | ||
| return adapter.adapter_name | ||
|
|
||
| @staticmethod | ||
| def get_adapters_by_type( | ||
| adapter_type: AdapterTypes, user: User | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,33 +9,39 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Adapter FK -> label shown on the Prompt Studio output tiles. | ||
| ADAPTER_LABELS = ( | ||
| (ProfileManagerKeys.LLM, "LLM"), | ||
| (ProfileManagerKeys.EMBEDDING_MODEL, "Embedding Model"), | ||
| (ProfileManagerKeys.VECTOR_STORE, "Vector Store"), | ||
| (ProfileManagerKeys.X2TEXT, "Text Extractor"), | ||
| ) | ||
|
|
||
|
|
||
| class ProfileManagerSerializer(AuditSerializer): | ||
| class Meta: | ||
| model = ProfileManager | ||
| fields = "__all__" | ||
| # View owns uniqueness (IntegrityError->DuplicateData on create); drop | ||
| # the DRF auto-validator that 400s on re-save / PUT before the view runs. | ||
| # Uniqueness is enforced by the view; the auto-validator 400s on re-save. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — this rewrite dropped the load-bearing half of the comment, and the shorter version is now inaccurate. Before: Three things were lost, and one inaccuracy was introduced:
|
||
| validators = [] | ||
|
|
||
| def to_representation(self, instance): # type: ignore | ||
| """Resolve the adapter FKs to the name, model and icon the UI renders. | ||
|
|
||
| Not filtered by adapter access - display data only, no credentials. | ||
| """ | ||
| rep: dict[str, str] = super().to_representation(instance) | ||
| llm = rep[ProfileManagerKeys.LLM] | ||
| embedding = rep[ProfileManagerKeys.EMBEDDING_MODEL] | ||
| vector_db = rep[ProfileManagerKeys.VECTOR_STORE] | ||
| x2text = rep[ProfileManagerKeys.X2TEXT] | ||
| if llm: | ||
| rep[ProfileManagerKeys.LLM] = AdapterProcessor.get_adapter_instance_by_id(llm) | ||
| if embedding: | ||
| rep[ProfileManagerKeys.EMBEDDING_MODEL] = ( | ||
| AdapterProcessor.get_adapter_instance_by_id(embedding) | ||
| ) | ||
| if vector_db: | ||
| rep[ProfileManagerKeys.VECTOR_STORE] = ( | ||
| AdapterProcessor.get_adapter_instance_by_id(vector_db) | ||
| ) | ||
| if x2text: | ||
| rep[ProfileManagerKeys.X2TEXT] = AdapterProcessor.get_adapter_instance_by_id( | ||
| x2text | ||
| ) | ||
| conf: dict[str, str] = {} | ||
| for field, label in ADAPTER_LABELS: | ||
| adapter = getattr(instance, field, None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — All four adapter FKs are Django's forward FK descriptor raises Suggested fix — be explicit about the impossible case: from django.core.exceptions import ObjectDoesNotExist
for field, label in ADAPTER_LABELS:
try:
adapter = getattr(instance, field)
except ObjectDoesNotExist:
logger.error(
"Profile %s references a missing %s adapter (%s)",
instance.profile_id, field, getattr(instance, f"{field}_id", None),
)
continue
Related: |
||
| if not adapter: | ||
| continue | ||
| icon, model = AdapterProcessor.get_display_info(adapter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 — the icon is resolved for all four adapters and kept for one, and each resolution rescans the entire SDK registry from disk.
with open(schema_path) as f:
return f.read()There are 37 adapter schema files in the registry. So each icon lookup is a full registry scan with ~37 Two independent fixes, both small:
for field, label in ADAPTER_LABELS:
adapter = getattr(instance, field)
conf[label] = AdapterProcessor.get_model_label(adapter)
rep[field] = adapter.adapter_name
if instance.llm_id:
rep["icon"] = AdapterProcessor.get_icon(instance.llm)
icon = Adapterkit().get_adapter_class_by_adapter_id(adapter.adapter_id).get_icon()Same underlying |
||
| rep[field] = adapter.adapter_name | ||
| conf[label] = model | ||
| if field == ProfileManagerKeys.LLM: | ||
| rep["icon"] = icon | ||
| if conf: | ||
| conf["Profile Name"] = instance.profile_name | ||
| rep["conf"] = conf | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 — Two things worth pinning down: 1. The frontend dereferences 2. Display labels are being used as payload keys. Also mixing Minimum: tighten the comment to say these are response keys and that Small accuracy note: |
||
| return rep | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||||
| """Profile serializer resolves adapter FKs to display data without an access check. | ||||||||||||||
|
|
||||||||||||||
| The DRF base is patched out so the assertions cover only that resolution. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| from __future__ import annotations | ||||||||||||||
|
|
||||||||||||||
| import unittest | ||||||||||||||
| from types import SimpleNamespace | ||||||||||||||
| from unittest.mock import patch | ||||||||||||||
|
|
||||||||||||||
| from backend.serializers import AuditSerializer | ||||||||||||||
|
|
||||||||||||||
| from prompt_studio.prompt_profile_manager_v2.serializers import ProfileManagerSerializer | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _adapter(name: str, model: str) -> SimpleNamespace: | ||||||||||||||
| return SimpleNamespace(adapter_name=name, model=model) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def _represent(instance: SimpleNamespace, base_rep: dict) -> dict: | ||||||||||||||
| with ( | ||||||||||||||
| patch.object(AuditSerializer, "to_representation", return_value=base_rep), | ||||||||||||||
| patch( | ||||||||||||||
| "prompt_studio.prompt_profile_manager_v2.serializers." | ||||||||||||||
| "AdapterProcessor.get_display_info", | ||||||||||||||
| side_effect=lambda adapter: ("openai.png", adapter.model), | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — the test mocks out the method it's named after, so none of the new logic is covered.
The mock also diverges from the real contract: the fake adapter exposes
@pytest.mark.parametrize("is_available,registry_icon,metadata,expected", [
(True, "/icons/adapter-icons/OpenAI.png", {"model": "gpt-4o"}, ("/icons/adapter-icons/OpenAI.png", "gpt-4o")),
(True, None, {"model": "gpt-4o"}, ("⚠️", "gpt-4o")), # b30385de
(False, "/icons/adapter-icons/OpenAI.png", {"model": "gpt-4o"}, ("⚠️", "gpt-4o")), # deprecated
(True, "/icons/adapter-icons/Qdrant.png", {}, ("/icons/adapter-icons/Qdrant.png", "qdrant")),
])plus One more worth adding, given this PR's whole premise is exposing this payload to users without adapter access: seed the fake metadata with Two smaller notes:
|
||||||||||||||
| ), | ||||||||||||||
| ): | ||||||||||||||
| return ProfileManagerSerializer().to_representation(instance) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class ProfileDisplayInfoTests(unittest.TestCase): | ||||||||||||||
| def test_display_info_resolved_without_adapter_access(self) -> None: | ||||||||||||||
| instance = SimpleNamespace( | ||||||||||||||
| profile_name="Prod", | ||||||||||||||
| llm=_adapter("Shared GPT", "gpt-4o"), | ||||||||||||||
| embedding_model=_adapter("Shared Embed", "text-embedding-3-small"), | ||||||||||||||
| vector_store=_adapter("Shared Qdrant", "qdrant"), | ||||||||||||||
| x2text=_adapter("Shared LLMW", "llmwhisperer"), | ||||||||||||||
| ) | ||||||||||||||
| base_rep = { | ||||||||||||||
| field: "some-uuid" | ||||||||||||||
| for field in ("llm", "embedding_model", "vector_store", "x2text") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| rep = _represent(instance, base_rep) | ||||||||||||||
|
|
||||||||||||||
| self.assertEqual( | ||||||||||||||
| rep["conf"], | ||||||||||||||
| { | ||||||||||||||
| "LLM": "gpt-4o", | ||||||||||||||
| "Embedding Model": "text-embedding-3-small", | ||||||||||||||
| "Vector Store": "qdrant", | ||||||||||||||
| "Text Extractor": "llmwhisperer", | ||||||||||||||
| "Profile Name": "Prod", | ||||||||||||||
| }, | ||||||||||||||
| ) | ||||||||||||||
| # Only the LLM contributes the tile icon. | ||||||||||||||
| self.assertEqual(rep["icon"], "openai.png") | ||||||||||||||
| # FK ids are replaced by the adapter names. | ||||||||||||||
| self.assertEqual(rep["llm"], "Shared GPT") | ||||||||||||||
|
|
||||||||||||||
| def test_unset_adapters_are_skipped(self) -> None: | ||||||||||||||
| instance = SimpleNamespace( | ||||||||||||||
| profile_name="Half configured", | ||||||||||||||
| llm=_adapter("Shared GPT", "gpt-4o"), | ||||||||||||||
| embedding_model=None, | ||||||||||||||
| vector_store=None, | ||||||||||||||
| x2text=None, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| rep = _represent(instance, {"llm": "some-uuid", "embedding_model": None}) | ||||||||||||||
|
|
||||||||||||||
| self.assertEqual( | ||||||||||||||
| rep["conf"], {"LLM": "gpt-4o", "Profile Name": "Half configured"} | ||||||||||||||
| ) | ||||||||||||||
| self.assertIsNone(rep["embedding_model"]) | ||||||||||||||
|
|
||||||||||||||
| def test_profile_with_no_adapters_has_empty_conf(self) -> None: | ||||||||||||||
| instance = SimpleNamespace( | ||||||||||||||
| profile_name="Empty", | ||||||||||||||
| llm=None, | ||||||||||||||
| embedding_model=None, | ||||||||||||||
| vector_store=None, | ||||||||||||||
| x2text=None, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| rep = _represent(instance, {}) | ||||||||||||||
|
|
||||||||||||||
| # No "Profile Name" either - the tile has nothing to show. | ||||||||||||||
| self.assertEqual(rep["conf"], {}) | ||||||||||||||
| self.assertNotIn("icon", rep) | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,10 @@ def get_permissions(self) -> list[Any]: | |
| return [IsOwnerOrSharedUserOrSharedToOrg()] | ||
|
|
||
| def get_queryset(self) -> QuerySet | None: | ||
| queryset = ProfileManager.objects.for_user(self.request.user) | ||
| # Serializer reads all four adapters per profile for the display info | ||
| queryset = ProfileManager.objects.for_user(self.request.user).select_related( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 — the
The endpoint that actually populates profile_manager_instances = ProfileManager.objects.filter(
prompt_studio_tool=prompt_tool
)
serialized_instances = ProfileManagerSerializer(
profile_manager_instances, many=True
).dataNo Suggested fix — add the same join in profile_manager_instances = ProfileManager.objects.filter(
prompt_studio_tool=prompt_tool
).select_related("llm", "embedding_model", "vector_store", "x2text")Worth keeping the join here too (it still helps |
||
| "llm", "embedding_model", "vector_store", "x2text" | ||
| ) | ||
| filter_args = FilterHelper.build_filter_args( | ||
| self.request, | ||
| ProfileManagerKeys.CREATED_BY, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,7 +83,6 @@ function PromptCardItems({ | |
| indexDocs, | ||
| isSimplePromptStudio, | ||
| isPublicSource, | ||
| adapters, | ||
| selectedHighlight, | ||
| details, | ||
| singlePassExtractMode, | ||
|
|
@@ -114,32 +113,6 @@ function PromptCardItems({ | |
| ); | ||
| }, [allTableSettings]); | ||
|
|
||
| const getModelOrAdapterId = (profile, adapters) => { | ||
| const result = { conf: {} }; | ||
| const keys = [ | ||
| { key: "llm", label: "LLM" }, | ||
| { key: "embedding_model", label: "Embedding Model" }, | ||
| { key: "vector_store", label: "Vector Store" }, | ||
| { key: "x2text", label: "Text Extractor" }, | ||
| ]; | ||
|
|
||
| keys.forEach((key) => { | ||
| const adapterName = profile[key.key]; | ||
| const adapter = adapters?.find( | ||
| (adapter) => adapter?.adapter_name === adapterName, | ||
| ); | ||
| if (adapter) { | ||
| result.conf[key.label] = | ||
| adapter?.model || adapter?.adapter_id?.split("|")[0]; | ||
| if (adapter?.adapter_type === "LLM") { | ||
| result.icon = adapter?.icon; | ||
| } | ||
| result.conf["Profile Name"] = profile?.profile_name; | ||
| } | ||
| }); | ||
| return result; | ||
| }; | ||
|
|
||
| const getUpdatedCoverage = (promptId, singlePass, promptOutputs) => { | ||
| let updatedCoverage = null; | ||
| Object.keys(promptOutputs).forEach((key) => { | ||
|
|
@@ -166,18 +139,18 @@ function PromptCardItems({ | |
| getUpdatedCoverage(promptId, singlePassExtractMode, promptOutputs) || | ||
| coverageCountData; | ||
|
|
||
| const getAdapterInfo = async (adapterData) => { | ||
| // If simple prompt studio, return early | ||
| useEffect(() => { | ||
| setExpandCard(true); | ||
| }, [isSinglePassExtractLoading]); | ||
|
|
||
| useEffect(() => { | ||
| if (isSimplePromptStudio) { | ||
| return; | ||
| } | ||
|
|
||
| // Update llmProfiles with additional fields | ||
| const updatedProfiles = llmProfiles?.map((profile) => { | ||
| return { ...getModelOrAdapterId(profile, adapterData), ...profile }; | ||
| }); | ||
| // conf/icon come off the profile payload; the viewer may not have | ||
| // access to the project's adapters. | ||
| setLlmProfileDetails( | ||
| updatedProfiles | ||
| (llmProfiles || []) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 — the fix is partial; two other output surfaces still resolve model names from the viewer's own adapter list. Removing the client-side lookup here is right, but the identical pattern survives in two places, both routed through
Both fetch Now that the backend ships If that's intentionally out of scope, worth saying so in the PR description — the "Can this PR break any existing features" section currently reads as though the fix is complete. |
||
| .map((profile) => ({ | ||
| ...profile, | ||
| isDefault: profile?.profile_id === selectedLlmProfileId, | ||
|
|
@@ -192,15 +165,7 @@ function PromptCardItems({ | |
| return 0; | ||
| }), | ||
| ); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| setExpandCard(true); | ||
| }, [isSinglePassExtractLoading]); | ||
|
|
||
| useEffect(() => { | ||
| getAdapterInfo(adapters); | ||
| }, [llmProfiles, selectedLlmProfileId]); | ||
| }, [llmProfiles, selectedLlmProfileId, isSimplePromptStudio]); | ||
|
|
||
| return ( | ||
| <Card | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { useState } from "react"; | |
| import { | ||
| displayPromptResult, | ||
| generateApiRunStatusId, | ||
| isImageUrl, | ||
| PROMPT_RUN_API_STATUSES, | ||
| PROMPT_RUN_TYPES, | ||
| } from "../../../helpers/GetStaticData"; | ||
|
|
@@ -355,13 +356,19 @@ function PromptOutput({ | |
| > | ||
| <div className="llm-info"> | ||
| <div className="llm-info-left"> | ||
| <Image | ||
| src={profile?.icon} | ||
| width={15} | ||
| height={15} | ||
| preview={false} | ||
| className="prompt-card-llm-icon" | ||
| /> | ||
| {isImageUrl(profile?.icon) ? ( | ||
| <Image | ||
| src={profile?.icon} | ||
| width={15} | ||
| height={15} | ||
| preview={false} | ||
| className="prompt-card-llm-icon" | ||
| /> | ||
| ) : ( | ||
| <span className="prompt-card-llm-icon"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 — when
The backend always sets One-character fix, and it reuses the fallback the backend already picked: <span className="prompt-card-llm-icon">{profile?.icon || "⚠️"}</span> |
||
| {profile?.icon} | ||
| </span> | ||
| )} | ||
| <Typography.Text | ||
| className="prompt-card-llm-title" | ||
| ellipsis={{ tooltip: profile?.conf?.LLM }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { Col, Image, Modal, Row, Typography } from "antd"; | ||
| import PropTypes from "prop-types"; | ||
| import { isImageUrl } from "../../../helpers/GetStaticData"; | ||
| import usePromptOutput from "../../../hooks/usePromptOutput"; | ||
| import { useCustomToolStore } from "../../../store/custom-tool-store"; | ||
| import SpaceWrapper from "../../widgets/space-wrapper/SpaceWrapper"; | ||
|
|
@@ -68,13 +69,19 @@ function PromptOutputsModal({ | |
| <div> | ||
| {displayLlmProfile && ( | ||
| <div className="prompt-output-llm-bg"> | ||
| <Image | ||
| src={profile?.icon} | ||
| width={15} | ||
| height={15} | ||
| preview={false} | ||
| className="prompt-card-llm-icon" | ||
| /> | ||
| {isImageUrl(profile?.icon) ? ( | ||
| <Image | ||
| src={profile?.icon} | ||
| width={15} | ||
| height={15} | ||
| preview={false} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 — this 13-line block is byte-identical to Worth extracting while it's only two copies — the
// prompt-card/ProfileIcon.jsx
function ProfileIcon({ icon }) {
if (!isImageUrl(icon)) {
return <span className="prompt-card-llm-icon">{icon || "⚠️"}</span>;
}
return (
<Image src={icon} width={15} height={15} preview={false}
className="prompt-card-llm-icon" />
);
}Both call sites collapse to |
||
| className="prompt-card-llm-icon" | ||
| /> | ||
| ) : ( | ||
| <span className="prompt-card-llm-icon"> | ||
| {profile?.icon} | ||
| </span> | ||
| )} | ||
| <Typography.Text className="prompt-card-llm-title"> | ||
| {profile?.conf?.LLM} | ||
| </Typography.Text> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3 (type design) —
tuple[str, str]is the weakest available encoding for(icon, model), and splitting the function removes the hazard for free.Both elements are
str, somodel, icon = AdapterProcessor.get_display_info(adapter)type-checks and silently ships an emoji as the model name. Only the docstring says which slot is which.Normally I'd weigh that against a
NamedTupleand conclude it isn't worth it — this repo has 34 bare-> tuple[...]returns against exactly one productionNamedTuple, andadapter_processor_v2/has zero dataclasses, so a 2-tuple is locally idiomatic and there's only one call site. But here a split is smaller than aNamedTupleand also fixes the wasted-work problem flagged onserializers.py:39:Two named single-value returns can't be swapped, need no new type, and let the caller resolve the icon only for the LLM — cutting registry walks from 4 per profile to 1.
Two smaller accuracy notes on the annotation itself:
strisn't actually enforced on either side.get_adapter_data_with_keyreturnsAny(line 87) and.metadatareturnsAny(models.py:191), so both values are unchecked. It holds in practice (modelis"type": "string"in all 37 adapter schemas), but mypy can't help —[tool.mypy] strict = trueis set inpyproject.toml, yet the mypy pre-commit hook is commented out and no workflow invokes it.adapter_idisCharField(default="")(models.py:75-79), so"".split("|")[0]is"". An adapter with nomodelin metadata and an emptyadapter_idreturns("⚠️", "")— satisfiesstr, renders as a blank tile, logs nothing.