diff --git a/api/oss/src/apis/fastapi/observability/utils/marshalling.py b/api/oss/src/apis/fastapi/observability/utils/marshalling.py index f29876d311..0e0bd7cc96 100644 --- a/api/oss/src/apis/fastapi/observability/utils/marshalling.py +++ b/api/oss/src/apis/fastapi/observability/utils/marshalling.py @@ -76,7 +76,7 @@ def unmarshall_attributes( marshalled: Dict[str, Any], ) -> Dict[str, Any]: """ - Unmarshals a dictionary of marshalled attributes into a nested dictionary + Unmarshalls a dictionary of marshalled attributes into a nested dictionary Example: marshalled = { diff --git a/api/oss/src/core/tracing/utils.py b/api/oss/src/core/tracing/utils.py index 36e7ccf12a..d62edb6ba9 100644 --- a/api/oss/src/core/tracing/utils.py +++ b/api/oss/src/core/tracing/utils.py @@ -59,7 +59,7 @@ def unmarshall_attributes( marshalled: OTelAttributes, ) -> OTelAttributes: """ - Unmarshals a dictionary of marshalled attributes into a nested dictionary + Unmarshalls a dictionary of marshalled attributes into a nested dictionary Example: marshalled = { @@ -89,42 +89,34 @@ def unmarshall_attributes( for key, value in marshalled.items(): keys = key.split(".") + current = unmarshalled - level = unmarshalled - - for i, part in enumerate(keys[:-1]): - if part.isdigit(): - part = int(part) - - if not isinstance(level, list): - level = [] - - while len(level) <= part: - level.append({}) - - level = level[part] + for i, key in enumerate(keys): + is_last = i == len(keys) - 1 + next_key = keys[i + 1] if not is_last else None + is_index = key.isdigit() + key = int(key) if is_index else key + if is_last: + if isinstance(current, list) and isinstance(key, int): + while len(current) <= key: + current.append(None) + current[key] = value + elif isinstance(current, dict): + current[key] = value else: - if part not in level: - level[part] = {} if not keys[i + 1].isdigit() else [] - - level = level[part] - - last_key = keys[-1] - - if last_key.isdigit(): - last_key = int(last_key) - - if not isinstance(level, list): - level = [] - - while len(level) <= last_key: - level.append(None) - - level[last_key] = value + next_is_index = next_key.isdigit() if next_key else False - else: - level[last_key] = value + if isinstance(current, list) and isinstance(key, int): + while len(current) <= key: + current.append([] if next_is_index else {}) + if current[key] is None: + current[key] = [] if next_is_index else {} + current = current[key] + elif isinstance(current, dict): + if key not in current: + current[key] = [] if next_is_index else {} + current = current[key] return unmarshalled diff --git a/api/oss/src/models/api/api_models.py b/api/oss/src/models/api/api_models.py index 509c162fa3..1623a722f2 100644 --- a/api/oss/src/models/api/api_models.py +++ b/api/oss/src/models/api/api_models.py @@ -74,6 +74,10 @@ class UpdateAppOutput(CreateAppOutput): pass +class ReadAppOutput(CreateAppOutput): + pass + + class AppOutput(CreateAppOutput): pass diff --git a/api/oss/src/routers/app_router.py b/api/oss/src/routers/app_router.py index 10fff40c93..bd7555f6c2 100644 --- a/api/oss/src/routers/app_router.py +++ b/api/oss/src/routers/app_router.py @@ -13,8 +13,9 @@ from oss.src.models.api.api_models import ( App, UpdateApp, - UpdateAppOutput, CreateAppOutput, + ReadAppOutput, + UpdateAppOutput, AddVariantFromURLPayload, AddVariantFromKeyPayload, ) @@ -260,6 +261,47 @@ async def create_app( return CreateAppOutput(app_id=str(app_db.id), app_name=str(app_db.app_name)) +@router.get("/{app_id}/", response_model=ReadAppOutput, operation_id="create_app") +async def read_app( + request: Request, + app_id: str, +) -> ReadAppOutput: + """ + Retrieve an app by its ID. + + Args: + app_id (str): The ID of the app to retrieve. + + Returns: + ReadAppOutput: The output containing the app's ID and name. + + Raises: + HTTPException: If there is an error retrieving the app or the user does not have permission to access the app. + """ + + try: + app = await db_manager.fetch_app_by_id(app_id) + except db_manager.NoResultFound: + raise HTTPException( + status_code=404, detail=f"No application with ID '{app_id}' found" + ) + + if is_ee(): + has_permission = await check_action_access( + user_uid=request.state.user_id, + project_id=str(app.project_id), + permission=Permission.VIEW_APPLICATIONS, + ) + if not has_permission: + error_msg = "You do not have access to perform this action. Please contact your organization admin." + return JSONResponse( + {"detail": error_msg}, + status_code=403, + ) + + return ReadAppOutput(app_id=str(app.id), app_name=str(app.app_name)) + + @router.patch("/{app_id}/", response_model=UpdateAppOutput, operation_id="update_app") async def update_app( app_id: str, diff --git a/api/oss/tests/manual/tracing/ingestion/openinference_openai_streaming.py b/api/oss/tests/manual/tracing/ingestion/openinference_openai_streaming.py index f059c2e952..fe25016bac 100644 --- a/api/oss/tests/manual/tracing/ingestion/openinference_openai_streaming.py +++ b/api/oss/tests/manual/tracing/ingestion/openinference_openai_streaming.py @@ -15,7 +15,6 @@ openai = OpenAI() - OpenAIInstrumentor().instrument() diff --git a/api/pyproject.toml b/api/pyproject.toml index 57fb76a93f..06b4db2c9f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api" -version = "0.56.2" +version = "0.56.3" description = "Agenta API" authors = [ { name = "Mahmoud Mabrouk", email = "mahmoud@agenta.ai" }, diff --git a/sdk/agenta/sdk/middleware/config.py b/sdk/agenta/sdk/middleware/config.py index dc2e6ef24f..cda301ff07 100644 --- a/sdk/agenta/sdk/middleware/config.py +++ b/sdk/agenta/sdk/middleware/config.py @@ -93,13 +93,18 @@ async def _get_config(self, request: Request) -> Optional[Tuple[Dict, Dict]]: return parameters, references - config = {} + config = dict() + app_ref = None is_test_path = request.url.path.endswith("/test") are_refs_missing = not variant_ref and not environment_ref - should_fetch = not is_test_path or not are_refs_missing + should_fetch_application_revision = not is_test_path or not are_refs_missing + is_app_ref_incomplete = ( + application_ref and application_ref.id and not application_ref.slug + ) + should_fetch_application = is_app_ref_incomplete - if should_fetch: + if should_fetch_application_revision: async with httpx.AsyncClient() as client: response = await client.post( f"{self.host}/api/variants/configs/fetch", @@ -110,13 +115,26 @@ async def _get_config(self, request: Request) -> Optional[Tuple[Dict, Dict]]: if response.status_code == 200: config = response.json() - if not config: - config["application_ref"] = refs[ - "application_ref" - ] # by default, application_ref will always have an id - parameters = None - else: + elif should_fetch_application: + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.host}/api/apps/{application_ref.id}", + headers=headers, + ) + + if response.status_code == 200: + app = response.json() + app_ref = { + "id": app.get("app_id"), + "slug": app.get("app_name"), + } + + if config: parameters = config.get("params") + else: + # by default, application_ref will always have an id + config["application_ref"] = app_ref or refs["application_ref"] + parameters = None references = {} diff --git a/sdk/agenta/sdk/tracing/attributes.py b/sdk/agenta/sdk/tracing/attributes.py index bdc042c9bb..4fd99c00b9 100644 --- a/sdk/agenta/sdk/tracing/attributes.py +++ b/sdk/agenta/sdk/tracing/attributes.py @@ -6,7 +6,7 @@ Attribute = Union[Primitive, PrimitivesSequence] -def _marshal( +def _marshall( unmarshalled: Dict[str, Any], *, parent_key: Optional[str] = "", @@ -59,7 +59,7 @@ def _marshal( dict_key = child_key marshalled.update( - _marshal( + _marshall( value, parent_key=dict_key, depth=depth + 1, @@ -76,7 +76,7 @@ def _marshal( if isinstance(item, (dict, list)): marshalled.update( - _marshal( + _marshall( item, parent_key=list_key, depth=depth + 1, @@ -177,7 +177,7 @@ def serialize( k: v for k, v in { _encode_key(namespace, key): _encode_value(value) - for key, value in _marshal(attributes, max_depth=max_depth).items() + for key, value in _marshall(attributes, max_depth=max_depth).items() }.items() if v is not None } diff --git a/sdk/agenta/sdk/tracing/inline.py b/sdk/agenta/sdk/tracing/inline.py index 971c4a108d..7400cde35c 100644 --- a/sdk/agenta/sdk/tracing/inline.py +++ b/sdk/agenta/sdk/tracing/inline.py @@ -532,11 +532,11 @@ def _connect_tree_dfs( from copy import copy -def _unmarshal_attributes( +def _unmarshall_attributes( marshalled: Dict[str, Any], ) -> Dict[str, Any]: """ - Unmarshals a dictionary of marshalled attributes into a nested dictionary + Unmarshalls a dictionary of marshalled attributes into a nested dictionary Example: marshalled = { @@ -566,42 +566,34 @@ def _unmarshal_attributes( for key, value in marshalled.items(): keys = key.split(".") - - level = unmarshalled - - for i, part in enumerate(keys[:-1]): - if part.isdigit(): - part = int(part) - - if not isinstance(level, list): - level = [] - - while len(level) <= part: - level.append({}) - - level = level[part] - + current = unmarshalled + + for i, key in enumerate(keys): + is_last = i == len(keys) - 1 + next_key = keys[i + 1] if not is_last else None + is_index = key.isdigit() + key = int(key) if is_index else key + + if is_last: + if isinstance(current, list) and isinstance(key, int): + while len(current) <= key: + current.append(None) + current[key] = value + elif isinstance(current, dict): + current[key] = value else: - if part not in level: - level[part] = {} if not keys[i + 1].isdigit() else [] - - level = level[part] - - last_key = keys[-1] - - if last_key.isdigit(): - last_key = int(last_key) - - if not isinstance(level, list): - level = [] - - while len(level) <= last_key: - level.append(None) - - level[last_key] = value - - else: - level[last_key] = value + next_is_index = next_key.isdigit() if next_key else False + + if isinstance(current, list) and isinstance(key, int): + while len(current) <= key: + current.append([] if next_is_index else {}) + if current[key] is None: + current[key] = [] if next_is_index else {} + current = current[key] + elif isinstance(current, dict): + if key not in current: + current[key] = [] if next_is_index else {} + current = current[key] return unmarshalled @@ -750,7 +742,7 @@ def _parse_from_attributes( for key in _data.keys(): del otel_span_dto.attributes[_encode_key("data", key)] - # _data = _unmarshal_attributes(_data) + # _data = _unmarshall_attributes(_data) _data = _data if _data else None # METRICS @@ -759,7 +751,7 @@ def _parse_from_attributes( for key in _metrics.keys(): del otel_span_dto.attributes[_encode_key("metrics", key)] - # _metrics = _unmarshal_attributes(_metrics) + # _metrics = _unmarshall_attributes(_metrics) _metrics = _metrics if _metrics else None # META @@ -768,7 +760,7 @@ def _parse_from_attributes( for key in _meta.keys(): del otel_span_dto.attributes[_encode_key("meta", key)] - # _meta = _unmarshal_attributes(_meta) + # _meta = _unmarshall_attributes(_meta) _meta = _meta if _meta else None # TAGS @@ -904,7 +896,7 @@ def parse_to_agenta_span_dto( ) -> SpanDTO: # DATA if span_dto.data: - span_dto.data = _unmarshal_attributes(span_dto.data) + span_dto.data = _unmarshall_attributes(span_dto.data) if "outputs" in span_dto.data: if "__default__" in span_dto.data["outputs"]: @@ -912,19 +904,19 @@ def parse_to_agenta_span_dto( # METRICS if span_dto.metrics: - span_dto.metrics = _unmarshal_attributes(span_dto.metrics) + span_dto.metrics = _unmarshall_attributes(span_dto.metrics) # META if span_dto.meta: - span_dto.meta = _unmarshal_attributes(span_dto.meta) + span_dto.meta = _unmarshall_attributes(span_dto.meta) # TAGS if span_dto.tags: - span_dto.tags = _unmarshal_attributes(span_dto.tags) + span_dto.tags = _unmarshall_attributes(span_dto.tags) # REFS if span_dto.refs: - span_dto.refs = _unmarshal_attributes(span_dto.refs) + span_dto.refs = _unmarshall_attributes(span_dto.refs) if isinstance(span_dto.links, list): for link in span_dto.links: diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index e23357c6e6..f83bee8d79 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.56.2" +version = "0.56.3" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = [ diff --git a/web/oss/package.json b/web/oss/package.json index 3916ba5053..c27a10f66e 100644 --- a/web/oss/package.json +++ b/web/oss/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/oss", - "version": "0.56.2", + "version": "0.56.3", "private": true, "engines": { "node": ">=18" @@ -76,7 +76,7 @@ "ajv": "^8.17.1", "antd": "^5.26.1", "autoprefixer": "10.4.20", - "axios": "^1.11.0", + "axios": "^1.12.2", "blakejs": "^1.2.1", "classnames": "^2.3.2", "clsx": "^2.1.1", diff --git a/web/oss/src/components/AppGlobalWrappers/index.tsx b/web/oss/src/components/AppGlobalWrappers/index.tsx index 0a33d871ba..b3c05b6536 100644 --- a/web/oss/src/components/AppGlobalWrappers/index.tsx +++ b/web/oss/src/components/AppGlobalWrappers/index.tsx @@ -12,6 +12,11 @@ const TraceDrawer = dynamic( {ssr: false}, ) +const EvalRunFocusDrawer = dynamic( + () => import("@/oss/components/EvalRunDetails/AutoEvalRun/components/EvalRunFocusDrawer"), + {ssr: false}, +) + const SelectDeployVariantModalWrapper = dynamic( () => import("@/oss/components/DeploymentsDashboard/modals/SelectDeployVariantModalWrapper"), {ssr: false}, @@ -165,6 +170,7 @@ const AppGlobalWrappers = () => { <> + diff --git a/web/oss/src/components/EnhancedUIs/Table/index.tsx b/web/oss/src/components/EnhancedUIs/Table/index.tsx index 57680c485b..ce6c84d21d 100644 --- a/web/oss/src/components/EnhancedUIs/Table/index.tsx +++ b/web/oss/src/components/EnhancedUIs/Table/index.tsx @@ -16,6 +16,7 @@ const EnhancedTableInner = , @@ -262,11 +263,10 @@ const EnhancedTableInner = extends Omit { // Get variant metadata and derived prompts (prefers local cache, falls back to spec) const variant = useAtomValue(variantByRevisionIdAtomFamily(variantId)) as any // const prompts = useAtomValue(promptsAtomFamily(variantId)) || [] - // Extract values from the variant object + // Guard against undefined variant during commit invalidation (after hooks) + if (!variant) { + return ( +
+
+ Loading variant data... +
+
+ ) + } + + // Extract values from the variant object (safe now) const variantName = variant.variantName const revision = variant.revision // Determine target revision based on the latest revision number for this variant, not the base @@ -50,9 +67,12 @@ const CommitVariantChangesModalContent = ({ // transformToRequestBody({variant: composedVariant})?.ag_config const oldParams = variant.parameters - const onChange = useCallback((e: RadioChangeEvent) => { - setSelectedCommitType({...selectedCommitType, type: e.target.value}) - }, []) + const onChange = useCallback( + (e: RadioChangeEvent) => { + setSelectedCommitType({...selectedCommitType, type: e.target.value}) + }, + [selectedCommitType, setSelectedCommitType], + ) // Snapshot diff content using refs (no re-renders, computed on first mount) const initialOriginalRef = useRef(null) @@ -77,84 +97,145 @@ const CommitVariantChangesModalContent = ({ } } - // Guard against undefined variant during commit invalidation (after hooks) - if (!variant) { - return ( -
-
- Loading variant data... -
-
- ) - } + const environmentOptions = ( + Object.keys(deploymentStatusColors) as Array + ).map((env) => ({ + value: env, + label: , + })) + + // Ensure DiffView gets strings even when params are undefined + const originalForDiff = initialOriginalRef.current ?? JSON.stringify(oldParams ?? {}) + const modifiedForDiff = initialModifiedRef.current ?? JSON.stringify(params ?? oldParams ?? {}) return ( -
-
- How would you like to save the changes? - -
- - As a new version - -
- {variantName} -
- - - +
+
+
+ + How would you like to save the changes? + +
+
+ + + As a new version + + + + + +
+ {variantName} +
+ + + +
+
+
+ +
+ + + As a new variant + + + + + +
+ + setSelectedCommitType((prev) => { + const prevType = prev?.type + const guaranteedType: "version" | "variant" = + prevType === "version" || prevType === "variant" + ? prevType + : "variant" + return { + type: guaranteedType, + name: e.target.value, + } + }) + } + suffix={} + /> +
-
- - As a new variant - -
- - setSelectedCommitType((prev) => { - const prevType = prev?.type - const guaranteedType: "version" | "variant" = - prevType === "version" || prevType === "variant" - ? prevType - : "variant" - return { - type: guaranteedType, - name: e.target.value, - } - }) - } - suffix={} +
+
+ onToggleDeploy(event.target.checked)} + disabled={isDeploymentPending} + > + + Deploy after commit + + + + + +