Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion sdks/python/agenta/sdk/middlewares/running/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@
if "mistral" not in _PROVIDER_KINDS:
_PROVIDER_KINDS.append("mistral")

# Mapping from provider kind to environment variable name.
# Most providers follow the pattern PROVIDER_API_KEY, but some have
# underscores in their kind string (e.g. "together_ai") where the env
# var drops the underscore (TOGETHERAI_API_KEY). This explicit mapping
# mirrors the one in the Daytona runner and the frontend llmProviders.ts.
_PROVIDER_ENV_VAR_MAP: Dict[str, str] = {
"openai": "OPENAI_API_KEY",
"cohere": "COHERE_API_KEY",
"anyscale": "ANYSCALE_API_KEY",
"deepinfra": "DEEPINFRA_API_KEY",
"alephalpha": "ALEPHALPHA_API_KEY",
"groq": "GROQ_API_KEY",
"minimax": "MINIMAX_API_KEY",
"mistral": "MISTRAL_API_KEY",
"mistralai": "MISTRAL_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"perplexityai": "PERPLEXITYAI_API_KEY",
"together_ai": "TOGETHERAI_API_KEY",
"openrouter": "OPENROUTER_API_KEY",
"gemini": "GEMINI_API_KEY",
}
Comment on lines +40 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Run ruff format and ruff check --fix before committing.

As per coding guidelines, Python changes in {api,sdk,sdks}/**/*.py must be formatted and linted with ruff.

Source: Coding guidelines


_AUTH_ENABLED = (
getenv("AGENTA_SERVICES_MIDDLEWARE_AUTH_ENABLED")
or getenv("AGENTA_SERVICE_MIDDLEWARE_AUTH_ENABLED")
Expand Down Expand Up @@ -306,7 +328,7 @@ async def get_secrets(
try:
for provider_kind in _PROVIDER_KINDS:
provider = provider_kind
key_name = f"{provider.upper()}_API_KEY"
key_name = _PROVIDER_ENV_VAR_MAP.get(provider, f"{provider.upper()}_API_KEY")
key = getenv(key_name)

if not key:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ export const triggerRunInvocationAtom = atom(
traceId: result.traceId ?? undefined,
status: "failure",
references,
error: {message: errorMessage},
error: {
message: errorMessage,
...(result.error?.stacktrace ? {stacktrace: result.error.stacktrace} : {}),
...(result.error?.type ? {type: result.error.type} : {}),
},
})

await updateScenarioStatus(scenarioId, EvaluationStatus.FAILURE)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type {MouseEvent} from "react"

import {message} from "@agenta/ui/app-message"
import {getDefaultStore} from "jotai"
import Router from "next/router"
Expand All @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState

const getActiveAppId = (): string | null => store.get(routerAppIdAtom)

export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement | null
if (!target) return false
const interactiveSelector =
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger"
return Boolean(target.closest(interactiveSelector))
}

interface NavigateToRunParams {
record: EvaluationRunTableRow
scope: "app" | "project"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {activePreviewProjectIdAtom} from "@/oss/components/EvalRunDetails/atoms/
import {clearAllMetricStatsCaches} from "@/oss/components/EvalRunDetails/atoms/runMetrics"
import {
InfiniteVirtualTableFeatureShell,
shouldIgnoreRowClick,
type TableFeaturePagination,
type TableScopeConfig,
} from "@/oss/components/InfiniteVirtualTable"
Expand All @@ -34,7 +35,6 @@ import {
} from "@/oss/lib/onboarding"
import {useQueryParamState} from "@/oss/state/appState"

import {shouldIgnoreRowClick} from "../../actions/navigationActions"
import {
evaluationRunsDeleteContextAtom,
evaluationRunsTableFetchEnabledAtom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,36 @@ import useTableExport from "./useTableExport"
const dummySearchAtom = atom("")

/**
* Helper to detect if a click event should be ignored for row navigation
* Default CSS selectors for interactive elements that should not trigger row navigation.
* Consolidated from all table implementations to ensure consistent click-through behavior.
*/
export const INTERACTIVE_ROW_SELECTORS = [
"button",
"a",
"input",
"textarea",
"select",
"[role='button']",
"[role='menuitem']",
"[role='checkbox']",
"[data-interactive]",
".ant-dropdown-trigger",
".ant-checkbox-wrapper",
".ant-checkbox",
".ant-checkbox-input",
".ant-checkbox-inner",
".ant-btn",
".ant-select",
].join(", ")

/**
* Helper to detect if a click event should be ignored for row navigation.
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
*/
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
const target = event.target as HTMLElement

// Check if clicking on interactive elements
if (
target.closest("button") ||
target.closest("a") ||
target.closest(".ant-dropdown-trigger") ||
target.closest(".ant-checkbox-wrapper") ||
target.closest(".ant-select") ||
target.closest("input") ||
target.closest("textarea")
) {
return true
}

return false
if (!target) return false
return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS))
}

/** Configuration for built-in search. When provided, the hook manages search state internally. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useCallback, useMemo, useState} from "react"
import React, {useCallback, useMemo, useState} from "react"

import {
ColumnVisibilityMenuTrigger,
defaultHeaderVariant,
detectColumnTypes,
InfiniteVirtualTableFeatureShell,
shouldIgnoreRowClick,
type TableScopeConfig,
type TypeChipConfig,
useTypeChipFeature,
Expand Down Expand Up @@ -758,7 +759,10 @@ export function TestcasesTableShell(props: TestcasesTableShellProps) {
size: "small" as const,
bordered: true,
onRow: (record: TestcaseTableRow) => ({
onClick: () => onRowClick(record),
onClick: (event: React.MouseEvent) => {
if (shouldIgnoreRowClick(event)) return
onRowClick(record)
},
className: "cursor-pointer hover:bg-gray-50",
}),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {type Key, type ReactNode, useCallback, useEffect, useMemo, useState} from "react"

import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table"
import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table"
import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table"
import {useAtomValue, useSetAtom, useStore} from "jotai"
import dynamic from "next/dynamic"
Expand Down Expand Up @@ -307,7 +307,10 @@ const ObservabilityTable = () => {
sticky: true,
style: {cursor: "pointer"},
onRow: (record, index) => ({
onClick: () => handleTraceRowClick(record),
onClick: (event) => {
if (shouldIgnoreRowClick(event)) return
handleTraceRowClick(record)
},
"data-tour": index === 0 ? "trace-row" : undefined,
}),
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useCallback, useEffect, useMemo, useState} from "react"

import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table"
import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table"
import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table"
import {useAtomValue, useSetAtom} from "jotai"
import dynamic from "next/dynamic"
Expand Down Expand Up @@ -141,7 +141,10 @@ const SessionsTable: React.FC = () => {
bordered: true,
loading: isLoading && sessionIds.length === 0,
onRow: (record) => ({
onClick: () => openDrawer({sessionId: record.session_id}),
onClick: (event) => {
if (shouldIgnoreRowClick(event)) return
openDrawer({sessionId: record.session_id})
},
style: {cursor: "pointer"},
}),
}}
Expand Down
6 changes: 5 additions & 1 deletion web/oss/src/components/pages/prompts/PromptsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TableFeaturePagination,
TableScopeConfig,
} from "@agenta/ui/table"
import {shouldIgnoreRowClick} from "@agenta/ui/table"
import {message} from "antd"
import type {TableProps} from "antd/es/table"
import {useAtomValue, useSetAtom} from "jotai"
Expand Down Expand Up @@ -686,7 +687,10 @@ const PromptsPage = () => {
scroll: {x: "max-content" as const},
expandable: tableExpandableConfig,
onRow: (record: PromptsTableRow) => ({
onClick: () => handleRowClick(record),
onClick: (event: React.MouseEvent) => {
if (shouldIgnoreRowClick(event)) return
handleRowClick(record)
},
className: "cursor-pointer",
draggable: true,
onDragStart: (event: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useState} from "react"
import type {User} from "@agenta/shared/types"
import {message} from "@agenta/ui/app-message"
import {EditOutlined, MoreOutlined, SyncOutlined} from "@ant-design/icons"
import {ArrowClockwise, Trash} from "@phosphor-icons/react"
import {ArrowClockwise, Key, Trash} from "@phosphor-icons/react"
import {Button, Dropdown, Input, Modal, Space, Tag, Tooltip, Typography} from "antd"

import AlertPopup from "@/oss/components/AlertPopup/AlertPopup"
Expand All @@ -12,7 +12,7 @@ import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE"
import {useEntitlements} from "@/oss/lib/helpers/useEntitlements"
import {snakeToTitle} from "@/oss/lib/helpers/utils"
import {WorkspaceMember} from "@/oss/lib/Types"
import {updateUsername} from "@/oss/services/profile"
import {resetPassword, updateUsername} from "@/oss/services/profile"
import {
assignWorkspaceRole,
removeFromWorkspace,
Expand All @@ -23,6 +23,9 @@ import {useOrgData} from "@/oss/state/org"
import {useProfileData} from "@/oss/state/profile"
import {useWorkspaceRoles} from "@/oss/state/workspace"

import GenerateResetLinkModal from "./Modals/GenerateResetLinkModal"
import PasswordResetLinkModal from "./Modals/PasswordResetLinkModal"

export const Actions: React.FC<{
member: WorkspaceMember
hidden?: boolean
Expand All @@ -39,6 +42,10 @@ export const Actions: React.FC<{
const {refetch: refetchProfile} = useProfileData()
const [renameOpen, setRenameOpen] = useState(false)
const [renameValue, setRenameValue] = useState(user.username || "")
const [generateResetLinkOpen, setGenerateResetLinkOpen] = useState(false)
const [resetLinkOpen, setResetLinkOpen] = useState(false)
const [resetLink, setResetLink] = useState("")
const [resetLoading, setResetLoading] = useState(false)

if (hidden && !selfMenu) return null

Expand Down Expand Up @@ -90,6 +97,24 @@ export const Actions: React.FC<{
}
}

const handleResetPassword = async () => {
setResetLoading(true)
try {
const link = await resetPassword(user.id)
setGenerateResetLinkOpen(false)
setResetLink(link)
setResetLinkOpen(true)
} catch (error: any) {
const detail =
error?.response?.data?.detail ||
error?.message ||
"Unable to generate reset password link"
message.error(detail)
} finally {
setResetLoading(false)
}
}

return (
<>
<Dropdown
Expand Down Expand Up @@ -127,6 +152,19 @@ export const Actions: React.FC<{
},
]
: []),
...(isMember
? [
{
key: "reset_password",
label: "Reset password",
icon: <Key size={16} />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
setGenerateResetLinkOpen(true)
},
},
]
: []),
{
key: "remove",
label: "Remove",
Expand Down Expand Up @@ -165,6 +203,21 @@ export const Actions: React.FC<{
placeholder="New username"
/>
</Modal>

<GenerateResetLinkModal
open={generateResetLinkOpen}
username={user.username}
onCancel={() => setGenerateResetLinkOpen(false)}
onOk={handleResetPassword}
confirmLoading={resetLoading}
/>

<PasswordResetLinkModal
open={resetLinkOpen}
username={user.username}
generatedLink={resetLink}
onCancel={() => setResetLinkOpen(false)}
/>
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion web/oss/src/services/evaluations/invocations/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const upsertStepResultWithInvocation = async ({
status: string
references?: InvocationReferences
outputs?: unknown
error?: {message: string; stacktrace?: string}
error?: {message: string; stacktrace?: string; type?: string}
}): Promise<void> => {
const {projectId} = getProjectValues()

Expand Down
14 changes: 14 additions & 0 deletions web/oss/src/services/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,17 @@ export const changePassword = async (payload: {
body: JSON.stringify(payload),
})
}

/**
* Generate a password reset link for a user (admin action).
* Returns the reset password link string.
*/
export const resetPassword = async (userId: string): Promise<string> => {
const base = getBaseUrl()
const url = new URL("api/profile/reset-password", base)
url.searchParams.set("user_id", userId)
const data = await fetchJson<string>(url, {
method: "POST",
})
return data
}
Comment on lines +60 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use Fern-generated client instead of raw fetchJson.

As per coding guidelines, all new frontend API code must go through the Fern-generated client, not raw fetch/axios. The Fern-generated client already provides a reset password method that handles this endpoint with proper typing and error handling.

♻️ Recommended refactor to use Fern-generated client
-export const resetPassword = async (userId: string): Promise<string> => {
-    const base = getBaseUrl()
-    const url = new URL("api/profile/reset-password", base)
-    url.searchParams.set("user_id", userId)
-    const data = await fetchJson<string>(url, {
-        method: "POST",
-    })
-    return data
-}
+export const resetPassword = async (userId: string): Promise<string> => {
+    const client = getAgentaSdkClient({host: getAgentaApiUrl()})
+    const response = await client.users.resetUserPassword({user_id: userId})
+    // Extract the reset link from the response based on the actual API contract
+    return response as unknown as string
+}

Note: You may need to adjust the return type handling based on the actual Fern client response structure.

Based on learnings, the Fern-generated client at web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts already implements the resetUserPassword method for the profile/reset-password endpoint with proper query parameter handling.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Generate a password reset link for a user (admin action).
* Returns the reset password link string.
*/
export const resetPassword = async (userId: string): Promise<string> => {
const base = getBaseUrl()
const url = new URL("api/profile/reset-password", base)
url.searchParams.set("user_id", userId)
const data = await fetchJson<string>(url, {
method: "POST",
})
return data
}
/**
* Generate a password reset link for a user (admin action).
* Returns the reset password link string.
*/
export const resetPassword = async (userId: string): Promise<string> => {
const client = getAgentaSdkClient({host: getAgentaApiUrl()})
const response = await client.users.resetUserPassword({user_id: userId})
// Extract the reset link from the response based on the actual API contract
return response as unknown as string
}

Source: Coding guidelines

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
EXPORT_RESOLVE_SKIP,
InfiniteVirtualTableFeatureShell,
createActionsColumn,
shouldIgnoreRowClick,
type InfiniteVirtualTableRowSelection,
type TableScopeConfig,
type TableExportColumnContext,
Expand Down Expand Up @@ -1674,8 +1675,7 @@ const ScenarioListView = memo(function ScenarioListView({

// Row click opens annotation drawer
const handleRowClick = useCallback((_event: React.MouseEvent, record: ScenarioTableRow) => {
const target = _event.target as HTMLElement
if (target?.closest("[data-ivt-stop-row-click]")) return
if (shouldIgnoreRowClick(_event)) return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
setDrawerScenarioId(record.scenarioId)
}, [])

Expand Down
2 changes: 2 additions & 0 deletions web/packages/agenta-entities/src/runnable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export interface ExecutionResult {
error?: {
message: string
code?: string
type?: string
stacktrace?: string
}
trace?: TraceInfo
metrics?: ExecutionMetrics
Expand Down
Loading