diff --git a/web/oss/src/components/DrillInView/OSSdrillInUIProvider.tsx b/web/oss/src/components/DrillInView/OSSdrillInUIProvider.tsx index 5f039f3a53..cb28b7665a 100644 --- a/web/oss/src/components/DrillInView/OSSdrillInUIProvider.tsx +++ b/web/oss/src/components/DrillInView/OSSdrillInUIProvider.tsx @@ -23,21 +23,21 @@ import {useMemo, type ReactNode} from "react" -import {DrillInUIProvider, type GatewayToolsBridge} from "@agenta/entity-ui/drill-in" -import {EditorProvider} from "@agenta/ui/editor" -import {SharedEditor} from "@agenta/ui/shared-editor" -import {useSetAtom} from "jotai" - import { buildToolSlug, catalogDrawerOpenAtom, + fetchActionDetail as fetchToolActionDetail, useCatalogActions, useConnectionsQuery, useIntegrationDetail, -} from "@/oss/features/gateway-tools" +} from "@agenta/entities/gatewayTool" +import {DrillInUIProvider, type GatewayToolsBridge} from "@agenta/entity-ui/drill-in" +import {EditorProvider} from "@agenta/ui/editor" +import {SharedEditor} from "@agenta/ui/shared-editor" +import {useSetAtom} from "jotai" + import {useLLMProviderConfig} from "@/oss/hooks/useLLMProviderConfig" import {isToolsEnabled} from "@/oss/lib/helpers/isEE" -import {fetchActionDetail as fetchToolActionDetail} from "@/oss/services/tools/api" interface OSSdrillInUIProviderProps { children: ReactNode @@ -121,27 +121,40 @@ function GatewayToolsEnabledProvider({ const gatewayTools = useMemo( () => ({ enabled: true, - connections: connections.map((connection) => ({ - id: connection.id, - slug: connection.slug, - name: connection.name, - integration_key: connection.integration_key, - provider_key: connection.provider_key, - flags: connection.flags, - })), + connections: connections + .filter((c) => typeof c.id === "string" && typeof c.slug === "string") + .map((connection) => ({ + id: connection.id as string, + slug: connection.slug as string, + name: connection.name ?? undefined, + integration_key: connection.integration_key, + provider_key: connection.provider_key, + flags: (connection.flags ?? undefined) as + | Record + | undefined, + })), connectionsLoading: isLoading, onOpenCatalog: () => setCatalogDrawerOpen(true), - useIntegrationInfo: useGatewayToolsIntegrationInfo, + useIntegrationInfo: (integrationKey: string) => { + const info = useGatewayToolsIntegrationInfo(integrationKey) + return { + name: info.name, + logo: info.logo ?? undefined, + isLoading: info.isLoading, + } + }, useActions: useGatewayToolsCatalogActions, buildToolSlug, fetchActionDetail: async (provider: string, integration: string, action: string) => { const detail = await fetchToolActionDetail(provider, integration, action) + const detailedAction = + detail.action && "schemas" in detail.action ? detail.action : null return { action: { - description: detail.action?.description, - schemas: { - inputs: detail.action?.schemas?.inputs, - }, + description: detailedAction?.description ?? undefined, + schemas: detailedAction?.schemas + ? {inputs: detailedAction.schemas.inputs} + : undefined, }, } }, diff --git a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/ConfigureProviderDrawerContent.tsx b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/ConfigureProviderDrawerContent.tsx index ec773e66bf..2ef7b96fa5 100644 --- a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/ConfigureProviderDrawerContent.tsx +++ b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/ConfigureProviderDrawerContent.tsx @@ -1,15 +1,19 @@ import React, {useEffect, useMemo, useState} from "react" +import { + PROVIDER_KINDS, + PROVIDER_LABELS, + STANDARD_PROVIDER_KINDS, + useVaultSecret, +} from "@agenta/entities/secret" +import type {LlmProvider} from "@agenta/shared/types" import {SelectLLMProviderBase, type ProviderGroup} from "@agenta/ui/select-llm-provider" import {capitalize} from "@agenta/ui/select-llm-provider" import {Plus, WarningCircle} from "@phosphor-icons/react" import {Button, Form, Input, Typography} from "antd" import {useWatch} from "antd/lib/form/Form" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" import {isSlugInputValid} from "@/oss/lib/helpers/utils" -import {PROVIDER_KINDS, PROVIDER_LABELS, SecretDTOProvider} from "@/oss/lib/Types" import LabelInput from "../../../assets/LabelInput" @@ -103,7 +107,7 @@ const ConfigureProviderDrawerContent = ({ const [errorMessage, setErrorMessage] = useState("") const {handleModifyCustomVaultSecret} = useVaultSecret() - const standardProviders = useMemo(() => [...Object.values(SecretDTOProvider)], []) + const standardProviders = useMemo(() => [...STANDARD_PROVIDER_KINDS], []) const customProviders = useMemo(() => ["azure", "bedrock", "vertex_ai", "custom"], []) const validProviders = useMemo( () => [...customProviders, ...standardProviders], diff --git a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/constants.ts b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/constants.ts index 3cefde32b0..def625f5c9 100644 --- a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/constants.ts +++ b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/constants.ts @@ -1,5 +1,5 @@ -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" -import {SecretDTOProvider} from "@/oss/lib/Types" +import {STANDARD_PROVIDER_KINDS} from "@agenta/entities/secret" +import type {LlmProvider} from "@agenta/shared/types" export const PROVIDER_FIELDS: { key: keyof LlmProvider @@ -21,7 +21,7 @@ export const PROVIDER_FIELDS: { label: "API key", placeholder: "Enter API key", note: "This secret will be encrypted in transit and at rest.", - model: ["azure", "custom", ...Object.values(SecretDTOProvider)], + model: ["azure", "custom", ...STANDARD_PROVIDER_KINDS], required: false, }, { diff --git a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/types.d.ts b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/types.d.ts index b8dcb59cde..0eddc170cd 100644 --- a/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/types.d.ts +++ b/web/oss/src/components/ModelRegistry/Drawers/ConfigureProviderDrawer/assets/types.d.ts @@ -1,7 +1,6 @@ +import type {LlmProvider} from "@agenta/shared/types" import {DrawerProps, FormInstance, InputProps} from "antd" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" - export interface ConfigureProviderDrawerProps extends DrawerProps { selectedProvider?: LlmProvider | null } diff --git a/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/assets/types.d.ts b/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/assets/types.d.ts index d3e5625e94..872025274e 100644 --- a/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/assets/types.d.ts +++ b/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/assets/types.d.ts @@ -1,7 +1,6 @@ +import type {LlmProvider} from "@agenta/shared/types" import {InputProps, ModalProps} from "antd" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" - export interface ConfigureProviderModalProps extends ModalProps { selectedProvider: LlmProvider | null } diff --git a/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx b/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx index 3412a27d98..185493a103 100644 --- a/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx +++ b/web/oss/src/components/ModelRegistry/Modals/ConfigureProviderModal/index.tsx @@ -1,10 +1,10 @@ import {useEffect, useState} from "react" +import {useVaultSecret} from "@agenta/entities/secret" import {message} from "@agenta/ui/app-message" import dynamic from "next/dynamic" import EnhancedModal from "@/oss/components/EnhancedUIs/Modal" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {ConfigureProviderModalProps} from "./assets/types" diff --git a/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/assets/types.d.ts b/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/assets/types.d.ts index d5fe6b8dc8..9875b28279 100644 --- a/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/assets/types.d.ts +++ b/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/assets/types.d.ts @@ -1,7 +1,6 @@ +import type {LlmProvider} from "@agenta/shared/types" import {ModalProps} from "antd" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" - export interface DeleteProviderModalProps extends ModalProps { selectedProvider: LlmProvider | null } diff --git a/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/index.tsx b/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/index.tsx index 2be6f01a10..0c7b4cbcff 100644 --- a/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/index.tsx +++ b/web/oss/src/components/ModelRegistry/Modals/DeleteProviderModal/index.tsx @@ -1,11 +1,11 @@ import {useState} from "react" +import {useVaultSecret} from "@agenta/entities/secret" +import type {LlmProvider} from "@agenta/shared/types" import {Trash} from "@phosphor-icons/react" import dynamic from "next/dynamic" import EnhancedModal from "@/oss/components/EnhancedUIs/Modal" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" import {DeleteProviderModalProps} from "./assets/types" diff --git a/web/oss/src/components/Playground/Components/PlaygroundGenerations/assets/GatewayToolExecuteButton.tsx b/web/oss/src/components/Playground/Components/PlaygroundGenerations/assets/GatewayToolExecuteButton.tsx index 1bd4b24b38..0b59c6fff1 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundGenerations/assets/GatewayToolExecuteButton.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundGenerations/assets/GatewayToolExecuteButton.tsx @@ -1,11 +1,10 @@ import React, {useCallback, useState} from "react" +import {executeToolCall} from "@agenta/entities/gatewayTool" import {CaretDown, Lightning} from "@phosphor-icons/react" import {Dropdown, message as antMessage} from "antd" import {v4 as uuidv4} from "uuid" -import {executeToolCall} from "@/oss/services/tools/api" - // Gateway tool function name format: tools__{provider}__{integration}__{action}__{connection} // Double-underscore is used because LLM providers forbid dots in function names. // The /tools/call API normalises __ → . before parsing. diff --git a/web/oss/src/components/Playground/Components/PlaygroundVariantConfigPrompt/assets/GatewayToolsPanel.tsx b/web/oss/src/components/Playground/Components/PlaygroundVariantConfigPrompt/assets/GatewayToolsPanel.tsx index 957b6b508a..d576a638de 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundVariantConfigPrompt/assets/GatewayToolsPanel.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundVariantConfigPrompt/assets/GatewayToolsPanel.tsx @@ -1,20 +1,21 @@ import {useMemo} from "react" -import {Play, Plus} from "@phosphor-icons/react" -import {Button, Collapse, Empty, Spin, Tag, Tooltip, Typography} from "antd" -import {useSetAtom} from "jotai" -import Image from "next/image" - -import ConnectionStatusBadge from "@/oss/components/pages/settings/Tools/components/ConnectionStatusBadge" import { useConnectionsQuery, catalogDrawerOpenAtom, executionDrawerAtom, useIntegrationDetail, -} from "@/oss/features/gateway-tools" -import CatalogDrawer from "@/oss/features/gateway-tools/drawers/CatalogDrawer" -import ToolExecutionDrawer from "@/oss/features/gateway-tools/drawers/ToolExecutionDrawer" -import type {ConnectionItem} from "@/oss/services/tools/api/types" + type ToolConnection, +} from "@agenta/entities/gatewayTool" +import { + CatalogDrawer, + ConnectionStatusBadge, + ToolExecutionDrawer, +} from "@agenta/entity-ui/gatewayTool" +import {Play, Plus} from "@phosphor-icons/react" +import {Button, Collapse, Empty, Spin, Tag, Tooltip, Typography} from "antd" +import {useSetAtom} from "jotai" +import Image from "next/image" interface GatewayToolsPanelProps { mountDrawers?: boolean @@ -27,7 +28,7 @@ export default function GatewayToolsPanel({mountDrawers = false}: GatewayToolsPa // Group connections by integration const grouped = useMemo(() => { - const map: Record = {} + const map: Record = {} for (const conn of connections) { const key = conn.integration_key if (!map[key]) map[key] = [] @@ -82,15 +83,16 @@ export default function GatewayToolsPanel({mountDrawers = false}: GatewayToolsPa
{grouped[integrationKey].map((conn) => ( + onTest={() => { + if (!conn.id || !conn.slug) return setExecutionDrawer({ connectionId: conn.id, connectionSlug: conn.slug, integrationKey: conn.integration_key, }) - } + }} /> ))}
@@ -132,7 +134,7 @@ function IntegrationSectionLabel({integrationKey}: {integrationKey: string}) { ) } -function ConnectionRow({connection, onTest}: {connection: ConnectionItem; onTest: () => void}) { +function ConnectionRow({connection, onTest}: {connection: ToolConnection; onTest: () => void}) { const isReady = connection.flags?.is_active && connection.flags?.is_valid const {integration} = useIntegrationDetail(connection.integration_key) const label = integration?.name || connection.integration_key.replace(/_/g, " ") diff --git a/web/oss/src/components/Playground/Playground.tsx b/web/oss/src/components/Playground/Playground.tsx index 967b7e77b2..23aecbc302 100644 --- a/web/oss/src/components/Playground/Playground.tsx +++ b/web/oss/src/components/Playground/Playground.tsx @@ -1,7 +1,9 @@ import {type FC, useCallback, useEffect, useMemo} from "react" +import {executeToolCall} from "@agenta/entities/gatewayTool" import {loadableController} from "@agenta/entities/loadable" import {testcaseMolecule} from "@agenta/entities/testcase" +import {CatalogDrawer} from "@agenta/entity-ui/gatewayTool" import {GatewayToolAssistantActions, type PlaygroundUIProviders} from "@agenta/playground-ui" import {useLocalDraftWarning} from "@agenta/playground-ui/hooks" import {preloadEditorPlugins, SyncStateTag} from "@agenta/ui" @@ -9,8 +11,6 @@ import {useAtomValue, useSetAtom} from "jotai" import SimpleSharedEditor from "@/oss/components/EditorViews/SimpleSharedEditor" import SharedGenerationResultUtils from "@/oss/components/SharedGenerationResultUtils" -import CatalogDrawer from "@/oss/features/gateway-tools/drawers/CatalogDrawer" -import {executeToolCall} from "@/oss/services/tools/api" import {playgroundSyncAtom} from "@/oss/state/url/playground" import PlaygroundMainView from "./Components/MainLayout" diff --git a/web/oss/src/components/Sidebar/hooks/useDropdownItems/types.d.ts b/web/oss/src/components/Sidebar/hooks/useDropdownItems/types.d.ts index 452a8675c3..7c3b1688c8 100644 --- a/web/oss/src/components/Sidebar/hooks/useDropdownItems/types.d.ts +++ b/web/oss/src/components/Sidebar/hooks/useDropdownItems/types.d.ts @@ -1,4 +1,6 @@ -import {Org, OrgDetails, User} from "@/oss/lib/Types" +import type {User} from "@agenta/shared/types" + +import {Org, OrgDetails} from "@/oss/lib/Types" import {ProjectsResponse} from "@/oss/services/project/types" export interface UseDropdownItemsProps { diff --git a/web/oss/src/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig.tsx b/web/oss/src/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig.tsx index 54096a71a5..b01b070a25 100644 --- a/web/oss/src/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig.tsx +++ b/web/oss/src/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig.tsx @@ -1,12 +1,12 @@ import {useCallback} from "react" +import {useVaultSecret} from "@agenta/entities/secret" +import type {LlmProvider} from "@agenta/shared/types" import {removeTrailingSlash} from "@agenta/shared/utils" import {useQueryClient} from "@tanstack/react-query" import {useSetAtom, useStore} from "jotai" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {usePostHogAg} from "@/oss/lib/helpers/analytics/hooks/usePostHogAg" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" import {isDemo} from "@/oss/lib/helpers/utils" import {createAppWithTemplate, ServiceType} from "@/oss/services/app-selector/api" import {useAppsData} from "@/oss/state/app" diff --git a/web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalInner.tsx b/web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalInner.tsx index b43a376fba..2b4e35f316 100644 --- a/web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalInner.tsx +++ b/web/oss/src/components/pages/evaluations/NewEvaluation/Components/NewEvaluationModalInner.tsx @@ -1,5 +1,6 @@ import {useCallback, memo, useEffect, useLayoutEffect, useMemo, useRef, useState} from "react" +import {useVaultSecret} from "@agenta/entities/secret" import {extractSourceIdFromDraft, isLocalDraftId, isValidUUID} from "@agenta/entities/shared" import { workflowMolecule, @@ -29,7 +30,6 @@ import { import {FIRST_EVALUATION_TOUR_ID} from "@/oss/components/Onboarding/tours/firstEvaluationTour" import {registryWorkflowIdOverrideAtom} from "@/oss/components/VariantsComponents/store/registryStore" import useURL from "@/oss/hooks/useURL" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {resolveEvaluatorKey} from "@/oss/lib/evaluators/utils" import {redirectIfNoLLMKeys} from "@/oss/lib/helpers/utils" import usePreviewEvaluations from "@/oss/lib/hooks/usePreviewEvaluations" diff --git a/web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx b/web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx index fa3436c42d..07fbc1422d 100644 --- a/web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx +++ b/web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx @@ -1,5 +1,7 @@ import {useMemo, useState} from "react" +import {useVaultSecret} from "@agenta/entities/secret" +import type {LlmProvider} from "@agenta/shared/types" import {LLMIconMap} from "@agenta/ui" import {GearSix, PencilSimpleLine, Plus, Trash} from "@phosphor-icons/react" import {Button, Table, Tag, Typography} from "antd" @@ -8,9 +10,7 @@ import {ColumnsType} from "antd/es/table" import ConfigureProviderDrawer from "@/oss/components/ModelRegistry/Drawers/ConfigureProviderDrawer" import ConfigureProviderModal from "@/oss/components/ModelRegistry/Modals/ConfigureProviderModal" import DeleteProviderModal from "@/oss/components/ModelRegistry/Modals/DeleteProviderModal" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {formatDay} from "@/oss/lib/helpers/dateTimeHelper" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" const SecretProviderTable = ({type}: {type: "standard" | "custom"}) => { const {customRowSecrets, secrets, loading} = useVaultSecret() diff --git a/web/oss/src/components/pages/settings/Tools/components/ActionsList.tsx b/web/oss/src/components/pages/settings/Tools/components/ActionsList.tsx index cf9c9157e3..8355adf38d 100644 --- a/web/oss/src/components/pages/settings/Tools/components/ActionsList.tsx +++ b/web/oss/src/components/pages/settings/Tools/components/ActionsList.tsx @@ -1,16 +1,15 @@ import {useMemo} from "react" +import type {ToolCatalogAction} from "@agenta/entities/gatewayTool" import {Table, Tag, Typography} from "antd" import type {ColumnsType} from "antd/es/table" -import type {ActionItem} from "@/oss/services/tools/api/types" - interface Props { - actions: ActionItem[] + actions: ToolCatalogAction[] } export default function ActionsList({actions}: Props) { - const columns: ColumnsType = useMemo( + const columns: ColumnsType = useMemo( () => [ { title: "Name", @@ -43,7 +42,7 @@ export default function ActionsList({actions}: Props) { ) return ( - + dataSource={actions} columns={columns} rowKey="key" diff --git a/web/oss/src/components/pages/settings/Tools/components/ConnectionsList.tsx b/web/oss/src/components/pages/settings/Tools/components/ConnectionsList.tsx index 3117067640..4943cd4b1f 100644 --- a/web/oss/src/components/pages/settings/Tools/components/ConnectionsList.tsx +++ b/web/oss/src/components/pages/settings/Tools/components/ConnectionsList.tsx @@ -1,23 +1,22 @@ import {useMemo} from "react" +import type {ToolConnection} from "@agenta/entities/gatewayTool" +import {ConnectionStatusBadge} from "@agenta/entity-ui/gatewayTool" import {ArrowClockwise, Trash} from "@phosphor-icons/react" import {Button, Table, Tooltip, Typography} from "antd" import type {ColumnsType} from "antd/es/table" import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" import {formatDay} from "@/oss/lib/helpers/dateTimeHelper" -import type {ConnectionItem} from "@/oss/services/tools/api/types" import {useToolsConnections} from "../hooks/useToolsConnections" -import ConnectionStatusBadge from "./ConnectionStatusBadge" - interface Props { integrationKey: string - connections: ConnectionItem[] + connections: ToolConnection[] } -const getRedirectUrl = (connection: ConnectionItem | null | undefined): string | undefined => { +const getRedirectUrl = (connection: ToolConnection | null | undefined): string | undefined => { if (!connection) return undefined const dataRedirect = connection.data?.redirect_url return typeof dataRedirect === "string" && dataRedirect ? dataRedirect : undefined @@ -26,7 +25,7 @@ const getRedirectUrl = (connection: ConnectionItem | null | undefined): string | export default function ConnectionsList({integrationKey, connections}: Props) { const {handleDelete, handleRefresh, invalidate} = useToolsConnections(integrationKey) - const confirmDelete = (connection: ConnectionItem) => { + const confirmDelete = (connection: ToolConnection) => { if (!connection.id) return AlertPopup({ title: "Delete Connection", @@ -36,7 +35,7 @@ export default function ConnectionsList({integrationKey, connections}: Props) { }) } - const onRefresh = async (connection: ConnectionItem) => { + const onRefresh = async (connection: ToolConnection) => { if (!connection.id) return const result = await handleRefresh(connection.id) @@ -61,7 +60,7 @@ export default function ConnectionsList({integrationKey, connections}: Props) { }, 1000) } - const columns: ColumnsType = useMemo( + const columns: ColumnsType = useMemo( () => [ { title: "Name", @@ -121,7 +120,7 @@ export default function ConnectionsList({integrationKey, connections}: Props) { ) return ( - + dataSource={connections} columns={columns} rowKey="slug" diff --git a/web/oss/src/components/pages/settings/Tools/components/GatewayToolsSection.tsx b/web/oss/src/components/pages/settings/Tools/components/GatewayToolsSection.tsx index 58ff4546d4..618a983b8d 100644 --- a/web/oss/src/components/pages/settings/Tools/components/GatewayToolsSection.tsx +++ b/web/oss/src/components/pages/settings/Tools/components/GatewayToolsSection.tsx @@ -1,5 +1,18 @@ import {useCallback, useMemo, useState} from "react" +import { + useConnectionsQuery, + useConnectionActions, + catalogDrawerOpenAtom, + executionDrawerAtom, + fetchConnection, + type ToolConnection, +} from "@agenta/entities/gatewayTool" +import { + CatalogDrawer, + ConnectionStatusBadge, + ToolExecutionDrawer, +} from "@agenta/entity-ui/gatewayTool" import {MoreOutlined} from "@ant-design/icons" import {ArrowClockwise, Play, Plus, Trash, XCircle} from "@phosphor-icons/react" import {Button, Dropdown, message, Table, Tag, Tooltip, Typography} from "antd" @@ -7,20 +20,8 @@ import type {ColumnsType} from "antd/es/table" import {useSetAtom} from "jotai" import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" -import { - useConnectionsQuery, - useConnectionActions, - catalogDrawerOpenAtom, - executionDrawerAtom, -} from "@/oss/features/gateway-tools" -import CatalogDrawer from "@/oss/features/gateway-tools/drawers/CatalogDrawer" -import ToolExecutionDrawer from "@/oss/features/gateway-tools/drawers/ToolExecutionDrawer" import {getAgentaApiUrl, getAgentaWebUrl} from "@/oss/lib/helpers/api" import {formatDay} from "@/oss/lib/helpers/dateTimeHelper" -import {fetchConnection} from "@/oss/services/tools/api" -import type {ConnectionItem} from "@/oss/services/tools/api/types" - -import ConnectionStatusBadge from "./ConnectionStatusBadge" export default function GatewayToolsSection() { const {connections, isLoading, refetch} = useConnectionsQuery() @@ -34,7 +35,12 @@ export default function GatewayToolsSection() { setReloading(true) try { // Poll each connection individually to trigger Composio status sync - await Promise.allSettled(connections.map((c) => fetchConnection(c.id))) + await Promise.allSettled( + connections + .map((c) => c.id) + .filter((id): id is string => typeof id === "string") + .map((id) => fetchConnection(id)), + ) invalidateConnections() } finally { setReloading(false) @@ -42,7 +48,8 @@ export default function GatewayToolsSection() { }, [connections, invalidateConnections]) const openExecution = useCallback( - (record: ConnectionItem) => { + (record: ToolConnection) => { + if (!record.id || !record.slug) return setExecutionDrawer({ connectionId: record.id, connectionSlug: record.slug, @@ -53,9 +60,11 @@ export default function GatewayToolsSection() { ) const onRefresh = useCallback( - async (connection: ConnectionItem) => { + async (connection: ToolConnection) => { + if (!connection.id) return + const connectionId = connection.id try { - const result = await handleRefresh(connection.id) + const result = await handleRefresh(connectionId) const redirectUrl = (result.connection?.data as Record | undefined) ?.redirect_url @@ -73,7 +82,7 @@ export default function GatewayToolsSection() { // Poll the individual connection endpoint which checks // Composio for status and updates is_valid in the DB. try { - await fetchConnection(connection.id) + await fetchConnection(connectionId) } catch { /* best-effort */ } @@ -121,12 +130,13 @@ export default function GatewayToolsSection() { ) const confirmDelete = useCallback( - (connection: ConnectionItem) => { + (connection: ToolConnection) => { AlertPopup({ title: "Delete Connection", message: "Are you sure you want to delete this connection? This action is irreversible.", onOk: async () => { + if (!connection.id) return try { await handleDelete(connection.id) message.success("Connection deleted") @@ -140,12 +150,13 @@ export default function GatewayToolsSection() { ) const confirmRevoke = useCallback( - (connection: ConnectionItem) => { + (connection: ToolConnection) => { AlertPopup({ title: "Revoke Connection", message: "This will mark the connection as invalid. You can refresh it later to reactivate.", onOk: async () => { + if (!connection.id) return try { await handleRevoke(connection.id) message.success("Connection revoked") @@ -158,7 +169,7 @@ export default function GatewayToolsSection() { [handleRevoke], ) - const columns: ColumnsType = useMemo( + const columns: ColumnsType = useMemo( () => [ { title: "Integration", @@ -305,7 +316,7 @@ export default function GatewayToolsSection() { - + className="ph-no-capture" columns={columns} dataSource={connections} diff --git a/web/oss/src/components/pages/settings/Tools/components/IntegrationGrid.tsx b/web/oss/src/components/pages/settings/Tools/components/IntegrationGrid.tsx index 0b99449087..28fed6e1df 100644 --- a/web/oss/src/components/pages/settings/Tools/components/IntegrationGrid.tsx +++ b/web/oss/src/components/pages/settings/Tools/components/IntegrationGrid.tsx @@ -1,11 +1,10 @@ import {useState, useMemo} from "react" +import type {ToolCatalogIntegration} from "@agenta/entities/gatewayTool" import {MagnifyingGlass} from "@phosphor-icons/react" import {Card, Empty, Input, Spin, Typography} from "antd" import Image from "next/image" -import type {IntegrationItem} from "@/oss/services/tools/api/types" - import {useToolsIntegrations} from "../hooks/useToolsIntegrations" interface Props { @@ -23,7 +22,7 @@ export default function IntegrationGrid({onSelect}: Props) { (i) => i.name.toLowerCase().includes(q) || i.description?.toLowerCase().includes(q) || - i.categories.some((c) => c.toLowerCase().includes(q)), + (i.categories ?? []).some((c) => c.toLowerCase().includes(q)), ) }, [integrations, search]) @@ -67,7 +66,7 @@ function IntegrationCard({ integration, onClick, }: { - integration: IntegrationItem + integration: ToolCatalogIntegration onClick: () => void }) { return ( diff --git a/web/oss/src/components/pages/settings/Tools/hooks/useIntegrationDetail.ts b/web/oss/src/components/pages/settings/Tools/hooks/useIntegrationDetail.ts index ad61c36002..f9d6a096b0 100644 --- a/web/oss/src/components/pages/settings/Tools/hooks/useIntegrationDetail.ts +++ b/web/oss/src/components/pages/settings/Tools/hooks/useIntegrationDetail.ts @@ -1,15 +1,18 @@ +import { + fetchActions, + integrationDetailQueryFamily, + queryConnections, + type ToolCatalogActionsResponse, + type ToolConnectionsResponse, +} from "@agenta/entities/gatewayTool" import {useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" -import {integrationDetailQueryFamily} from "@/oss/features/gateway-tools/hooks/useIntegrationDetail" -import {fetchActions, queryConnections} from "@/oss/services/tools/api" -import type {ActionsListResponse, ConnectionsQueryResponse} from "@/oss/services/tools/api/types" - const DEFAULT_PROVIDER = "composio" export const integrationActionsQueryFamily = atomFamily((integrationKey: string) => - atomWithQuery(() => ({ + atomWithQuery(() => ({ queryKey: ["tools", "actions", DEFAULT_PROVIDER, integrationKey], queryFn: () => fetchActions(DEFAULT_PROVIDER, integrationKey, {important: true}), staleTime: 5 * 60_000, @@ -19,7 +22,7 @@ export const integrationActionsQueryFamily = atomFamily((integrationKey: string) ) export const integrationConnectionsQueryFamily = atomFamily((integrationKey: string) => - atomWithQuery(() => ({ + atomWithQuery(() => ({ queryKey: ["tools", "integrationConnections", DEFAULT_PROVIDER, integrationKey], queryFn: () => queryConnections({ diff --git a/web/oss/src/components/pages/settings/Tools/hooks/useToolsConnections.ts b/web/oss/src/components/pages/settings/Tools/hooks/useToolsConnections.ts index c6810cebfa..6f6d4d75e4 100644 --- a/web/oss/src/components/pages/settings/Tools/hooks/useToolsConnections.ts +++ b/web/oss/src/components/pages/settings/Tools/hooks/useToolsConnections.ts @@ -1,12 +1,12 @@ import {useCallback} from "react" -import {queryClient} from "@/oss/lib/api/queryClient" import { createConnection, deleteToolConnection, refreshToolConnection, -} from "@/oss/services/tools/api" -import type {ConnectionCreateRequest} from "@/oss/services/tools/api/types" + type ToolConnectionCreatePayload, +} from "@agenta/entities/gatewayTool" +import {queryClient} from "@agenta/shared/api" const DEFAULT_PROVIDER = "composio" @@ -36,7 +36,7 @@ export const useToolsConnections = (integrationKey: string) => { const handleCreate = useCallback( async (payload: CreateConnectionInput) => { - const request: ConnectionCreateRequest = { + const request: ToolConnectionCreatePayload = { connection: { slug: payload.slug, name: payload.name, diff --git a/web/oss/src/components/pages/settings/Tools/hooks/useToolsIntegrations.ts b/web/oss/src/components/pages/settings/Tools/hooks/useToolsIntegrations.ts index 9d5b027aea..5a00133347 100644 --- a/web/oss/src/components/pages/settings/Tools/hooks/useToolsIntegrations.ts +++ b/web/oss/src/components/pages/settings/Tools/hooks/useToolsIntegrations.ts @@ -1,12 +1,17 @@ +import { + fetchIntegrations, + type ToolCatalogIntegration, + type ToolCatalogIntegrationDetails, + type ToolCatalogIntegrationsResponse, +} from "@agenta/entities/gatewayTool" import {useAtomValue} from "jotai" import {atomWithQuery} from "jotai-tanstack-query" -import {fetchIntegrations} from "@/oss/services/tools/api" -import type {IntegrationItem, IntegrationsResponse} from "@/oss/services/tools/api/types" - const DEFAULT_PROVIDER = "composio" -export const integrationsQueryAtom = atomWithQuery(() => ({ +type CatalogIntegrationItem = ToolCatalogIntegration | ToolCatalogIntegrationDetails + +export const integrationsQueryAtom = atomWithQuery(() => ({ queryKey: ["tools", "integrations", DEFAULT_PROVIDER], queryFn: () => fetchIntegrations(DEFAULT_PROVIDER), staleTime: 5 * 60_000, @@ -15,7 +20,7 @@ export const integrationsQueryAtom = atomWithQuery(() => ( export const useToolsIntegrations = () => { const query = useAtomValue(integrationsQueryAtom) - const integrations: IntegrationItem[] = query.data?.integrations ?? [] + const integrations: CatalogIntegrationItem[] = query.data?.integrations ?? [] return { integrations, diff --git a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx index ebc16037ca..d689d57135 100644 --- a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx +++ b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx @@ -1,5 +1,6 @@ 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" @@ -10,7 +11,6 @@ import {useWorkspacePermissions} from "@/oss/hooks/useWorkspacePermissions" import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE" import {useEntitlements} from "@/oss/lib/helpers/useEntitlements" import {snakeToTitle} from "@/oss/lib/helpers/utils" -import {User} from "@/oss/lib/Types" import {WorkspaceMember} from "@/oss/lib/Types" import {updateUsername} from "@/oss/services/profile" import { diff --git a/web/oss/src/features/gateway-tools/components/ScrollSentinel.tsx b/web/oss/src/features/gateway-tools/components/ScrollSentinel.tsx deleted file mode 100644 index 7ec2335895..0000000000 --- a/web/oss/src/features/gateway-tools/components/ScrollSentinel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import {useEffect, useRef} from "react" - -interface ScrollSentinelProps { - onVisible: () => void - hasMore: boolean - isFetching: boolean -} - -export default function ScrollSentinel({onVisible, hasMore, isFetching}: ScrollSentinelProps) { - const ref = useRef(null) - - useEffect(() => { - const el = ref.current - if (!el || !hasMore) return - - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && !isFetching) { - onVisible() - } - }, - {rootMargin: "200px"}, - ) - observer.observe(el) - return () => observer.disconnect() - }, [onVisible, hasMore, isFetching]) - - if (!hasMore) return null - - return
-} diff --git a/web/oss/src/features/gateway-tools/components/ScrollToTopButton.tsx b/web/oss/src/features/gateway-tools/components/ScrollToTopButton.tsx deleted file mode 100644 index 455c1a6522..0000000000 --- a/web/oss/src/features/gateway-tools/components/ScrollToTopButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import {type RefObject, useEffect, useState} from "react" - -import {ArrowUp} from "@phosphor-icons/react" -import {Button} from "antd" - -interface ScrollToTopButtonProps { - scrollRef: RefObject -} - -export default function ScrollToTopButton({scrollRef}: ScrollToTopButtonProps) { - const [visible, setVisible] = useState(false) - - useEffect(() => { - const el = scrollRef.current - if (!el) return - - const onScroll = () => { - setVisible(el.scrollTop > 300) - } - el.addEventListener("scroll", onScroll, {passive: true}) - return () => el.removeEventListener("scroll", onScroll) - }, [scrollRef]) - - if (!visible) return null - - return ( -
-
- ) -} diff --git a/web/oss/src/features/gateway-tools/index.ts b/web/oss/src/features/gateway-tools/index.ts deleted file mode 100644 index ad5858da6a..0000000000 --- a/web/oss/src/features/gateway-tools/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {buildGatewayToolSlug, isGatewayToolSlug, parseGatewayToolSlug} from "@agenta/shared/utils" - -// State -export { - actionSearchAtom, - catalogDrawerOpenAtom, - catalogSearchAtom, - executionDrawerAtom, - selectedCatalogActionAtom, - selectedCatalogIntegrationAtom, -} from "./state/atoms" -export type {ExecutionDrawerState} from "./state/atoms" - -// Hooks -export {buildGatewayToolSlug, isGatewayToolSlug, parseGatewayToolSlug} -export {useActionDetail} from "./hooks/useActionDetail" -export {useCatalogActions} from "./hooks/useCatalogActions" -export {useCatalogIntegrations} from "./hooks/useCatalogIntegrations" -export {useConnectionActions} from "./hooks/useConnectionActions" -export {useConnectionQuery} from "./hooks/useConnectionQuery" -export {useConnectionsQuery} from "./hooks/useConnectionsQuery" -export {useIntegrationDetail} from "./hooks/useIntegrationDetail" -export {buildToolSlug, useToolExecution} from "./hooks/useToolExecution" - -export {removePromptToolByNameAtomFamily} from "./prompt/atoms" diff --git a/web/oss/src/features/gateway-tools/utils/slugify.ts b/web/oss/src/features/gateway-tools/utils/slugify.ts deleted file mode 100644 index 6da3a0354d..0000000000 --- a/web/oss/src/features/gateway-tools/utils/slugify.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Slugify a string for use as a connection slug. - * - * Rules: - * - Lowercase - * - Replace spaces and underscores with hyphens - * - Strip any character that is not [a-z0-9-] - * - Collapse consecutive hyphens to one - * - Trim leading/trailing hyphens - */ -export function slugify(text: string): string { - return text - .toLowerCase() - .replace(/[\s_]+/g, "-") - .replace(/[^a-z0-9-]/g, "") - .replace(/-{2,}/g, "-") - .replace(/^-+|-+$/g, "") -} - -/** - * Generate a random alphanumeric string of length `n` (lowercase). - */ -export function randomAlphanumeric(n: number): string { - const chars = "abcdefghijklmnopqrstuvwxyz0123456789" - let result = "" - for (let i = 0; i < n; i++) { - result += chars[Math.floor(Math.random() * chars.length)] - } - return result -} - -/** - * Generate a default connection slug from a display name. - * - * Format: `slugify(name)-<3-char random suffix>` - * Example: "Google Calendar" → "google-calendar-7mx" - */ -export function generateDefaultSlug(name: string, suffix = randomAlphanumeric(3)): string { - const base = slugify(name) - return base ? `${base}-${suffix}` : suffix -} diff --git a/web/oss/src/hooks/useLLMProviderConfig.tsx b/web/oss/src/hooks/useLLMProviderConfig.tsx index 2a11801741..cd2af07d2d 100644 --- a/web/oss/src/hooks/useLLMProviderConfig.tsx +++ b/web/oss/src/hooks/useLLMProviderConfig.tsx @@ -1,12 +1,12 @@ import {useMemo, useState} from "react" +import {useVaultSecret} from "@agenta/entities/secret" import {Anthropic, Gemini, Mistral, OpenAi, Together} from "@agenta/ui" import type {ProviderGroup} from "@agenta/ui/select-llm-provider" import {Plus} from "@phosphor-icons/react" import {Button, Divider} from "antd" import ConfigureProviderDrawer from "@/oss/components/ModelRegistry/Drawers/ConfigureProviderDrawer" -import {useVaultSecret} from "@/oss/hooks/useVaultSecret" import {capitalize} from "@/oss/lib/helpers/utils" const icons = [OpenAi, Gemini, Anthropic, Mistral, Together] diff --git a/web/oss/src/hooks/useVaultSecret.ts b/web/oss/src/hooks/useVaultSecret.ts deleted file mode 100644 index a368b412f0..0000000000 --- a/web/oss/src/hooks/useVaultSecret.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export the new atom-based useVaultSecret hook -export {useVaultSecret} from "@/oss/state/app" diff --git a/web/oss/src/lib/Types.ts b/web/oss/src/lib/Types.ts index 9124336bcf..7f2779cd3b 100644 --- a/web/oss/src/lib/Types.ts +++ b/web/oss/src/lib/Types.ts @@ -110,120 +110,11 @@ export interface LanguageItem { languageKey: string } -export interface HeaderDTO { - name?: string | null - description?: string | null -} - -export interface StandardSecret { - kind: SecretDTOProvider - provider: { - key: string - } -} - -export type StandardSecretDTO = { - header: HeaderDTO -} & (T extends "payload" - ? {secret: {data: StandardSecret; kind: SecretDTOKind.PROVIDER_KEY}} - : { - kind: SecretDTOKind.PROVIDER_KEY - data: StandardSecret - id: string - lifecycle: {created_at: string} - }) - -export enum SecretDTOKind { - PROVIDER_KEY = "provider_key", - CUSTOM_PROVIDER_KEY = "custom_provider", -} - -export enum SecretDTOProvider { - OPENAI = "openai", - COHERE = "cohere", - ANYSCALE = "anyscale", - DEEPINFRA = "deepinfra", - ALEPHALPHA = "alephalpha", - GROQ = "groq", - MISTRAL = "mistral", - ANTHROPIC = "anthropic", - PERPLEXITYAI = "perplexityai", - TOGETHERAI = "together_ai", - OPENROUTER = "openrouter", - GEMINI = "gemini", - MINIMAX = "minimax", -} - -export const PROVIDER_LABELS: Record = { - openai: "OpenAI", - cohere: "Cohere", - anyscale: "Anyscale", - deepinfra: "DeepInfra", - alephalpha: "Aleph Alpha", - groq: "Groq", - mistral: "Mistral AI", - mistralai: "Mistral AI", - anthropic: "Anthropic", - perplexityai: "Perplexity AI", - together_ai: "Together AI", - openrouter: "OpenRouter", - gemini: "Google Gemini", - vertex_ai: "Google Vertex AI", - bedrock: "AWS Bedrock", - azure: "Azure OpenAI", - minimax: "MiniMax", - custom: "Custom Provider", -} - -export const PROVIDER_KINDS: Record = { - ...Object.entries(PROVIDER_LABELS).reduce( - (acc, [kind, label]) => { - acc[kind] = kind - acc[label.toLowerCase()] = kind - return acc - }, - {} as Record, - ), - // Normalize legacy "mistralai" slug to canonical "mistral" - mistralai: "mistral", -} - -interface VaultModels { - slug: string -} -interface VaultProvider { - url: string - version: string - extras: { - aws_access_key_id?: string - aws_secret_access_key?: string - aws_session_token?: string - aws_region_name?: string - vertex_ai_project?: string - vertex_ai_location?: string - vertex_ai_credentials?: string - api_key?: string - } -} - -interface VaultData { - kind: string - provider: VaultProvider - models: VaultModels[] - model_keys: string[] - provider_slug: string -} - -export type CustomSecretDTO = { - header: HeaderDTO -} & (T extends "payload" - ? {secret: {kind: SecretDTOKind.CUSTOM_PROVIDER_KEY; data: VaultData}} - : { - kind: SecretDTOKind.CUSTOM_PROVIDER_KEY - data: VaultData - id: string - lifecycle: {created_at: string} - }) +// Secret-domain types (HeaderDTO, StandardSecretDTO, CustomSecretDTO, +// VaultData, SecretDTOKind, SecretDTOProvider, PROVIDER_KINDS, +// PROVIDER_LABELS, etc.) live in @agenta/entities/secret. Import from +// there directly — re-exporting from @agenta packages is disallowed +// (see eslint no-restricted-syntax rule for tree-shaking). export type GenericObject = Record export type KeyValuePair = Record @@ -493,12 +384,9 @@ export interface EvaluationSettingsTemplate { options?: string[] } -export interface User { - id: string - uid: string - username: string - email: string -} +// User type lives in @agenta/shared/types. Import from there directly — +// re-exporting from @agenta packages is disallowed (see eslint +// no-restricted-syntax rule for tree-shaking). // billings export enum Plan { diff --git a/web/oss/src/lib/helpers/llmProviders.ts b/web/oss/src/lib/helpers/llmProviders.ts deleted file mode 100644 index 9ad2a42153..0000000000 --- a/web/oss/src/lib/helpers/llmProviders.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {StandardSecretDTO, CustomSecretDTO, SecretDTOKind, PROVIDER_KINDS} from "../Types" - -export const llmAvailableProvidersToken = "llmAvailableProvidersToken" - -export interface LlmProvider { - title?: string - key?: string - provider?: string - name?: string - apiKey?: string - apiBaseUrl?: string - version?: string - region?: string - vertexProject?: string - vertexLocation?: string - vertexCredentials?: string - accessKeyId?: string - accessKey?: string - sessionToken?: string - models?: string[] - modelKeys?: string[] - id?: string - type?: `${SecretDTOKind}` - created_at?: string -} - -export const transformSecret = (secrets: CustomSecretDTO[] | StandardSecretDTO[]) => { - return secrets.reduce((acc, curr) => { - if (curr.kind == SecretDTOKind.PROVIDER_KEY) { - const secret = curr as StandardSecretDTO - - const name = secret.data.kind - const key = secret.data.provider.key - const provider = secret.data.kind - - const envNameMap: Record = { - openai: "OPENAI_API_KEY", - cohere: "COHERE_API_KEY", - anyscale: "ANYSCALE_API_KEY", - deepinfra: "DEEPINFRA_API_KEY", - alephalpha: "ALEPHALPHA_API_KEY", - groq: "GROQ_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", - minimax: "MINIMAX_API_KEY", - } - - acc.push({ - title: name || "", - key: key, - name: envNameMap[provider] || "", - id: secret.id, - type: secret.kind, - created_at: secret.lifecycle.created_at, - }) - } else if (curr.kind === SecretDTOKind.CUSTOM_PROVIDER_KEY) { - const secret = curr as CustomSecretDTO - acc.push({ - name: secret.header.name || "", - id: secret.id, - type: secret.kind, - provider: secret.data?.kind, - apiKey: secret.data.provider.extras?.api_key || "", - apiBaseUrl: secret.data.provider.url || "", - region: secret.data.provider.extras?.aws_region_name || "", - vertexProject: secret.data.provider.extras?.vertex_ai_project || "", - vertexLocation: secret.data.provider.extras?.vertex_ai_location || "", - vertexCredentials: secret.data.provider.extras?.vertex_ai_credentials || "", - accessKeyId: secret.data.provider.extras?.aws_access_key_id || "", - accessKey: secret.data.provider.extras?.aws_secret_access_key || "", - sessionToken: secret.data.provider.extras?.aws_session_token || "", - models: secret?.data.models.map((model) => model.slug), - modelKeys: secret?.data.model_keys, - version: secret.data.provider?.version || "", - created_at: secret.lifecycle?.created_at || "", - }) - } - return acc - }, [] as LlmProvider[]) -} - -export const llmAvailableProviders: LlmProvider[] = [ - {title: "OpenAI", key: "", name: "OPENAI_API_KEY"}, - {title: "Mistral AI", key: "", name: "MISTRAL_API_KEY"}, - {title: "Cohere", key: "", name: "COHERE_API_KEY"}, - {title: "Anthropic", key: "", name: "ANTHROPIC_API_KEY"}, - {title: "Anyscale", key: "", name: "ANYSCALE_API_KEY"}, - {title: "Perplexity AI", key: "", name: "PERPLEXITYAI_API_KEY"}, - {title: "DeepInfra", key: "", name: "DEEPINFRA_API_KEY"}, - {title: "Together AI", key: "", name: "TOGETHERAI_API_KEY"}, - {title: "Aleph Alpha", key: "", name: "ALEPHALPHA_API_KEY"}, - {title: "OpenRouter", key: "", name: "OPENROUTER_API_KEY"}, - {title: "Groq", key: "", name: "GROQ_API_KEY"}, - {title: "Google Gemini", key: "", name: "GEMINI_API_KEY"}, - {title: "MiniMax", key: "", name: "MINIMAX_API_KEY"}, -] - -export const transformCustomProviderPayloadData = (values: LlmProvider) => { - const providerInput = values.provider?.trim() ?? "" - const providerKind = providerInput - ? (PROVIDER_KINDS[providerInput] ?? - PROVIDER_KINDS[providerInput.toLowerCase()] ?? - providerInput.toLowerCase()) - : "" - - return { - header: { - name: values.name, - description: values.name, - }, - secret: { - kind: SecretDTOKind.CUSTOM_PROVIDER_KEY, - data: { - kind: providerKind, - provider: { - url: values.apiBaseUrl, - version: values.version, - extras: { - api_key: values.apiKey, - vertex_ai_location: values.vertexLocation, - vertex_ai_project: values.vertexProject, - vertex_ai_credentials: values.vertexCredentials, - aws_region_name: values.region, - aws_access_key_id: values.accessKeyId, - aws_secret_access_key: values.accessKey, - aws_session_token: values.sessionToken, - }, - }, - models: values.models?.map((slug) => ({slug})), - }, - }, - } as CustomSecretDTO<"payload"> -} diff --git a/web/oss/src/lib/helpers/utils.ts b/web/oss/src/lib/helpers/utils.ts index 3463974dc4..b9d7fd0e1c 100644 --- a/web/oss/src/lib/helpers/utils.ts +++ b/web/oss/src/lib/helpers/utils.ts @@ -1,4 +1,11 @@ -import {dataUriToObjectUrl, isBase64, isUrl, safeJson5Parse} from "@agenta/shared/utils" +import type {LlmProvider} from "@agenta/shared/types" +import { + dataUriToObjectUrl, + isBase64, + isUrl, + removeEmptyFromObjects as sharedRemoveEmptyFromObjects, + safeJson5Parse, +} from "@agenta/shared/utils" import {notification} from "antd" import yaml from "js-yaml" import JSON5 from "json5" @@ -6,7 +13,6 @@ import Router from "next/router" import {v4 as uuidv4} from "uuid" import dayjs from "@/oss/lib/helpers/dateTimeHelper/dayjs" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" import {waitForValidURL} from "@/oss/state/url" import {GenericObject} from "../Types" @@ -217,26 +223,9 @@ export const formatVariantIdWithHash = (variantId: string) => { export const getUsernameFromEmail = (email: string) => email.split("@")[0] -export const removeEmptyFromObjects = (obj: any): any => { - if (Array.isArray(obj)) { - return obj - .map(removeEmptyFromObjects) - .filter((item) => item && (typeof item !== "object" || Object.keys(item).length)) - } - if (obj && typeof obj === "object") { - return Object.entries(obj).reduce( - (acc, [key, value]) => { - const cleaned = removeEmptyFromObjects(value) - if (cleaned !== null && cleaned !== undefined && cleaned !== "") { - acc[key] = cleaned - } - return acc - }, - {} as Record, - ) - } - return obj -} +// Canonical implementation lives in @agenta/shared/utils. +// Re-exported here to keep the existing @/oss/lib/helpers/utils import path working. +export const removeEmptyFromObjects = sharedRemoveEmptyFromObjects export const isUuid = (id: string) => { // Check for full UUID format (8-4-4-4-12) diff --git a/web/oss/src/services/app-selector/api/index.ts b/web/oss/src/services/app-selector/api/index.ts index 60839f3e90..cf9a54ec8b 100644 --- a/web/oss/src/services/app-selector/api/index.ts +++ b/web/oss/src/services/app-selector/api/index.ts @@ -4,13 +4,13 @@ import { type CreateAppFromTemplateResult, seedCreatedWorkflowCache, } from "@agenta/entities/workflow" +import type {LlmProvider} from "@agenta/shared/types" import {getDefaultStore} from "jotai" import Router from "next/router" import axios from "@/oss/lib/api/assets/axiosConfig" import {fetchJson} from "@/oss/lib/api/assets/fetchClient" import {getAgentaApiUrl} from "@/oss/lib/helpers/api" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" import {buildRevisionsQueryParam} from "@/oss/lib/helpers/url" import {recentAppIdAtom} from "@/oss/state/app" import {getOrgValues} from "@/oss/state/org" diff --git a/web/oss/src/services/tools/api/index.ts b/web/oss/src/services/tools/api/index.ts deleted file mode 100644 index 278ddf2a3c..0000000000 --- a/web/oss/src/services/tools/api/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import axios from "@/oss/lib/api/assets/axiosConfig" -import {getAgentaApiUrl} from "@/oss/lib/helpers/api" - -import type { - ProvidersResponse, - IntegrationsResponse, - IntegrationDetailResponse, - ActionsListResponse, - ActionDetailResponse, - ConnectionCreateRequest, - ConnectionResponse, - ConnectionsQueryResponse, - ToolCallRequest, - ToolCallResponse, -} from "./types" - -// Prefix convention: -// - fetch: GET single/list entity from server -// - create: POST data to server -// - delete: DELETE data from server -// - query: POST query with filters - -const BASE = () => `${getAgentaApiUrl()}/tools` - -// --- Catalog browse --- - -export const fetchProviders = async (): Promise => { - const {data} = await axios.get(`${BASE()}/catalog/providers/`) - return data -} - -export const fetchIntegrations = async ( - providerKey: string, - params?: {search?: string; sort_by?: string; limit?: number; cursor?: string}, -): Promise => { - const {data} = await axios.get(`${BASE()}/catalog/providers/${providerKey}/integrations/`, { - params, - }) - return data -} - -export const fetchIntegrationDetail = async ( - providerKey: string, - integrationKey: string, -): Promise => { - const {data} = await axios.get( - `${BASE()}/catalog/providers/${providerKey}/integrations/${integrationKey}`, - ) - return data -} - -export const fetchActions = async ( - providerKey: string, - integrationKey: string, - params?: { - query?: string - categories?: string[] - limit?: number - cursor?: string - important?: boolean - }, -): Promise => { - const {data} = await axios.get( - `${BASE()}/catalog/providers/${providerKey}/integrations/${integrationKey}/actions/`, - {params}, - ) - return data -} - -export const fetchActionDetail = async ( - providerKey: string, - integrationKey: string, - actionKey: string, -): Promise => { - const {data} = await axios.get( - `${BASE()}/catalog/providers/${providerKey}/integrations/${integrationKey}/actions/${actionKey}`, - ) - return data -} - -// --- Connections --- - -export const queryConnections = async (params?: { - provider_key?: string - integration_key?: string -}): Promise => { - const {data} = await axios.post(`${BASE()}/connections/query`, null, {params}) - return data -} - -export const fetchConnection = async (connectionId: string): Promise => { - const {data} = await axios.get(`${BASE()}/connections/${connectionId}`) - return data -} - -export const createConnection = async ( - payload: ConnectionCreateRequest, -): Promise => { - const {data} = await axios.post(`${BASE()}/connections/`, payload) - return data -} - -export const deleteToolConnection = async (connectionId: string): Promise => { - await axios.delete(`${BASE()}/connections/${connectionId}`) -} - -export const refreshToolConnection = async ( - connectionId: string, - force?: boolean, -): Promise => { - const {data} = await axios.post(`${BASE()}/connections/${connectionId}/refresh`, null, { - params: force ? {force: true} : undefined, - }) - return data -} - -export const revokeToolConnection = async (connectionId: string): Promise => { - const {data} = await axios.post(`${BASE()}/connections/${connectionId}/revoke`) - return data -} - -// --- Tool execution --- - -export const executeToolCall = async (payload: ToolCallRequest): Promise => { - const {data} = await axios.post(`${BASE()}/call`, payload) - return data -} diff --git a/web/oss/src/services/tools/api/types.ts b/web/oss/src/services/tools/api/types.ts deleted file mode 100644 index 79e75d8aaf..0000000000 --- a/web/oss/src/services/tools/api/types.ts +++ /dev/null @@ -1,157 +0,0 @@ -// TypeScript interfaces mirroring api/oss/src/apis/fastapi/tools/models.py - -// --------------------------------------------------------------------------- -// Catalog browse -// --------------------------------------------------------------------------- - -export interface ProviderItem { - key: string - name: string - description?: string - integrations_count?: number -} - -export interface ProvidersResponse { - count: number - providers: ProviderItem[] -} - -export type ToolAuthScheme = "oauth" | "api_key" - -export interface IntegrationItem { - key: string - name: string - description?: string - logo?: string - url?: string - actions_count?: number - categories: string[] - auth_schemes?: ToolAuthScheme[] -} - -export interface IntegrationsResponse { - count: number - total: number - cursor?: string | null - integrations: IntegrationItem[] -} - -export interface IntegrationDetailResponse { - count: number - integration: IntegrationItem | null -} - -export interface ActionItem { - key: string - name: string - description?: string - categories?: string[] - logo?: string -} - -export interface ActionDetailItem extends ActionItem { - schemas?: { - inputs?: Record - outputs?: Record - } - scopes?: string[] -} - -export interface ActionsListResponse { - count: number - total: number - cursor?: string | null - actions: ActionItem[] -} - -export interface ActionDetailResponse { - count: number - action: ActionDetailItem | null -} - -// --------------------------------------------------------------------------- -// Connections -// --------------------------------------------------------------------------- - -export interface ConnectionItem { - id: string - slug: string - name?: string - description?: string - provider_key: string - integration_key: string - flags?: {is_active?: boolean; is_valid?: boolean} - status?: Record - data?: Record - created_at?: string - updated_at?: string -} - -export interface ConnectionCreateRequest { - connection: { - slug: string - name?: string - description?: string - provider_key: string - integration_key: string - data?: { - auth_scheme?: ToolAuthScheme - credentials?: Record - } - } -} - -export interface ConnectionResponse { - count: number - connection: ConnectionItem | null -} - -export interface ConnectionsQueryResponse { - count: number - connections: ConnectionItem[] -} - -// --------------------------------------------------------------------------- -// Tool execution -// --------------------------------------------------------------------------- - -export interface ToolCallFunction { - name: string // slug: tools__{provider}__{integration}__{action}__{connection} - arguments: string | Record // JSON string (as LLM returns) or parsed dict -} - -export interface ToolCallData { - id: string // LLM call ID (e.g. "call_zEoV...") - type?: string - function: ToolCallFunction -} - -/** Request — wraps the raw OpenAI tool call verbatim. */ -export interface ToolCallRequest { - data: ToolCallData -} - -export interface ToolResultData { - role: string // "tool" - tool_call_id: string // echoed from ToolCallData.id - content: string // execution result as JSON string -} - -export interface Status { - timestamp: string // ISO datetime - type: string // "ok" | "error" - code?: string - message?: string - stacktrace?: string -} - -/** Response — Agenta envelope with identity, status, and the OpenAI tool message. */ -export interface ToolCallResult { - id?: string // Agenta UUID - status?: Status - data?: ToolResultData -} - -export interface ToolCallResponse { - call: ToolCallResult -} diff --git a/web/oss/src/services/vault/api/index.ts b/web/oss/src/services/vault/api/index.ts deleted file mode 100644 index 86a889ff79..0000000000 --- a/web/oss/src/services/vault/api/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import axios from "@/oss/lib/api/assets/axiosConfig" -import {getAgentaApiUrl} from "@/oss/lib/helpers/api" -import {transformSecret} from "@/oss/lib/helpers/llmProviders" -import {CustomSecretDTO, StandardSecretDTO} from "@/oss/lib/Types" - -//Prefix convention: -// - fetch: GET single entity from server -// - fetchAll: GET all entities from server -// - create: POST data to server -// - update: PUT data to server -// - delete: DELETE data from server - -export const fetchVaultSecret = async ({projectId}: {projectId: string}) => { - const response = await axios.get( - `${getAgentaApiUrl()}/vault/v1/secrets/?project_id=${projectId}`, - ) - return transformSecret(response.data as StandardSecretDTO[] | CustomSecretDTO[]) -} - -export const createVaultSecret = async ({ - projectId, - payload, -}: { - projectId: string - payload: T -}) => { - const response = await axios.post( - `${getAgentaApiUrl()}/vault/v1/secrets/?project_id=${projectId}`, - payload, - ) - return response.data as T -} - -export const updateVaultSecret = async ({ - projectId, - secret_id, - payload, -}: { - projectId: string - secret_id: string - payload: T -}) => { - const response = await axios.put( - `${getAgentaApiUrl()}/vault/v1/secrets/${secret_id}?project_id=${projectId}`, - payload, - ) - return response.data as T -} - -export const deleteVaultSecret = async ({ - projectId, - secret_id, -}: { - projectId: string - secret_id: string -}) => { - return await axios.delete( - `${getAgentaApiUrl()}/vault/v1/secrets/${secret_id}?project_id=${projectId}`, - ) -} diff --git a/web/oss/src/state/Providers.tsx b/web/oss/src/state/Providers.tsx index c24495dbaf..58f4a95e7a 100644 --- a/web/oss/src/state/Providers.tsx +++ b/web/oss/src/state/Providers.tsx @@ -17,6 +17,7 @@ import {queryClientAtom} from "jotai-tanstack-query" import WebWorkerProvider from "../components/Playground/Components/WebWorkerProvider" import AgSWRConfig from "../lib/api/SWRConfig" +import UserListener from "./profile/UserListener" import {SessionListener} from "./session" // Initialize the selection system with all entity configs @@ -46,6 +47,7 @@ const GlobalStateProvider = ({children}: PropsWithChildren) => { + {children} diff --git a/web/oss/src/state/app/index.ts b/web/oss/src/state/app/index.ts index dc4b4c3b58..8b01279187 100644 --- a/web/oss/src/state/app/index.ts +++ b/web/oss/src/state/app/index.ts @@ -1,10 +1,8 @@ export * from "./atoms/fetcher" export * from "./atoms/templates" -export * from "./atoms/vault" export * from "./selectors/app" export * from "./hooks" export * from "./hooks/useTemplates" -export * from "./hooks/useVaultSecret" import {invalidateWorkflowsListCache} from "@agenta/entities/workflow" import {getDefaultStore} from "jotai" diff --git a/web/oss/src/state/org/selectors/org.ts b/web/oss/src/state/org/selectors/org.ts index 520aa1ca02..5851aa78f4 100644 --- a/web/oss/src/state/org/selectors/org.ts +++ b/web/oss/src/state/org/selectors/org.ts @@ -1,10 +1,10 @@ import {logAtom} from "@agenta/shared/state" +import type {User} from "@agenta/shared/types" import {atom} from "jotai" import {atomWithQuery} from "jotai-tanstack-query" import {queryClient} from "@/oss/lib/api/queryClient" import {Org, OrgDetails} from "@/oss/lib/Types" -import type {User} from "@/oss/lib/Types" import {fetchAllOrgsList, fetchSingleOrg} from "@/oss/services/organization/api" import {fetchProject} from "@/oss/services/project" import {appIdentifiersAtom, appStateSnapshotAtom, requestNavigationAtom} from "@/oss/state/appState" diff --git a/web/oss/src/state/profile/UserListener.tsx b/web/oss/src/state/profile/UserListener.tsx new file mode 100644 index 0000000000..b609bf5496 --- /dev/null +++ b/web/oss/src/state/profile/UserListener.tsx @@ -0,0 +1,31 @@ +"use client" + +import {useEffect} from "react" + +import {setUserAtom} from "@agenta/shared/state" +import {useAtomValue, useSetAtom} from "jotai" + +import {userAtom} from "./selectors/user" + +/** + * Bootstraps the shared `userAtom` (`@agenta/shared/state`) from the OSS + * profile state. + * + * Pattern mirrors `SessionListener` for `sessionAtom` and the + * `setSharedProjectIdAtom` wiring for `projectIdAtom` — keeping the + * package-level primitive atoms populated by app code so that entity + * packages (`@agenta/entities/secret`, etc.) can read user identity + * without reaching back into OSS state. + */ +const UserListener = () => { + const user = useAtomValue(userAtom) + const setSharedUser = useSetAtom(setUserAtom) + + useEffect(() => { + setSharedUser(user) + }, [user, setSharedUser]) + + return null +} + +export default UserListener diff --git a/web/oss/src/state/profile/selectors/user.ts b/web/oss/src/state/profile/selectors/user.ts index 3442599273..97304da986 100644 --- a/web/oss/src/state/profile/selectors/user.ts +++ b/web/oss/src/state/profile/selectors/user.ts @@ -1,10 +1,10 @@ import {logAtom} from "@agenta/shared/state" +import type {User} from "@agenta/shared/types" import type {AxiosError} from "axios" import {atom} from "jotai" import {atomWithQuery} from "jotai-tanstack-query" import Router from "next/router" -import {User} from "@/oss/lib/Types" import {fetchProfile, getJWT} from "@/oss/services/api" import {sessionExistsAtom} from "../../session" diff --git a/web/oss/src/state/project/selectors/project.ts b/web/oss/src/state/project/selectors/project.ts index d96615fdd5..2773acad71 100644 --- a/web/oss/src/state/project/selectors/project.ts +++ b/web/oss/src/state/project/selectors/project.ts @@ -1,10 +1,10 @@ import {logAtom, projectIdAtom} from "@agenta/shared/state" +import type {User} from "@agenta/shared/types" import {atom} from "jotai" import {atomWithStorage} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" import {queryClient} from "@/oss/lib/api/queryClient" -import {User} from "@/oss/lib/Types" import {fetchAllProjects} from "@/oss/services/project" import {ProjectsResponse} from "@/oss/services/project/types" import {appIdentifiersAtom, appStateSnapshotAtom, requestNavigationAtom} from "@/oss/state/appState" diff --git a/web/packages/agenta-entities/package.json b/web/packages/agenta-entities/package.json index caf5e8ba04..0d4258f735 100644 --- a/web/packages/agenta-entities/package.json +++ b/web/packages/agenta-entities/package.json @@ -32,6 +32,8 @@ "./trace": "./src/trace/index.ts", "./testset": "./src/testset/index.ts", "./testcase": "./src/testcase/index.ts", + "./secret": "./src/secret/index.ts", + "./gatewayTool": "./src/gatewayTool/index.ts", "./environment": "./src/environment/index.ts", "./simpleQueue": "./src/simpleQueue/index.ts", "./evaluationQueue": "./src/evaluationQueue/index.ts", @@ -43,6 +45,7 @@ "./shared/invalidation": "./src/shared/invalidation/index.ts" }, "dependencies": { + "@agentaai/api-client": "workspace:../agenta-api-client", "@agenta/sdk": "workspace:../agenta-sdk", "@agenta/shared": "workspace:../agenta-shared", "@agenta/ui": "workspace:../agenta-ui", diff --git a/web/packages/agenta-entities/src/gatewayTool/api/api.ts b/web/packages/agenta-entities/src/gatewayTool/api/api.ts new file mode 100644 index 0000000000..02459bf94e --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/api/api.ts @@ -0,0 +1,159 @@ +/** + * Gateway-tool API functions. + * + * Thin wrappers over the Fern-generated `tools` resource client that + * preserve the call signatures the existing hooks/UI rely on. Return + * types are the Fern wire shapes verbatim — we no longer maintain a + * parallel set of DTOs. + */ + +import type { + ToolCall, + ToolCallResponse, + ToolCatalogActionResponse, + ToolCatalogActionsResponse, + ToolCatalogIntegrationResponse, + ToolCatalogIntegrationsResponse, + ToolCatalogProvidersResponse, + ToolConnectionCreatePayload, + ToolConnectionResponse, + ToolConnectionsResponse, +} from "../core/types" + +import {getToolsClient, projectScopedRequest} from "./client" + +// --- Catalog browse --- + +export const fetchProviders = async (): Promise => { + return getToolsClient().listToolProviders({}, projectScopedRequest()) +} + +export const fetchIntegrations = async ( + providerKey: string, + params?: {search?: string; sort_by?: string; limit?: number; cursor?: string}, +): Promise => { + return getToolsClient().listToolIntegrations( + { + provider_key: providerKey, + search: params?.search, + sort_by: params?.sort_by, + limit: params?.limit, + cursor: params?.cursor, + }, + projectScopedRequest(), + ) +} + +export const fetchIntegrationDetail = async ( + providerKey: string, + integrationKey: string, +): Promise => { + return getToolsClient().fetchToolIntegration( + {provider_key: providerKey, integration_key: integrationKey}, + projectScopedRequest(), + ) +} + +export const fetchActions = async ( + providerKey: string, + integrationKey: string, + params?: { + query?: string + categories?: string[] + limit?: number + cursor?: string + important?: boolean + }, +): Promise => { + return getToolsClient().listToolActions( + { + provider_key: providerKey, + integration_key: integrationKey, + query: params?.query, + categories: params?.categories, + limit: params?.limit, + cursor: params?.cursor, + }, + projectScopedRequest(), + ) +} + +export const fetchActionDetail = async ( + providerKey: string, + integrationKey: string, + actionKey: string, +): Promise => { + return getToolsClient().fetchToolAction( + { + provider_key: providerKey, + integration_key: integrationKey, + action_key: actionKey, + }, + projectScopedRequest(), + ) +} + +// --- Connections --- + +export const queryConnections = async (params?: { + provider_key?: string + integration_key?: string +}): Promise => { + return getToolsClient().queryToolConnections( + { + provider_key: params?.provider_key, + integration_key: params?.integration_key, + }, + projectScopedRequest(), + ) +} + +export const fetchConnection = async (connectionId: string): Promise => { + return getToolsClient().fetchToolConnection( + {connection_id: connectionId}, + projectScopedRequest(), + ) +} + +export const createConnection = async ( + payload: ToolConnectionCreatePayload, +): Promise => { + // Cast through Parameters<...> because Fern's typed payload doesn't + // model the legacy `credentials` field that the backend still accepts. + return getToolsClient().createToolConnection( + payload as Parameters["createToolConnection"]>[0], + projectScopedRequest(), + ) +} + +export const deleteToolConnection = async (connectionId: string): Promise => { + await getToolsClient().deleteToolConnection( + {connection_id: connectionId}, + projectScopedRequest(), + ) +} + +export const refreshToolConnection = async ( + connectionId: string, + force?: boolean, +): Promise => { + return getToolsClient().refreshToolConnection( + {connection_id: connectionId, force}, + projectScopedRequest(), + ) +} + +export const revokeToolConnection = async ( + connectionId: string, +): Promise => { + return getToolsClient().revokeToolConnection( + {connection_id: connectionId}, + projectScopedRequest(), + ) +} + +// --- Tool execution --- + +export const executeToolCall = async (payload: ToolCall): Promise => { + return getToolsClient().callTool(payload, projectScopedRequest()) +} diff --git a/web/packages/agenta-entities/src/gatewayTool/api/client.ts b/web/packages/agenta-entities/src/gatewayTool/api/client.ts new file mode 100644 index 0000000000..d94cacec25 --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/api/client.ts @@ -0,0 +1,28 @@ +import {getAgentaSdkClient} from "@agenta/sdk" +import {projectIdAtom} from "@agenta/shared/state" +import {getDefaultStore} from "jotai" + +/** + * Resource client for the gateway-tools API endpoints, taken from the + * Fern-generated `@agentaai/api-client` via the workspace SDK singleton. + * + * The host app is responsible for initialising the SDK singleton at boot + * (host, auth). All entities share the same instance through + * `getAgentaSdkClient()`. + */ +export function getToolsClient() { + return getAgentaSdkClient().tools +} + +/** + * Per-request options that scope a Fern call to the current project. + * + * Fern's generated tool requests don't model `project_id` — the legacy axios + * interceptor injected it as a query param via global middleware. We mirror + * that behaviour by reading the shared `projectIdAtom` and emitting it + * through Fern's `BaseRequestOptions.queryParams`. + */ +export function projectScopedRequest() { + const projectId = getDefaultStore().get(projectIdAtom) + return projectId ? {queryParams: {project_id: projectId}} : undefined +} diff --git a/web/packages/agenta-entities/src/gatewayTool/api/index.ts b/web/packages/agenta-entities/src/gatewayTool/api/index.ts new file mode 100644 index 0000000000..6a5a712e2c --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/api/index.ts @@ -0,0 +1,15 @@ +export {getToolsClient, projectScopedRequest} from "./client" +export { + createConnection, + deleteToolConnection, + executeToolCall, + fetchActionDetail, + fetchActions, + fetchConnection, + fetchIntegrationDetail, + fetchIntegrations, + fetchProviders, + queryConnections, + refreshToolConnection, + revokeToolConnection, +} from "./api" diff --git a/web/packages/agenta-entities/src/gatewayTool/core/index.ts b/web/packages/agenta-entities/src/gatewayTool/core/index.ts new file mode 100644 index 0000000000..fa4ab36cb5 --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/core/index.ts @@ -0,0 +1,32 @@ +export type { + Status, + ToolAuthScheme, + ToolCall, + ToolCallData, + ToolCallFunction, + ToolCallResponse, + ToolCatalogAction, + ToolCatalogActionDetails, + ToolCatalogActionResponse, + ToolCatalogActionsResponse, + ToolCatalogIntegration, + ToolCatalogIntegrationDetails, + ToolCatalogIntegrationResponse, + ToolCatalogIntegrationsResponse, + ToolCatalogProvider, + ToolCatalogProviderDetails, + ToolCatalogProviderResponse, + ToolCatalogProvidersResponse, + ToolConnection, + ToolConnectionCreate, + ToolConnectionCreateData, + ToolConnectionCreatePayload, + ToolConnectionCreatePayloadData, + ToolConnectionResponse, + ToolConnectionStatus, + ToolConnectionsResponse, + ToolProviderKind, + ToolResult, + ToolResultData, +} from "./types" +export {isConnectionActive, isConnectionValid} from "./types" diff --git a/web/packages/agenta-entities/src/gatewayTool/core/types.ts b/web/packages/agenta-entities/src/gatewayTool/core/types.ts new file mode 100644 index 0000000000..53021b1fc6 --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/core/types.ts @@ -0,0 +1,106 @@ +/** + * Gateway-tool domain types. + * + * All wire shapes are taken directly from the Fern-generated client + * (`@agentaai/api-client`) so that this package never drifts from the + * backend OpenAPI definition. We re-export them under their Fern names so + * downstream consumers can import everything from one place + * (`@agenta/entities/gatewayTool`) without reaching into the api-client. + * + * Two small helpers (`isConnectionActive`, `isConnectionValid`) bridge the + * gap between Fern's loosely-typed `ToolConnection.flags` + * (`Record`) and the boolean values the + * backend actually puts in it. + */ + +import type {AgentaApi} from "@agentaai/api-client" + +// --------------------------------------------------------------------------- +// Catalog browse +// --------------------------------------------------------------------------- + +export type ToolCatalogProvider = AgentaApi.ToolCatalogProvider +export type ToolCatalogProviderDetails = AgentaApi.ToolCatalogProviderDetails +export type ToolCatalogProviderResponse = AgentaApi.ToolCatalogProviderResponse +export type ToolCatalogProvidersResponse = AgentaApi.ToolCatalogProvidersResponse + +export type ToolAuthScheme = AgentaApi.ToolAuthScheme +export type ToolProviderKind = AgentaApi.ToolProviderKind + +export type ToolCatalogIntegration = AgentaApi.ToolCatalogIntegration +export type ToolCatalogIntegrationDetails = AgentaApi.ToolCatalogIntegrationDetails +export type ToolCatalogIntegrationResponse = AgentaApi.ToolCatalogIntegrationResponse +export type ToolCatalogIntegrationsResponse = AgentaApi.ToolCatalogIntegrationsResponse + +export type ToolCatalogAction = AgentaApi.ToolCatalogAction +export type ToolCatalogActionDetails = AgentaApi.ToolCatalogActionDetails +export type ToolCatalogActionResponse = AgentaApi.ToolCatalogActionResponse +export type ToolCatalogActionsResponse = AgentaApi.ToolCatalogActionsResponse + +// --------------------------------------------------------------------------- +// Connections +// --------------------------------------------------------------------------- + +export type ToolConnection = AgentaApi.ToolConnection +export type ToolConnectionCreate = AgentaApi.ToolConnectionCreate +export type ToolConnectionCreateData = AgentaApi.ToolConnectionCreateData +export type ToolConnectionResponse = AgentaApi.ToolConnectionResponse +export type ToolConnectionsResponse = AgentaApi.ToolConnectionsResponse +export type ToolConnectionStatus = AgentaApi.ToolConnectionStatus + +// --------------------------------------------------------------------------- +// Tool execution +// --------------------------------------------------------------------------- + +export type ToolCall = AgentaApi.ToolCall +export type ToolCallData = AgentaApi.ToolCallData +export type ToolCallFunction = AgentaApi.ToolCallFunction +export type ToolCallResponse = AgentaApi.ToolCallResponse +export type ToolResult = AgentaApi.ToolResult +export type ToolResultData = AgentaApi.ToolResultData +export type Status = AgentaApi.Status + +// --------------------------------------------------------------------------- +// Legacy API extension +// +// The backend accepts an additional `credentials` field inside the create- +// connection payload's `data` object (used by the API-key auth path), but +// the OpenAPI spec used by Fern doesn't model it yet. We extend the Fern +// type so existing flows compile; when the spec is updated this alias can +// be removed. +// --------------------------------------------------------------------------- + +export type ToolConnectionCreatePayloadData = ToolConnectionCreateData & { + credentials?: Record +} + +export interface ToolConnectionCreatePayload { + connection: Omit & { + data?: ToolConnectionCreatePayloadData | null + } +} + +// --------------------------------------------------------------------------- +// Connection flag accessors +// +// Fern types `ToolConnection.flags` as `Record` +// because the backend model is open-ended. In practice the server only stores +// booleans there; these helpers do the cast in one place so call sites stay +// readable. +// --------------------------------------------------------------------------- + +function readConnectionFlag( + connection: ToolConnection | null | undefined, + flag: string, +): boolean | undefined { + const value = (connection?.flags as Record | null | undefined)?.[flag] + return typeof value === "boolean" ? value : undefined +} + +export function isConnectionActive(connection: ToolConnection | null | undefined): boolean { + return readConnectionFlag(connection, "is_active") ?? false +} + +export function isConnectionValid(connection: ToolConnection | null | undefined): boolean { + return readConnectionFlag(connection, "is_valid") ?? false +} diff --git a/web/packages/agenta-entities/src/gatewayTool/hooks/index.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/index.ts new file mode 100644 index 0000000000..a18ecd3bb5 --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/index.ts @@ -0,0 +1,20 @@ +export {actionDetailQueryFamily, useActionDetail} from "./useActionDetail" +export { + actionsSearchAtom, + catalogActionsInfiniteFamily, + useCatalogActions, +} from "./useCatalogActions" +export { + catalogIntegrationsInfiniteAtom, + integrationsSearchAtom, + useCatalogIntegrations, +} from "./useCatalogIntegrations" +export {useConnectionActions} from "./useConnectionActions" +export {connectionQueryAtomFamily, useConnectionQuery} from "./useConnectionQuery" +export {connectionsQueryAtom, useConnectionsQuery} from "./useConnectionsQuery" +export { + integrationConnectionsAtomFamily, + useIntegrationConnections, +} from "./useIntegrationConnections" +export {integrationDetailQueryFamily, useIntegrationDetail} from "./useIntegrationDetail" +export {buildToolSlug, useToolExecution} from "./useToolExecution" diff --git a/web/oss/src/features/gateway-tools/hooks/useActionDetail.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useActionDetail.ts similarity index 85% rename from web/oss/src/features/gateway-tools/hooks/useActionDetail.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useActionDetail.ts index 7e72c719c7..cadb93f1cd 100644 --- a/web/oss/src/features/gateway-tools/hooks/useActionDetail.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useActionDetail.ts @@ -2,14 +2,14 @@ import {useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" -import {fetchActionDetail} from "@/oss/services/tools/api" -import type {ActionDetailItem} from "@/oss/services/tools/api/types" +import {fetchActionDetail} from "../api" +import type {ToolCatalogActionResponse} from "../core/types" const DEFAULT_PROVIDER = "composio" export const actionDetailQueryFamily = atomFamily( ({integrationKey, actionKey}: {integrationKey: string; actionKey: string}) => - atomWithQuery<{action: ActionDetailItem | null}>(() => ({ + atomWithQuery(() => ({ queryKey: [ "tools", "catalog", diff --git a/web/oss/src/features/gateway-tools/hooks/useCatalogActions.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogActions.ts similarity index 86% rename from web/oss/src/features/gateway-tools/hooks/useCatalogActions.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogActions.ts index 32fdce4e3c..1d8921391f 100644 --- a/web/oss/src/features/gateway-tools/hooks/useCatalogActions.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogActions.ts @@ -4,8 +4,14 @@ import {atom, useAtomValue, useSetAtom} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithInfiniteQuery} from "jotai-tanstack-query" -import {fetchActions} from "@/oss/services/tools/api" -import type {ActionItem, ActionsListResponse} from "@/oss/services/tools/api/types" +import {fetchActions} from "../api" +import type { + ToolCatalogAction, + ToolCatalogActionDetails, + ToolCatalogActionsResponse, +} from "../core/types" + +type CatalogActionItem = ToolCatalogAction | ToolCatalogActionDetails const DEFAULT_PROVIDER = "composio" const CHUNK_SIZE = 10 @@ -15,7 +21,7 @@ const PREFETCH = 2 export const actionsSearchAtom = atom("") export const catalogActionsInfiniteFamily = atomFamily((integrationKey: string) => - atomWithInfiniteQuery((get) => { + atomWithInfiniteQuery((get) => { const search = get(actionsSearchAtom) return { @@ -39,14 +45,14 @@ export const useCatalogActions = (integrationKey: string) => { const query = useAtomValue(catalogActionsInfiniteFamily(integrationKey)) const setSearch = useSetAtom(actionsSearchAtom) - const actions = useMemo(() => { + const actions = useMemo(() => { const pages = query.data?.pages ?? [] - return pages.flatMap((p) => p.actions) + return pages.flatMap((p) => p.actions ?? []) }, [query.data?.pages]) const total = useMemo(() => { const pages = query.data?.pages ?? [] - return pages.length > 0 ? pages[0].total : 0 + return pages.length > 0 ? (pages[0].total ?? 0) : 0 }, [query.data?.pages]) // --- Prefetch logic --- diff --git a/web/oss/src/features/gateway-tools/hooks/useCatalogIntegrations.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogIntegrations.ts similarity index 83% rename from web/oss/src/features/gateway-tools/hooks/useCatalogIntegrations.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogIntegrations.ts index 80c2173e41..16cedf741a 100644 --- a/web/oss/src/features/gateway-tools/hooks/useCatalogIntegrations.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useCatalogIntegrations.ts @@ -3,8 +3,14 @@ import {useCallback, useEffect, useMemo, useRef, useState} from "react" import {atom, useAtomValue, useSetAtom} from "jotai" import {atomWithInfiniteQuery} from "jotai-tanstack-query" -import {fetchIntegrations} from "@/oss/services/tools/api" -import type {IntegrationItem, IntegrationsResponse} from "@/oss/services/tools/api/types" +import {fetchIntegrations} from "../api" +import type { + ToolCatalogIntegration, + ToolCatalogIntegrationDetails, + ToolCatalogIntegrationsResponse, +} from "../core/types" + +type CatalogIntegrationItem = ToolCatalogIntegration | ToolCatalogIntegrationDetails const DEFAULT_PROVIDER = "composio" const CHUNK_SIZE = 10 @@ -13,8 +19,8 @@ const PREFETCH = 2 // Server-side search atom — set by the drawer, drives the query export const integrationsSearchAtom = atom("") -export const catalogIntegrationsInfiniteAtom = atomWithInfiniteQuery( - (get) => { +export const catalogIntegrationsInfiniteAtom = + atomWithInfiniteQuery((get) => { const search = get(integrationsSearchAtom) return { @@ -30,21 +36,20 @@ export const catalogIntegrationsInfiniteAtom = atomWithInfiniteQuery { const query = useAtomValue(catalogIntegrationsInfiniteAtom) const setSearch = useSetAtom(integrationsSearchAtom) - const integrations = useMemo(() => { + const integrations = useMemo(() => { const pages = query.data?.pages ?? [] - return pages.flatMap((p) => p.integrations) + return pages.flatMap((p) => p.integrations ?? []) }, [query.data?.pages]) const total = useMemo(() => { const pages = query.data?.pages ?? [] - return pages.length > 0 ? pages[0].total : 0 + return pages.length > 0 ? (pages[0].total ?? 0) : 0 }, [query.data?.pages]) // --- Prefetch logic --- diff --git a/web/oss/src/features/gateway-tools/hooks/useConnectionActions.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionActions.ts similarity index 84% rename from web/oss/src/features/gateway-tools/hooks/useConnectionActions.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionActions.ts index de8b919180..bf02c29178 100644 --- a/web/oss/src/features/gateway-tools/hooks/useConnectionActions.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionActions.ts @@ -1,11 +1,8 @@ import {useCallback} from "react" -import {queryClient} from "@/oss/lib/api/queryClient" -import { - deleteToolConnection, - refreshToolConnection, - revokeToolConnection, -} from "@/oss/services/tools/api" +import {queryClient} from "@agenta/shared/api" + +import {deleteToolConnection, refreshToolConnection, revokeToolConnection} from "../api" const invalidateConnections = () => { queryClient.invalidateQueries({queryKey: ["tools", "connections"]}) diff --git a/web/oss/src/features/gateway-tools/hooks/useConnectionQuery.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionQuery.ts similarity index 81% rename from web/oss/src/features/gateway-tools/hooks/useConnectionQuery.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionQuery.ts index c85e7a907f..ffbaa2fb08 100644 --- a/web/oss/src/features/gateway-tools/hooks/useConnectionQuery.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionQuery.ts @@ -4,18 +4,18 @@ import {atom, useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" -import {fetchConnection} from "@/oss/services/tools/api" -import type {ConnectionResponse} from "@/oss/services/tools/api/types" +import {fetchConnection} from "../api" +import type {ToolConnectionResponse} from "../core/types" interface ConnectionQueryState { - data?: ConnectionResponse + data?: ToolConnectionResponse isPending: boolean error: unknown refetch: () => Promise } export const connectionQueryAtomFamily = atomFamily((connectionId: string) => - atomWithQuery(() => ({ + atomWithQuery(() => ({ queryKey: ["tools", "connections", connectionId], queryFn: () => fetchConnection(connectionId), enabled: !!connectionId, @@ -25,7 +25,7 @@ export const connectionQueryAtomFamily = atomFamily((connectionId: string) => ) const emptyConnectionQueryAtom = atom({ - data: undefined as ConnectionResponse | undefined, + data: undefined as ToolConnectionResponse | undefined, isPending: false, error: null, refetch: async () => ({}), diff --git a/web/oss/src/features/gateway-tools/hooks/useConnectionsQuery.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionsQuery.ts similarity index 68% rename from web/oss/src/features/gateway-tools/hooks/useConnectionsQuery.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionsQuery.ts index 991ee0b740..dc5f3b4bf8 100644 --- a/web/oss/src/features/gateway-tools/hooks/useConnectionsQuery.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useConnectionsQuery.ts @@ -1,13 +1,10 @@ import {useAtomValue} from "jotai" import {atomWithQuery} from "jotai-tanstack-query" -import {queryConnections} from "@/oss/services/tools/api" -import type {ConnectionItem} from "@/oss/services/tools/api/types" +import {queryConnections} from "../api" +import type {ToolConnectionsResponse} from "../core/types" -export const connectionsQueryAtom = atomWithQuery<{ - count: number - connections: ConnectionItem[] -}>(() => ({ +export const connectionsQueryAtom = atomWithQuery(() => ({ queryKey: ["tools", "connections"], queryFn: () => queryConnections(), staleTime: 30_000, diff --git a/web/oss/src/features/gateway-tools/hooks/useIntegrationConnections.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationConnections.ts similarity index 80% rename from web/oss/src/features/gateway-tools/hooks/useIntegrationConnections.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationConnections.ts index 7e534696d0..34637d4a0e 100644 --- a/web/oss/src/features/gateway-tools/hooks/useIntegrationConnections.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationConnections.ts @@ -4,13 +4,13 @@ import {useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" -import {queryConnections} from "@/oss/services/tools/api" -import type {ConnectionItem, ConnectionsQueryResponse} from "@/oss/services/tools/api/types" +import {queryConnections} from "../api" +import type {ToolConnection, ToolConnectionsResponse} from "../core/types" const DEFAULT_PROVIDER = "composio" export const integrationConnectionsAtomFamily = atomFamily((integrationKey: string) => - atomWithQuery(() => ({ + atomWithQuery(() => ({ queryKey: ["tools", "connections", DEFAULT_PROVIDER, integrationKey], queryFn: () => queryConnections({ @@ -26,7 +26,7 @@ export const integrationConnectionsAtomFamily = atomFamily((integrationKey: stri export const useIntegrationConnections = (integrationKey: string) => { const query = useAtomValue(integrationConnectionsAtomFamily(integrationKey)) - const connections = useMemo( + const connections = useMemo( () => query.data?.connections ?? [], [query.data?.connections], ) diff --git a/web/oss/src/features/gateway-tools/hooks/useIntegrationDetail.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationDetail.ts similarity index 80% rename from web/oss/src/features/gateway-tools/hooks/useIntegrationDetail.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationDetail.ts index 7874fa3c09..a45bb5f1ef 100644 --- a/web/oss/src/features/gateway-tools/hooks/useIntegrationDetail.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useIntegrationDetail.ts @@ -2,13 +2,13 @@ import {useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" import {atomWithQuery} from "jotai-tanstack-query" -import {fetchIntegrationDetail} from "@/oss/services/tools/api" -import type {IntegrationDetailResponse} from "@/oss/services/tools/api/types" +import {fetchIntegrationDetail} from "../api" +import type {ToolCatalogIntegrationResponse} from "../core/types" const DEFAULT_PROVIDER = "composio" export const integrationDetailQueryFamily = atomFamily((integrationKey: string) => - atomWithQuery(() => ({ + atomWithQuery(() => ({ queryKey: ["tools", "catalog", "integrationDetail", DEFAULT_PROVIDER, integrationKey], queryFn: () => fetchIntegrationDetail(DEFAULT_PROVIDER, integrationKey), staleTime: 5 * 60_000, diff --git a/web/oss/src/features/gateway-tools/hooks/useToolExecution.ts b/web/packages/agenta-entities/src/gatewayTool/hooks/useToolExecution.ts similarity index 90% rename from web/oss/src/features/gateway-tools/hooks/useToolExecution.ts rename to web/packages/agenta-entities/src/gatewayTool/hooks/useToolExecution.ts index 3af59f19f9..3c79db4f01 100644 --- a/web/oss/src/features/gateway-tools/hooks/useToolExecution.ts +++ b/web/packages/agenta-entities/src/gatewayTool/hooks/useToolExecution.ts @@ -3,14 +3,14 @@ import {useCallback, useState} from "react" import {buildGatewayToolSlug} from "@agenta/shared/utils" import {v4 as uuidv4} from "uuid" -import {executeToolCall} from "@/oss/services/tools/api" -import type {ToolCallResult} from "@/oss/services/tools/api/types" +import {executeToolCall} from "../api" +import type {ToolResult} from "../core/types" export const buildToolSlug = buildGatewayToolSlug export const useToolExecution = () => { const [isExecuting, setIsExecuting] = useState(false) - const [result, setResult] = useState(null) + const [result, setResult] = useState(null) const [error, setError] = useState(null) const execute = useCallback( diff --git a/web/packages/agenta-entities/src/gatewayTool/index.ts b/web/packages/agenta-entities/src/gatewayTool/index.ts new file mode 100644 index 0000000000..97f011b22d --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/index.ts @@ -0,0 +1,123 @@ +/** + * Gateway-tool entity module. + * + * Browser-side state, queries, and mutations for the `/tools/*` endpoint + * family. API calls go through the Fern-generated `@agentaai/api-client` + * (resolved via `@agenta/sdk`) so request/response shapes stay in sync with + * the backend OpenAPI definition. + * + * Lifted from `web/oss/src/features/gateway-tools/` (the hand-rolled + * services + hooks layer is going away; the OSS feature folder shrinks to + * just orchestration glue). + */ + +// --------------------------------------------------------------------------- +// CORE — domain types +// --------------------------------------------------------------------------- + +export type { + Status, + ToolAuthScheme, + ToolCall, + ToolCallData, + ToolCallFunction, + ToolCallResponse, + ToolCatalogAction, + ToolCatalogActionDetails, + ToolCatalogActionResponse, + ToolCatalogActionsResponse, + ToolCatalogIntegration, + ToolCatalogIntegrationDetails, + ToolCatalogIntegrationResponse, + ToolCatalogIntegrationsResponse, + ToolCatalogProvider, + ToolCatalogProviderDetails, + ToolCatalogProviderResponse, + ToolCatalogProvidersResponse, + ToolConnection, + ToolConnectionCreate, + ToolConnectionCreateData, + ToolConnectionCreatePayload, + ToolConnectionCreatePayloadData, + ToolConnectionResponse, + ToolConnectionStatus, + ToolConnectionsResponse, + ToolProviderKind, + ToolResult, + ToolResultData, +} from "./core" +export {isConnectionActive, isConnectionValid} from "./core" + +// --------------------------------------------------------------------------- +// API — Fern-backed HTTP wrappers +// --------------------------------------------------------------------------- + +export { + createConnection, + deleteToolConnection, + executeToolCall, + fetchActionDetail, + fetchActions, + fetchConnection, + fetchIntegrationDetail, + fetchIntegrations, + fetchProviders, + getToolsClient, + projectScopedRequest, + queryConnections, + refreshToolConnection, + revokeToolConnection, +} from "./api" + +// --------------------------------------------------------------------------- +// STATE — drawer + selection atoms +// --------------------------------------------------------------------------- + +export { + actionSearchAtom, + catalogDrawerOpenAtom, + catalogSearchAtom, + connectionDrawerAtom, + executionDrawerAtom, + selectedCatalogActionAtom, + selectedCatalogIntegrationAtom, +} from "./state" +export type {ConnectionDrawerState, ExecutionDrawerState} from "./state" + +// --------------------------------------------------------------------------- +// HOOKS — query/mutation hooks for React consumers +// --------------------------------------------------------------------------- + +export { + actionDetailQueryFamily, + actionsSearchAtom, + buildToolSlug, + catalogActionsInfiniteFamily, + catalogIntegrationsInfiniteAtom, + connectionQueryAtomFamily, + connectionsQueryAtom, + integrationConnectionsAtomFamily, + integrationDetailQueryFamily, + integrationsSearchAtom, + useActionDetail, + useCatalogActions, + useCatalogIntegrations, + useConnectionActions, + useConnectionQuery, + useConnectionsQuery, + useIntegrationConnections, + useIntegrationDetail, + useToolExecution, +} from "./hooks" + +// --------------------------------------------------------------------------- +// PROMPT — cross-entity bridge (workflow-aware tool removal) +// --------------------------------------------------------------------------- + +export {removePromptToolByNameAtomFamily} from "./prompt" + +// --------------------------------------------------------------------------- +// SLUG HELPERS — re-exported from @agenta/shared for ergonomic single-import +// --------------------------------------------------------------------------- + +export {buildGatewayToolSlug, isGatewayToolSlug, parseGatewayToolSlug} from "@agenta/shared/utils" diff --git a/web/oss/src/features/gateway-tools/prompt/atoms.ts b/web/packages/agenta-entities/src/gatewayTool/prompt/atoms.ts similarity index 67% rename from web/oss/src/features/gateway-tools/prompt/atoms.ts rename to web/packages/agenta-entities/src/gatewayTool/prompt/atoms.ts index 6a940ae5f4..f367527b3f 100644 --- a/web/oss/src/features/gateway-tools/prompt/atoms.ts +++ b/web/packages/agenta-entities/src/gatewayTool/prompt/atoms.ts @@ -1,7 +1,8 @@ -import {workflowMolecule} from "@agenta/entities/workflow" import {atom} from "jotai" import {atomFamily} from "jotai/utils" +import {workflowMolecule} from "../../workflow" + /** promptId may contain colons, so split only on the first ":" */ function splitCompoundKey(compoundKey: string): [string, string] { const idx = compoundKey.indexOf(":") @@ -26,12 +27,27 @@ export const removePromptToolByNameAtomFamily = atomFamily((compoundKey: string) const parameters = entity.data.parameters as Record + // Enhanced-value prompt shape: a Record with __id / __name plus an + // llm_config (or llmConfig) record whose `tools.value` array holds + // each enhanced tool. Loosely typed because enhanced values are + // schema-driven and intentionally heterogeneous. + type EnhancedPrompt = { + __id?: string + __name?: string + llm_config?: Record + llmConfig?: Record + } & Record + type EnhancedTool = { + value?: {function?: {name?: string}} + __tool?: string + } & Record + // Find and update the prompt in parameters const updatedParameters: Record = {} let changed = false for (const [key, value] of Object.entries(parameters)) { - const prompt = value as any + const prompt = value as EnhancedPrompt if (!(prompt?.__id === promptId || prompt?.__name === promptId)) { updatedParameters[key] = value continue @@ -39,15 +55,16 @@ export const removePromptToolByNameAtomFamily = atomFamily((compoundKey: string) // Preserve whichever key the prompt already uses const configKey = prompt?.llm_config ? "llm_config" : "llmConfig" - const llm = prompt?.[configKey] || {} - const toolsArr = llm?.tools?.value + const llm = (prompt?.[configKey] as Record | undefined) || {} + const toolsField = llm?.tools as {value?: unknown} | undefined + const toolsArr = toolsField?.value if (!Array.isArray(toolsArr)) { updatedParameters[key] = value continue } - const updatedTools = toolsArr.filter( - (tool: any) => + const updatedTools = (toolsArr as EnhancedTool[]).filter( + (tool) => tool?.value?.function?.name !== toolIdentifier && tool?.__tool !== toolIdentifier, ) @@ -57,12 +74,13 @@ export const removePromptToolByNameAtomFamily = atomFamily((compoundKey: string) } changed = true + const existingTools = (toolsField ?? {}) as Record updatedParameters[key] = { ...prompt, [configKey]: { ...llm, tools: { - ...llm?.tools, + ...existingTools, value: updatedTools, }, }, diff --git a/web/packages/agenta-entities/src/gatewayTool/prompt/index.ts b/web/packages/agenta-entities/src/gatewayTool/prompt/index.ts new file mode 100644 index 0000000000..122daecf7c --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/prompt/index.ts @@ -0,0 +1 @@ +export {removePromptToolByNameAtomFamily} from "./atoms" diff --git a/web/oss/src/features/gateway-tools/state/atoms.ts b/web/packages/agenta-entities/src/gatewayTool/state/atoms.ts similarity index 100% rename from web/oss/src/features/gateway-tools/state/atoms.ts rename to web/packages/agenta-entities/src/gatewayTool/state/atoms.ts diff --git a/web/packages/agenta-entities/src/gatewayTool/state/index.ts b/web/packages/agenta-entities/src/gatewayTool/state/index.ts new file mode 100644 index 0000000000..2b97f95a72 --- /dev/null +++ b/web/packages/agenta-entities/src/gatewayTool/state/index.ts @@ -0,0 +1,10 @@ +export { + actionSearchAtom, + catalogDrawerOpenAtom, + catalogSearchAtom, + connectionDrawerAtom, + executionDrawerAtom, + selectedCatalogActionAtom, + selectedCatalogIntegrationAtom, +} from "./atoms" +export type {ConnectionDrawerState, ExecutionDrawerState} from "./atoms" diff --git a/web/packages/agenta-entities/src/index.ts b/web/packages/agenta-entities/src/index.ts index f79b6eb58c..c35ca0806e 100644 --- a/web/packages/agenta-entities/src/index.ts +++ b/web/packages/agenta-entities/src/index.ts @@ -287,3 +287,7 @@ export type {Annotation, AnnotationDraft} from "./annotation" // import { evaluationQueueMolecule } from '@agenta/entities/evaluationQueue' // import { annotationMolecule, encodeAnnotationId } from '@agenta/entities/annotation' // import { evaluationRunMolecule } from '@agenta/entities/evaluationRun' +// import { +// useCatalogIntegrations, +// catalogDrawerOpenAtom, +// } from '@agenta/entities/gatewayTool' diff --git a/web/packages/agenta-entities/src/secret/README.md b/web/packages/agenta-entities/src/secret/README.md new file mode 100644 index 0000000000..b173df5105 --- /dev/null +++ b/web/packages/agenta-entities/src/secret/README.md @@ -0,0 +1,136 @@ +# @agenta/entities/secret + +Vault-backed secret storage for LLM provider keys, scoped per project and gated by user authentication. + +This is a **reference implementation** of the entities/molecule pattern using its smallest viable shape — query + mutation only. Use it as the template when migrating other vault-like state into entity packages. + +--- + +## Folder shape + +``` +secret/ +├── api/ +│ ├── api.ts # 4 axios calls against /vault/v1/secrets/ +│ └── index.ts +├── core/ +│ ├── types.ts # DTOs, enums, provider labels/kinds +│ ├── transforms.ts # transformSecret, transformCustomProviderPayloadData, getEnvNameMap +│ └── index.ts +├── state/ +│ ├── atoms.ts # query/mutation atoms + migration atom + action atoms +│ ├── useVaultSecret.ts # React hook (preserved name and return shape from OSS) +│ └── index.ts +├── README.md # this file +└── index.ts # public surface +``` + +## Why no full molecule API + +Other entity packages (`testcase`, `trace`, `workflow`) expose a full molecule with: + +- `molecule.atoms.{data, draft, serverData, isDirty, isNew, ...}` +- `molecule.actions.{update, discard, save, ...}` +- `molecule.get.*` / `molecule.set.*` imperative API +- `molecule.useController(id)` returning `[state, dispatch]` + +Secret deliberately does **not** expose any of that surface. Vault is: + +- A **list-bearing query** (`["vault", "secrets", user?.id, projectId]`) plus three mutations (create/update/delete) — there is no entity-by-id concept. +- **Not revisioned.** No commit/history semantics; the wire DTO has no lineage. +- **Not draft-edited.** Each create/update is sent immediately as a mutation; there is no local draft layer to merge against the server snapshot. +- **Not addressable per-row.** Consumers always work with the full `LlmProvider[]` slice, never with `secretMolecule.atoms.data(id)`. + +Bolting on `draft`, `isDirty`, `discard`, or `useController` here would produce dead methods. The molecule pattern is composable: use the slots your entity actually exercises, document the absent slots, move on. + +## Where the canonical shapes live + +| Concern | Canonical home | Re-exported by | +|---|---|---| +| `User` (auth identity) | `@agenta/shared/types/user` | `@/oss/lib/Types` | +| `LlmProvider` (form/UI shape) | `@agenta/shared/types/llmProvider` | — | +| `llmAvailableProviders`, `llmAvailableProvidersToken` | `@agenta/shared/utils/llmProviders` | — | +| `removeEmptyFromObjects` | `@agenta/shared/utils/objectUtils` | `@/oss/lib/helpers/utils` | +| `userAtom` (primitive, package-readable) | `@agenta/shared/state/user` | populated by OSS `UserListener` | +| `projectIdAtom` (primitive) | `@agenta/shared/state/project` | populated by OSS app-state wiring | +| `SecretDTO*`, enums, `PROVIDER_KINDS`, `PROVIDER_LABELS` | `@agenta/entities/secret/core/types` | `@/oss/lib/Types` (transitional re-export) | +| `transformSecret`, `transformCustomProviderPayloadData`, `getEnvNameMap` | `@agenta/entities/secret/core/transforms` | — | + +## Invariants (preserve these on any future change) + +1. **Query key identity.** `["vault", "secrets", user?.id, projectId]` — exact tuple. Adding/removing/reordering elements invalidates existing cache entries and risks cache double-up if any transitional shim exists. + +2. **Migration atom idempotency.** + - `migrateVaultKeysAtom` (the setter) early-returns if `migrating || migrated`. This guarantees that calling it from multiple subscribers does not double-fire the localStorage migration. + - The hook's `useEffect` triggers `migrateKeys()` when `user && !migrating && !migrated` — fires exactly once after authentication. + - On `!user` (logout), the hook resets the atom to `{migrating: false, migrated: false}` — re-arms migration for the next sign-in in the same session. + - Success path: `{migrating: false, migrated: true}`. Failure path: rollback to `{migrating: false, migrated: false}`. + +3. **`useVaultSecret` return shape.** Preserved verbatim from OSS so that consumer migration is import-path-only: + + ```ts + { + loading: boolean + secrets: LlmProvider[] + customRowSecrets: LlmProvider[] + mutate: () => void + handleModifyVaultSecret(provider): Promise + handleDeleteVaultSecret(provider): Promise + handleModifyCustomVaultSecret(provider): Promise + } + ``` + + Renaming the hook or restructuring the return turns 9 mechanical edits into 9 small rewrites — exactly the regression risk that big-bang migration trades against. + +4. **Failure-prone step.** The localStorage-to-vault migration runs once with side effects (writes to server, writes to localStorage backup, removes the legacy key). On a fresh profile with seeded `localStorage[llmAvailableProvidersToken]`, validate that: + - It fires exactly once. + - It does not re-fire after logout/login in the same session. + - On failure, the migration status rolls back so the next mount can retry. + - `localStorage[llmAvailableProvidersTokenBackup]` is set after success. + +## Usage + +```typescript +import {useVaultSecret} from "@agenta/entities/secret" + +function MyProviderConfig() { + const { + secrets, + customRowSecrets, + loading, + handleModifyVaultSecret, + } = useVaultSecret() + + if (loading) return + + return ( + + ) +} +``` + +For direct atom access (e.g. selecting a derived view without subscribing the whole hook): + +```typescript +import {useAtomValue} from "jotai" +import {standardSecretsAtom, customSecretsAtom} from "@agenta/entities/secret" + +const standard = useAtomValue(standardSecretsAtom) +const custom = useAtomValue(customSecretsAtom) +``` + +## Migration history + +This module replaces the OSS vault stack: + +- `web/oss/src/services/vault/api/index.ts` — moved to `secret/api/api.ts` +- `web/oss/src/state/app/atoms/vault.ts` — moved to `secret/state/atoms.ts` +- `web/oss/src/state/app/hooks/useVaultSecret.ts` — moved to `secret/state/useVaultSecret.ts` +- `web/oss/src/hooks/useVaultSecret.ts` — deleted (was a re-export shim) +- `web/oss/src/lib/helpers/llmProviders.ts` — split: types/constants to `@agenta/shared`, transforms to `secret/core/transforms.ts` + +Design doc: see `~/.gstack/projects/Agenta-AI-agenta/ardaerzin-claude-cool-davinci-1f9b59-design-20260508-014901-vault-to-entities-secret.md` (Status: APPROVED). diff --git a/web/packages/agenta-entities/src/secret/api/api.ts b/web/packages/agenta-entities/src/secret/api/api.ts new file mode 100644 index 0000000000..efee395393 --- /dev/null +++ b/web/packages/agenta-entities/src/secret/api/api.ts @@ -0,0 +1,75 @@ +/** + * Secret API Functions + * + * HTTP functions for the `/secrets/` endpoint family, backed by the + * Fern-generated `@agentaai/api-client` via `@agenta/sdk`. The backend + * still exposes the deprecated `/vault/v1/secrets/` mount for backwards + * compatibility, but new callers go through the canonical path. + * + * The function signatures match the legacy axios wrappers exactly so + * `state/atoms.ts` can keep its existing call sites unchanged. + * + * Naming convention: + * - fetch / fetchAll : GET single / GET all + * - create : POST + * - update : PUT + * - delete : DELETE + */ + +import type {LlmProvider} from "@agenta/shared/types" + +import {transformSecret} from "../core/transforms" + +import {getSecretsClient, projectScopedRequest} from "./client" + +type CreateSecretRequest = Parameters["createSecret"]>[0] +type UpdateSecretRequest = Parameters["updateSecret"]>[0] + +export const fetchVaultSecret = async ({ + projectId, +}: { + projectId: string +}): Promise => { + const result = await getSecretsClient().listSecrets(projectScopedRequest(projectId)) + return transformSecret(result) +} + +export const createVaultSecret = async ({ + projectId, + payload, +}: { + projectId: string + payload: T +}): Promise => { + const result = await getSecretsClient().createSecret( + payload as unknown as CreateSecretRequest, + projectScopedRequest(projectId), + ) + return result as unknown as T +} + +export const updateVaultSecret = async ({ + projectId, + secret_id, + payload, +}: { + projectId: string + secret_id: string + payload: T +}): Promise => { + const result = await getSecretsClient().updateSecret( + {secret_id, ...(payload as Record)} as UpdateSecretRequest, + projectScopedRequest(projectId), + ) + return result as unknown as T +} + +export const deleteVaultSecret = async ({ + projectId, + secret_id, +}: { + projectId: string + secret_id: string +}) => { + return await getSecretsClient().deleteSecret({secret_id}, projectScopedRequest(projectId)) +} diff --git a/web/packages/agenta-entities/src/secret/api/client.ts b/web/packages/agenta-entities/src/secret/api/client.ts new file mode 100644 index 0000000000..b9b1909e1b --- /dev/null +++ b/web/packages/agenta-entities/src/secret/api/client.ts @@ -0,0 +1,29 @@ +import {getAgentaSdkClient} from "@agenta/sdk" + +/** + * Resource client for the vault/secrets API endpoints, taken from the + * Fern-generated `@agentaai/api-client` via the workspace SDK singleton. + * + * The host app is responsible for initialising the SDK singleton at boot + * (host, auth). All entities share the same instance through + * `getAgentaSdkClient()`. + */ +export function getSecretsClient() { + return getAgentaSdkClient().secrets +} + +/** + * Per-request options that scope a Fern call to a specific project. + * + * Fern's generated secrets requests don't model `project_id` — the legacy + * axios layer injected it as a query param via global middleware. We mirror + * that behaviour by emitting it through Fern's + * `BaseRequestOptions.queryParams`. + * + * Unlike the gateway-tools entity (which reads `projectIdAtom` + * imperatively), the secret entity's call sites already have the + * `projectId` in scope, so we keep it as an explicit argument. + */ +export function projectScopedRequest(projectId: string) { + return {queryParams: {project_id: projectId}} +} diff --git a/web/packages/agenta-entities/src/secret/api/index.ts b/web/packages/agenta-entities/src/secret/api/index.ts new file mode 100644 index 0000000000..96c9c774bb --- /dev/null +++ b/web/packages/agenta-entities/src/secret/api/index.ts @@ -0,0 +1,2 @@ +export {fetchVaultSecret, createVaultSecret, updateVaultSecret, deleteVaultSecret} from "./api" +export {getSecretsClient, projectScopedRequest} from "./client" diff --git a/web/packages/agenta-entities/src/secret/core/index.ts b/web/packages/agenta-entities/src/secret/core/index.ts new file mode 100644 index 0000000000..48324c581e --- /dev/null +++ b/web/packages/agenta-entities/src/secret/core/index.ts @@ -0,0 +1,25 @@ +export type { + CreateSecretDto, + CustomModelSettingsDto, + CustomProviderDto, + CustomProviderSettingsDto, + Header, + LegacyLifecycleDto, + SecretDto, + SecretResponseDto, + StandardProviderDto, + StandardProviderSettingsDto, + UpdateSecretDto, + VaultMigrationStatus, +} from "./types" + +export { + CustomProviderKind, + PROVIDER_KINDS, + PROVIDER_LABELS, + STANDARD_PROVIDER_KINDS, + SecretKind, + StandardProviderKind, +} from "./types" + +export {transformSecret, transformCustomProviderPayloadData, getEnvNameMap} from "./transforms" diff --git a/web/packages/agenta-entities/src/secret/core/transforms.ts b/web/packages/agenta-entities/src/secret/core/transforms.ts new file mode 100644 index 0000000000..31adf0bf1d --- /dev/null +++ b/web/packages/agenta-entities/src/secret/core/transforms.ts @@ -0,0 +1,160 @@ +/** + * Secret Entity — Transforms + * + * Pure helpers between the Fern wire shapes (`SecretResponseDto` / + * `CreateSecretDto`) and the in-app `LlmProvider` shape that consumers and + * UI components work with. + * + * The generic `LlmProvider` type and the canonical provider catalog + * (`llmAvailableProviders`, `llmAvailableProvidersToken`) live in + * `@agenta/shared` so non-secret consumers (e.g. + * `@agenta/ui/select-llm-provider`) can use them without pulling in this + * entity package. + */ + +import type {LlmProvider} from "@agenta/shared/types" + +import { + PROVIDER_KINDS, + SecretKind, + StandardProviderKind, + type CreateSecretDto, + type CustomProviderDto, + type SecretResponseDto, + type StandardProviderDto, +} from "./types" + +/** + * Transform raw `/secrets/` response items into the `LlmProvider` shape + * used throughout the app. Standard provider secrets and custom provider + * secrets have different wire shapes; both collapse into the common + * `LlmProvider` representation here. + */ +export const transformSecret = (secrets: SecretResponseDto[]): LlmProvider[] => { + return secrets.reduce((acc, secret) => { + if (secret.kind === SecretKind.ProviderKey) { + const data = secret.data as StandardProviderDto + + const provider = data.kind + const name = provider + const key = data.provider.key + + const envNameMap: Record = { + openai: "OPENAI_API_KEY", + cohere: "COHERE_API_KEY", + anyscale: "ANYSCALE_API_KEY", + deepinfra: "DEEPINFRA_API_KEY", + alephalpha: "ALEPHALPHA_API_KEY", + groq: "GROQ_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", + minimax: "MINIMAX_API_KEY", + } + + acc.push({ + title: name || "", + key, + name: envNameMap[provider] || "", + id: secret.id ?? undefined, + type: secret.kind, + created_at: secret.lifecycle?.created_at ?? undefined, + }) + } else if (secret.kind === SecretKind.CustomProvider) { + const data = secret.data as CustomProviderDto + const extras = (data.provider.extras ?? {}) as Record + + acc.push({ + name: secret.header.name ?? "", + id: secret.id ?? undefined, + type: secret.kind, + provider: data.kind, + apiKey: extras.api_key || "", + apiBaseUrl: data.provider.url ?? "", + region: extras.aws_region_name || "", + vertexProject: extras.vertex_ai_project || "", + vertexLocation: extras.vertex_ai_location || "", + vertexCredentials: extras.vertex_ai_credentials || "", + accessKeyId: extras.aws_access_key_id || "", + accessKey: extras.aws_secret_access_key || "", + sessionToken: extras.aws_session_token || "", + models: data.models.map((model) => model.slug), + modelKeys: data.model_keys ?? undefined, + version: data.provider.version ?? "", + created_at: secret.lifecycle?.created_at ?? "", + }) + } + return acc + }, [] as LlmProvider[]) +} + +/** + * Transform a form-shaped `LlmProvider` into a `CreateSecretDto` suitable + * for POST/PUT against `/secrets/`. + */ +export const transformCustomProviderPayloadData = (values: LlmProvider): CreateSecretDto => { + const providerInput = values.provider?.trim() ?? "" + const providerKind = providerInput + ? (PROVIDER_KINDS[providerInput] ?? + PROVIDER_KINDS[providerInput.toLowerCase()] ?? + providerInput.toLowerCase()) + : "" + + return { + header: { + name: values.name, + description: values.name, + }, + secret: { + kind: SecretKind.CustomProvider, + data: { + kind: providerKind as CustomProviderDto["kind"], + provider: { + url: values.apiBaseUrl, + version: values.version, + extras: { + api_key: values.apiKey, + vertex_ai_location: values.vertexLocation, + vertex_ai_project: values.vertexProject, + vertex_ai_credentials: values.vertexCredentials, + aws_region_name: values.region, + aws_access_key_id: values.accessKeyId, + aws_secret_access_key: values.accessKey, + aws_session_token: values.sessionToken, + }, + }, + models: values.models?.map((slug) => ({slug})) ?? [], + } as CustomProviderDto, + }, + } +} + +/** + * Map the env-var name (e.g. `OPENAI_API_KEY`) used by `LlmProvider.name` + * back to the canonical `StandardProviderKind` value used when creating + * a standard provider secret. + * + * Returns `undefined` for unknown env-var names; the caller is expected + * to throw a domain error in that case. + */ +export const getEnvNameMap = (): Record => ({ + OPENAI_API_KEY: StandardProviderKind.Openai, + COHERE_API_KEY: StandardProviderKind.Cohere, + ANYSCALE_API_KEY: StandardProviderKind.Anyscale, + DEEPINFRA_API_KEY: StandardProviderKind.Deepinfra, + ALEPHALPHA_API_KEY: StandardProviderKind.Alephalpha, + GROQ_API_KEY: StandardProviderKind.Groq, + MISTRAL_API_KEY: StandardProviderKind.Mistral, + // Backward-compatible mapping for legacy Mistral provider name + MISTRALAI_API_KEY: StandardProviderKind.Mistral, + ANTHROPIC_API_KEY: StandardProviderKind.Anthropic, + PERPLEXITYAI_API_KEY: StandardProviderKind.Perplexityai, + TOGETHERAI_API_KEY: StandardProviderKind.TogetherAi, + OPENROUTER_API_KEY: StandardProviderKind.Openrouter, + GEMINI_API_KEY: StandardProviderKind.Gemini, + MINIMAX_API_KEY: StandardProviderKind.Minimax, +}) diff --git a/web/packages/agenta-entities/src/secret/core/types.ts b/web/packages/agenta-entities/src/secret/core/types.ts new file mode 100644 index 0000000000..fcf651e083 --- /dev/null +++ b/web/packages/agenta-entities/src/secret/core/types.ts @@ -0,0 +1,114 @@ +/** + * Secret Entity — Domain Types + * + * All wire shapes come from the Fern-generated client + * (`@agentaai/api-client`) so this package stays aligned with the backend + * OpenAPI definition. The hand-rolled DTOs that used to live here have + * been removed; consumers should import the Fern names directly. + * + * What stays: + * - `PROVIDER_LABELS` / `PROVIDER_KINDS` — app-level provider catalog + * (rendering, slug normalization). No wire equivalent. + * - `STANDARD_PROVIDER_KINDS` — the standard provider list minus the + * legacy `"mistralai"` alias (kept in Fern for backwards compat but + * not shown in OSS provider pickers). + * - `VaultMigrationStatus` — UI state for the one-time localStorage + * migration; not a wire shape. + */ + +import {AgentaApi} from "@agentaai/api-client" + +// --------------------------------------------------------------------------- +// Fern type aliases +// --------------------------------------------------------------------------- + +export type Header = AgentaApi.Header +export type LegacyLifecycleDto = AgentaApi.LegacyLifecycleDto + +export type SecretDto = AgentaApi.SecretDto +export type SecretResponseDto = AgentaApi.SecretResponseDto +export type CreateSecretDto = AgentaApi.CreateSecretDto +export type UpdateSecretDto = AgentaApi.UpdateSecretDto + +export type StandardProviderDto = AgentaApi.StandardProviderDto +export type StandardProviderSettingsDto = AgentaApi.StandardProviderSettingsDto +export type CustomProviderDto = AgentaApi.CustomProviderDto +export type CustomProviderSettingsDto = AgentaApi.CustomProviderSettingsDto +export type CustomModelSettingsDto = AgentaApi.CustomModelSettingsDto + +// `SecretKind` / `StandardProviderKind` / `CustomProviderKind` are Fern +// const-asserted objects. Re-export both the value and the derived type +// so callers can use them like an enum (`SecretKind.ProviderKey`). +export const SecretKind = AgentaApi.SecretKind +export type SecretKind = AgentaApi.SecretKind + +export const StandardProviderKind = AgentaApi.StandardProviderKind +export type StandardProviderKind = AgentaApi.StandardProviderKind + +export const CustomProviderKind = AgentaApi.CustomProviderKind +export type CustomProviderKind = AgentaApi.CustomProviderKind + +// --------------------------------------------------------------------------- +// App-level catalog (no wire equivalent) +// --------------------------------------------------------------------------- + +export const PROVIDER_LABELS: Record = { + openai: "OpenAI", + cohere: "Cohere", + anyscale: "Anyscale", + deepinfra: "DeepInfra", + alephalpha: "Aleph Alpha", + groq: "Groq", + mistral: "Mistral AI", + mistralai: "Mistral AI", + anthropic: "Anthropic", + perplexityai: "Perplexity AI", + together_ai: "Together AI", + openrouter: "OpenRouter", + gemini: "Google Gemini", + vertex_ai: "Google Vertex AI", + bedrock: "AWS Bedrock", + azure: "Azure OpenAI", + minimax: "MiniMax", + custom: "Custom Provider", +} + +export const PROVIDER_KINDS: Record = { + ...Object.entries(PROVIDER_LABELS).reduce( + (acc, [kind, label]) => { + acc[kind] = kind + acc[label.toLowerCase()] = kind + return acc + }, + {} as Record, + ), + // Normalize legacy "mistralai" slug to canonical "mistral" + mistralai: "mistral", +} + +/** + * Standard provider kinds shown in the OSS provider picker. + * + * Fern includes both `"mistral"` and `"mistralai"` in `StandardProviderKind` + * for backwards compatibility, but the OSS UI only shows the canonical + * `"mistral"` entry — filter the alias out here. + */ +export const STANDARD_PROVIDER_KINDS: StandardProviderKind[] = ( + Object.values(StandardProviderKind) as StandardProviderKind[] +).filter((kind) => kind !== StandardProviderKind.Mistralai) + +// --------------------------------------------------------------------------- +// Migration status (UI state, not wire) +// --------------------------------------------------------------------------- + +/** + * Migration status for the legacy localStorage → vault migration. + * + * `migrating: true` while migration is in flight; `migrated: true` after success. + * On logout the hook resets both to `false` so that the next sign-in re-arms + * the migration if needed. + */ +export interface VaultMigrationStatus { + migrating: boolean + migrated: boolean +} diff --git a/web/packages/agenta-entities/src/secret/index.ts b/web/packages/agenta-entities/src/secret/index.ts new file mode 100644 index 0000000000..2bff3d3e98 --- /dev/null +++ b/web/packages/agenta-entities/src/secret/index.ts @@ -0,0 +1,82 @@ +/** + * Secret Entity Module + * + * Vault-backed secret storage for LLM provider keys (and, in the future, + * other secrets) — scoped per project, gated by user authentication. + * + * Reference implementation for the entities/molecule pattern with the + * smallest viable shape: query + mutation only. No draft semantics, no + * imperative `get`/`set` API, no `isDirty` tracking — vault has no + * artifact-revision concept, so adding those would be dead code. + * + * @example + * ```typescript + * import {useVaultSecret} from "@agenta/entities/secret" + * + * const { + * loading, + * secrets, // standard provider configs (LlmProvider[]) + * customRowSecrets, // custom provider configs (LlmProvider[]) + * mutate, // refetch the secrets cache + * handleModifyVaultSecret, // create/update standard + * handleDeleteVaultSecret, // delete + * handleModifyCustomVaultSecret, // create/update custom + * } = useVaultSecret() + * ``` + */ + +// ============================================================================ +// CORE - Types, Enums, Constants, Transforms +// ============================================================================ + +export type { + CreateSecretDto, + CustomModelSettingsDto, + CustomProviderDto, + CustomProviderSettingsDto, + Header, + LegacyLifecycleDto, + SecretDto, + SecretResponseDto, + StandardProviderDto, + StandardProviderSettingsDto, + UpdateSecretDto, + VaultMigrationStatus, +} from "./core" + +export { + CustomProviderKind, + PROVIDER_KINDS, + PROVIDER_LABELS, + STANDARD_PROVIDER_KINDS, + SecretKind, + StandardProviderKind, + getEnvNameMap, + transformCustomProviderPayloadData, + transformSecret, +} from "./core" + +// ============================================================================ +// API - HTTP Functions +// ============================================================================ + +export {fetchVaultSecret, createVaultSecret, updateVaultSecret, deleteVaultSecret} from "./api" + +// ============================================================================ +// STATE - Atoms + Hook +// ============================================================================ + +export { + vaultMigrationAtom, + vaultSecretsQueryAtom, + standardSecretsAtom, + customSecretsAtom, + createVaultSecretMutationAtom, + updateVaultSecretMutationAtom, + deleteVaultSecretMutationAtom, + createStandardSecretAtom, + createCustomSecretAtom, + deleteSecretAtom, + migrateVaultKeysAtom, + useVaultSecret, +} from "./state" diff --git a/web/oss/src/state/app/atoms/vault.ts b/web/packages/agenta-entities/src/secret/state/atoms.ts similarity index 60% rename from web/oss/src/state/app/atoms/vault.ts rename to web/packages/agenta-entities/src/secret/state/atoms.ts index 01ff0578c6..ddb2c6bbe0 100644 --- a/web/oss/src/state/app/atoms/vault.ts +++ b/web/packages/agenta-entities/src/secret/state/atoms.ts @@ -1,39 +1,71 @@ -import {atom} from "jotai" -import {atomWithMutation, atomWithQuery} from "jotai-tanstack-query" +/** + * Secret Entity — Jotai Atoms + * + * Ported verbatim from `web/oss/src/state/app/atoms/vault.ts`. + * + * Pattern: minimal molecule — query + mutation only. No draft semantics, + * no isDirty, no imperative get/set scaffolding. Vault is not an artifact, + * it has no revision lineage; the molecule shape would only contribute + * dead methods. + * + * Important invariants preserved from OSS: + * + * 1. Query key MUST stay byte-identical with the OSS legacy path so + * that any transitional state (e.g. multiple consumers being + * swept) hits one cache entry, not two: + * + * ["vault", "secrets", user?.id, projectId] + * + * 2. The migration atom (`vaultMigrationAtom` + `migrateVaultKeysAtom`) + * runs at most once per authenticated session. The setter + * early-returns if `migrating || migrated` so it is idempotent at + * the action level. The `useVaultSecret` hook gates the trigger + * with `user && !migrating && !migrated`, and resets to + * `{migrating: false, migrated: false}` on logout so a subsequent + * sign-in in the same session can re-run the migration. + * + * 3. Project scoping comes from `@agenta/shared/state.projectIdAtom`, + * which OSS hydrates from the URL via the existing + * `setProjectIdAtom` wiring. User identity comes from + * `@agenta/shared/state.userAtom`, hydrated by OSS `UserListener`. + */ +import {projectIdAtom, userAtom} from "@agenta/shared/state" +import type {LlmProvider} from "@agenta/shared/types" import { llmAvailableProviders, llmAvailableProvidersToken, - LlmProvider, - transformCustomProviderPayloadData, -} from "@/oss/lib/helpers/llmProviders" -import {removeEmptyFromObjects} from "@/oss/lib/helpers/utils" -import {SecretDTOProvider, SecretDTOKind} from "@/oss/lib/Types" -import { - fetchVaultSecret, - createVaultSecret, - updateVaultSecret, - deleteVaultSecret, -} from "@/oss/services/vault/api" + removeEmptyFromObjects, +} from "@agenta/shared/utils" +import {atom} from "jotai" +import {getDefaultStore} from "jotai/vanilla" +import {atomWithMutation, atomWithQuery} from "jotai-tanstack-query" -import {userAtom} from "../../profile/selectors/user" -import {getProjectValues, projectIdAtom} from "../../project" +import {createVaultSecret, deleteVaultSecret, fetchVaultSecret, updateVaultSecret} from "../api/api" +import {getEnvNameMap, transformCustomProviderPayloadData} from "../core/transforms" +import {SecretKind, type VaultMigrationStatus} from "../core/types" /** - * Atom for tracking vault key migration status - * Used to ensure migration only happens once and track its progress + * Atom for tracking vault key migration status. + * Used to ensure migration only happens once and track its progress. */ -export const vaultMigrationAtom = atom({ +export const vaultMigrationAtom = atom({ migrating: false, migrated: false, }) /** - * Query atom for fetching vault secrets - * Only enabled when user is authenticated and migration is complete + * Query atom for fetching vault secrets. + * Only enabled when user is authenticated and a project is selected. + * + * The query key includes `user?.id` so that switching users invalidates + * the cache (a different user's secrets must not leak through React Query's + * cache). */ export const vaultSecretsQueryAtom = atomWithQuery((get) => { const user = get(userAtom) + // Read migration status to keep this atom subscribed to migration changes + // (matches OSS behavior — migration completion can trigger a refetch). const _migrationStatus = get(vaultMigrationAtom) const projectId = get(projectIdAtom) @@ -51,13 +83,13 @@ export const vaultSecretsQueryAtom = atomWithQuery((get) => { refetchOnReconnect: false, refetchOnMount: true, enabled: !!user && !!projectId, - // && migrationStatus.migrated, // Only fetch when user exists and migration is done } }) /** - * Derived atom for standard provider secrets - * Maps vault data to available providers with their keys + * Derived atom for standard provider secrets. + * Maps the canonical provider catalog to vault data, attaching the stored + * `key` / `id` / `created_at` for any matched provider. */ export const standardSecretsAtom = atom((get) => { const queryResult = get(vaultSecretsQueryAtom) @@ -79,96 +111,72 @@ export const standardSecretsAtom = atom((get) => { }) /** - * Derived atom for custom provider secrets - * Filters vault data for custom provider configurations + * Derived atom for custom provider secrets. + * Filters vault data for custom provider configurations. */ export const customSecretsAtom = atom((get) => { const queryResult = get(vaultSecretsQueryAtom) const data = queryResult.data || [] - return data.filter((secret) => secret.type === SecretDTOKind.CUSTOM_PROVIDER_KEY) + return data.filter((secret) => secret.type === SecretKind.CustomProvider) }) /** - * Mutation atom for creating vault secrets + * Helper: read the current projectId from the shared store. + * + * Replaces the OSS `getProjectValues()` helper. We only need `projectId` + * here, so reading it directly from the primitive atom keeps the package + * agnostic of OSS-only state (org, projects query, etc.). + */ +const readProjectId = (): string | null => { + return getDefaultStore().get(projectIdAtom) +} + +/** + * Mutation atom for creating vault secrets. */ export const createVaultSecretMutationAtom = atomWithMutation(() => ({ - mutationFn: async (payload: any) => { - const {projectId} = getProjectValues() + mutationFn: async (payload: unknown) => { + const projectId = readProjectId() if (!projectId) { throw new Error("[vault] Missing projectId for createVaultSecret") } return await createVaultSecret({projectId, payload}) }, - onSuccess: () => { - // Invalidate and refetch vault secrets - // This will be handled by the hook - }, })) /** - * Mutation atom for updating vault secrets + * Mutation atom for updating vault secrets. */ export const updateVaultSecretMutationAtom = atomWithMutation(() => ({ - mutationFn: async ({secret_id, payload}: {secret_id: string; payload: any}) => { - const {projectId} = getProjectValues() + mutationFn: async ({secret_id, payload}: {secret_id: string; payload: unknown}) => { + const projectId = readProjectId() if (!projectId) { throw new Error("[vault] Missing projectId for updateVaultSecret") } return await updateVaultSecret({projectId, secret_id, payload}) }, - onSuccess: () => { - // Invalidate and refetch vault secrets - // This will be handled by the hook - }, })) /** - * Mutation atom for deleting vault secrets + * Mutation atom for deleting vault secrets. */ export const deleteVaultSecretMutationAtom = atomWithMutation(() => ({ mutationFn: async (secret_id: string) => { - const {projectId} = getProjectValues() + const projectId = readProjectId() if (!projectId) { throw new Error("[vault] Missing projectId for deleteVaultSecret") } return await deleteVaultSecret({projectId, secret_id}) }, - onSuccess: () => { - // Invalidate and refetch vault secrets - // This will be handled by the hook - }, })) /** - * Helper function to get environment name mapping for providers - * Maps environment variable names to their SecretDTOProvider enum values - * This matches the original working implementation - */ -const getEnvNameMap = (): Record => ({ - OPENAI_API_KEY: SecretDTOProvider.OPENAI, - COHERE_API_KEY: SecretDTOProvider.COHERE, - ANYSCALE_API_KEY: SecretDTOProvider.ANYSCALE, - DEEPINFRA_API_KEY: SecretDTOProvider.DEEPINFRA, - ALEPHALPHA_API_KEY: SecretDTOProvider.ALEPHALPHA, - GROQ_API_KEY: SecretDTOProvider.GROQ, - MISTRAL_API_KEY: SecretDTOProvider.MISTRAL, - // Backward-compatible mapping for legacy Mistral provider name - MISTRALAI_API_KEY: SecretDTOProvider.MISTRAL, - ANTHROPIC_API_KEY: SecretDTOProvider.ANTHROPIC, - PERPLEXITYAI_API_KEY: SecretDTOProvider.PERPLEXITYAI, - TOGETHERAI_API_KEY: SecretDTOProvider.TOGETHERAI, - OPENROUTER_API_KEY: SecretDTOProvider.OPENROUTER, - GEMINI_API_KEY: SecretDTOProvider.GEMINI, - MINIMAX_API_KEY: SecretDTOProvider.MINIMAX, -}) - -/** - * Atom for creating standard provider vault secrets - * Handles the complex payload creation and provider mapping + * Atom for creating standard provider vault secrets. + * Handles the payload creation and provider mapping. */ export const createStandardSecretAtom = atom(null, async (get, set, provider: LlmProvider) => { const envNameMap = getEnvNameMap() @@ -184,13 +192,12 @@ export const createStandardSecretAtom = atom(null, async (get, set, provider: Ll ) } - // Match the original working payload structure exactly const payload = { header: { name: provider.title, }, secret: { - kind: SecretDTOKind.PROVIDER_KEY, + kind: SecretKind.ProviderKey, data: { kind: providerKind, provider: { @@ -214,8 +221,8 @@ export const createStandardSecretAtom = atom(null, async (get, set, provider: Ll }) /** - * Atom for creating custom provider vault secrets - * Handles custom provider payload transformation and cleanup + * Atom for creating custom provider vault secrets. + * Handles custom provider payload transformation and cleanup. */ export const createCustomSecretAtom = atom(null, async (get, set, provider: LlmProvider) => { const customSecrets = get(customSecretsAtom) @@ -240,7 +247,7 @@ export const createCustomSecretAtom = atom(null, async (get, set, provider: LlmP }) /** - * Atom for deleting vault secrets + * Atom for deleting vault secrets. */ export const deleteSecretAtom = atom(null, async (get, set, provider: LlmProvider) => { const deleteMutation = get(deleteVaultSecretMutationAtom) @@ -256,8 +263,15 @@ export const deleteSecretAtom = atom(null, async (get, set, provider: LlmProvide }) /** - * Migration atom for handling localStorage to vault migration - * This is a write-only atom that performs the migration process + * Migration atom for handling localStorage → vault migration. + * + * Idempotent at the action level: early-returns if already migrating or + * migrated. The hook's `useEffect` is responsible for the user-presence + * trigger and the logout reset (re-arm). + * + * On success, sets `{migrating: false, migrated: true}`. + * On failure, rolls back to `{migrating: false, migrated: false}` so the + * next mount can retry. */ export const migrateVaultKeysAtom = atom(null, async (get, set) => { const migrationStatus = get(vaultMigrationAtom) diff --git a/web/packages/agenta-entities/src/secret/state/index.ts b/web/packages/agenta-entities/src/secret/state/index.ts new file mode 100644 index 0000000000..d9caacede9 --- /dev/null +++ b/web/packages/agenta-entities/src/secret/state/index.ts @@ -0,0 +1,15 @@ +export { + vaultMigrationAtom, + vaultSecretsQueryAtom, + standardSecretsAtom, + customSecretsAtom, + createVaultSecretMutationAtom, + updateVaultSecretMutationAtom, + deleteVaultSecretMutationAtom, + createStandardSecretAtom, + createCustomSecretAtom, + deleteSecretAtom, + migrateVaultKeysAtom, +} from "./atoms" + +export {useVaultSecret} from "./useVaultSecret" diff --git a/web/oss/src/state/app/hooks/useVaultSecret.ts b/web/packages/agenta-entities/src/secret/state/useVaultSecret.ts similarity index 56% rename from web/oss/src/state/app/hooks/useVaultSecret.ts rename to web/packages/agenta-entities/src/secret/state/useVaultSecret.ts index 1944b82194..6330d4583b 100644 --- a/web/oss/src/state/app/hooks/useVaultSecret.ts +++ b/web/packages/agenta-entities/src/secret/state/useVaultSecret.ts @@ -1,112 +1,103 @@ +/** + * useVaultSecret — React hook over the secret molecule's atoms. + * + * Hook name is preserved (NOT renamed to useSecretController) so that the + * 9 OSS consumer files migrate by changing only the import path. Same + * identifier, same return shape, same call signature — keeps the big-bang + * PR a mechanical refactor. + * + * Return shape (unchanged from OSS): + * - loading: boolean + * - secrets: LlmProvider[] (standard provider configs) + * - customRowSecrets: LlmProvider[] (custom provider configs) + * - mutate: () => void (manual cache refetch) + * - handleModifyVaultSecret(provider) (create/update standard) + * - handleDeleteVaultSecret(provider) (delete) + * - handleModifyCustomVaultSecret(provider) (create/update custom) + */ + import {useCallback, useEffect, useMemo} from "react" +import {userAtom} from "@agenta/shared/state" +import type {LlmProvider} from "@agenta/shared/types" import {useAtom, useAtomValue, useSetAtom} from "jotai" -import {LlmProvider} from "@/oss/lib/helpers/llmProviders" -import {useProfileData} from "@/oss/state/profile" - import { - vaultMigrationAtom, - vaultSecretsQueryAtom, - standardSecretsAtom, - customSecretsAtom, - createStandardSecretAtom, createCustomSecretAtom, + createStandardSecretAtom, + customSecretsAtom, deleteSecretAtom, migrateVaultKeysAtom, -} from "../atoms/vault" + standardSecretsAtom, + vaultMigrationAtom, + vaultSecretsQueryAtom, +} from "./atoms" /** - * Hook for managing vault secrets and LLM provider keys using Jotai atoms - * Replaces the SWR-based useVaultSecret hook + * Hook for managing vault secrets and LLM provider keys. * - * Features: - * - Handles migration from localStorage to vault system - * - Manages CRUD operations for provider keys using atoms - * - Provides real-time synchronization using React Query via Jotai - * - Supports both standard and custom provider configurations + * Behaviors preserved verbatim from the OSS implementation: * - * @returns { - * loading: boolean - Loading state including migration - * secrets: LlmProvider[] - List of standard provider configurations - * customRowSecrets: LlmProvider[] - List of custom provider configurations - * mutate: Function - Function to refresh vault data - * handleModifyVaultSecret: Function - Update/create standard provider - * handleDeleteVaultSecret: Function - Delete provider configuration - * handleModifyCustomVaultSecret: Function - Update/create custom provider - * } + * - Triggers `migrateKeys()` once when the user is authenticated and + * migration has not been attempted (`user && !migrating && !migrated`). + * - Resets the migration status to `{migrating: false, migrated: false}` + * on logout so a subsequent sign-in in the same session re-arms the + * migration. + * - Refetches the secrets query after each mutation succeeds. + * - `loading` is the union of query pending state + migration in flight + * + migration not yet completed. */ export const useVaultSecret = () => { - const {user} = useProfileData() + const user = useAtomValue(userAtom) - // Atoms for state management const [migrationStatus, setMigrationStatus] = useAtom(vaultMigrationAtom) const vaultQuery = useAtomValue(vaultSecretsQueryAtom) const standardSecrets = useAtomValue(standardSecretsAtom) const customSecrets = useAtomValue(customSecretsAtom) - // Action atoms const createStandardSecret = useSetAtom(createStandardSecretAtom) const createCustomSecret = useSetAtom(createCustomSecretAtom) const deleteSecret = useSetAtom(deleteSecretAtom) const migrateKeys = useSetAtom(migrateVaultKeysAtom) - /** - * Handle migration when user is available and migration hasn't been attempted - */ useEffect(() => { if (user && !migrationStatus.migrating && !migrationStatus.migrated) { migrateKeys() } else if (!user && (migrationStatus.migrated || migrationStatus.migrating)) { - // Reset migration status when user logs out + // Reset migration status when user logs out so the next sign-in + // can re-attempt migration if needed. setMigrationStatus({migrating: false, migrated: false}) } }, [user, migrationStatus.migrating, migrationStatus.migrated, migrateKeys, setMigrationStatus]) - /** - * Handle standard provider secret creation/update - * This matches the original implementation pattern - */ const handleModifyVaultSecret = useCallback( async (provider: LlmProvider) => { await createStandardSecret(provider) - vaultQuery.refetch() // Refresh data after mutation + vaultQuery.refetch() }, [createStandardSecret, vaultQuery], ) - /** - * Handle custom provider secret creation/update - */ const handleModifyCustomVaultSecret = useCallback( async (provider: LlmProvider) => { await createCustomSecret(provider) - vaultQuery.refetch() // Refresh data after mutation + vaultQuery.refetch() }, [createCustomSecret, vaultQuery], ) - /** - * Handle provider secret deletion - */ const handleDeleteVaultSecret = useCallback( async (provider: LlmProvider) => { await deleteSecret(provider) - vaultQuery.refetch() // Refresh data after mutation + vaultQuery.refetch() }, [deleteSecret, vaultQuery], ) - /** - * Manual refresh function for vault data - */ const mutate = useCallback(() => { vaultQuery.refetch() }, [vaultQuery]) - /** - * Computed loading state considering both data fetching and migration - */ const loading = useMemo(() => { return vaultQuery.isPending || migrationStatus.migrating || !migrationStatus.migrated }, [vaultQuery.isPending, migrationStatus.migrating, migrationStatus.migrated]) diff --git a/web/packages/agenta-entity-ui/package.json b/web/packages/agenta-entity-ui/package.json index ef9986fddb..4325e26920 100644 --- a/web/packages/agenta-entity-ui/package.json +++ b/web/packages/agenta-entity-ui/package.json @@ -14,6 +14,7 @@ ".": "./src/index.ts", "./adapters": "./src/adapters/index.ts", "./drill-in": "./src/DrillInView/index.ts", + "./gatewayTool": "./src/gatewayTool/index.ts", "./modals": "./src/modals/index.ts", "./selection": "./src/selection/index.ts", "./testcase": "./src/testcase/index.ts", diff --git a/web/oss/src/components/pages/settings/Tools/components/ConnectionStatusBadge.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/components/ConnectionStatusBadge.tsx similarity index 55% rename from web/oss/src/components/pages/settings/Tools/components/ConnectionStatusBadge.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/components/ConnectionStatusBadge.tsx index 3b1a864218..81d5a34b4a 100644 --- a/web/oss/src/components/pages/settings/Tools/components/ConnectionStatusBadge.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/components/ConnectionStatusBadge.tsx @@ -1,10 +1,13 @@ +import { + isConnectionActive, + isConnectionValid, + type ToolConnection, +} from "@agenta/entities/gatewayTool" import {Tag} from "antd" -import type {ConnectionItem} from "@/oss/services/tools/api/types" - -export default function ConnectionStatusBadge({connection}: {connection: ConnectionItem}) { - const isActive = connection.flags?.is_active ?? false - const isValid = connection.flags?.is_valid ?? false +export default function ConnectionStatusBadge({connection}: {connection: ToolConnection}) { + const isActive = isConnectionActive(connection) + const isValid = isConnectionValid(connection) if (isValid && isActive) { return Connected diff --git a/web/oss/src/features/gateway-tools/components/ResultViewer.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/components/ResultViewer.tsx similarity index 98% rename from web/oss/src/features/gateway-tools/components/ResultViewer.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/components/ResultViewer.tsx index 844c4044bc..ded6f3bd60 100644 --- a/web/oss/src/features/gateway-tools/components/ResultViewer.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/components/ResultViewer.tsx @@ -1,19 +1,17 @@ import {useMemo} from "react" -import {Editor} from "@agenta/ui/editor" -import {CopySimple} from "@phosphor-icons/react" -import {Alert, Button, Form, Input, InputNumber, message, Typography} from "antd" - -import type {ToolCallResult} from "@/oss/services/tools/api/types" - +import type {ToolResult} from "@agenta/entities/gatewayTool" import { buildFormFieldsFromData, buildFormFieldsFromSchema, type FormFieldDescriptor, -} from "../utils/schema" +} from "@agenta/shared/utils" +import {Editor} from "@agenta/ui/editor" +import {CopySimple} from "@phosphor-icons/react" +import {Alert, Button, Form, Input, InputNumber, message, Typography} from "antd" interface Props { - result: ToolCallResult | null + result: ToolResult | null error?: string | null outputSchema?: Record | null jsonMode?: boolean diff --git a/web/oss/src/features/gateway-tools/components/SchemaForm.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsx similarity index 99% rename from web/oss/src/features/gateway-tools/components/SchemaForm.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsx index 613050d03d..256034ec21 100644 --- a/web/oss/src/features/gateway-tools/components/SchemaForm.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsx @@ -1,12 +1,11 @@ import {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from "react" +import {buildFormFieldsFromSchema, type FormFieldDescriptor} from "@agenta/shared/utils" import {Editor} from "@agenta/ui/editor" import {MinusCircle, Plus} from "@phosphor-icons/react" import {Button, Collapse, Form, Input, InputNumber, Switch, Select, Typography} from "antd" import type {FormInstance} from "antd" -import {buildFormFieldsFromSchema, type FormFieldDescriptor} from "../utils/schema" - export interface SchemaFormHandle { getValues: () => Promise> } diff --git a/web/oss/src/features/gateway-tools/drawers/CatalogDrawer.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/CatalogDrawer.tsx similarity index 93% rename from web/oss/src/features/gateway-tools/drawers/CatalogDrawer.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/drawers/CatalogDrawer.tsx index b210a79886..2bee8225fd 100644 --- a/web/oss/src/features/gateway-tools/drawers/CatalogDrawer.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/CatalogDrawer.tsx @@ -1,5 +1,19 @@ import React, {useCallback, useMemo, useRef, useState} from "react" +import { + actionsSearchAtom, + catalogDrawerOpenAtom, + executionDrawerAtom, + integrationsSearchAtom, + isConnectionActive, + useCatalogActions, + useCatalogIntegrations, + useIntegrationConnections, + type ToolCatalogIntegration, + type ToolCatalogIntegrationDetails, + type ToolConnection, +} from "@agenta/entities/gatewayTool" +import {useDebouncedAtomSearch} from "@agenta/shared/hooks" import {ScrollSentinel, ScrollToTopButton} from "@agenta/ui" import {ArrowLeft, CaretDown, MagnifyingGlass, Plus} from "@phosphor-icons/react" import type {MenuProps} from "antd" @@ -19,16 +33,10 @@ import { import {useAtom, useSetAtom} from "jotai" import Image from "next/image" -import type {ConnectionItem, IntegrationItem} from "@/oss/services/tools/api/types" - -import {actionsSearchAtom, useCatalogActions} from "../hooks/useCatalogActions" -import {integrationsSearchAtom, useCatalogIntegrations} from "../hooks/useCatalogIntegrations" -import {useDebouncedAtomSearch} from "../hooks/useDebouncedAtomSearch" -import {useIntegrationConnections} from "../hooks/useIntegrationConnections" -import {catalogDrawerOpenAtom, executionDrawerAtom} from "../state/atoms" - import ConnectDrawer from "./ConnectDrawer" +type CatalogIntegrationItem = ToolCatalogIntegration | ToolCatalogIntegrationDetails + // --------------------------------------------------------------------------- // Expandable description — 2-line clamp with inline "see more" / "see less" // --------------------------------------------------------------------------- @@ -59,8 +67,12 @@ interface Props { export default function CatalogDrawer({onConnectionCreated}: Props) { const [open, setOpen] = useAtom(catalogDrawerOpenAtom) - const [selectedIntegration, setSelectedIntegration] = useState(null) - const [connectIntegration, setConnectIntegration] = useState(null) + const [selectedIntegration, setSelectedIntegration] = useState( + null, + ) + const [connectIntegration, setConnectIntegration] = useState( + null, + ) const setIntegrationsSearch = useSetAtom(integrationsSearchAtom) const setActionsSearch = useSetAtom(actionsSearchAtom) @@ -78,7 +90,7 @@ export default function CatalogDrawer({onConnectionCreated}: Props) { setActionsSearch("") }, [setActionsSearch]) - const handleConnect = useCallback((integration: IntegrationItem) => { + const handleConnect = useCallback((integration: CatalogIntegrationItem) => { setConnectIntegration(integration) }, []) @@ -120,8 +132,8 @@ export default function CatalogDrawer({onConnectionCreated}: Props) { open={!!connectIntegration} integrationKey={connectIntegration.key} integrationName={connectIntegration.name} - integrationLogo={connectIntegration.logo} - integrationDescription={connectIntegration.description} + integrationLogo={connectIntegration.logo ?? undefined} + integrationDescription={connectIntegration.description ?? undefined} authSchemes={connectIntegration.auth_schemes ?? []} onClose={() => setConnectIntegration(null)} onSuccess={handleConnectionSuccess} @@ -135,7 +147,7 @@ export default function CatalogDrawer({onConnectionCreated}: Props) { // Integrations view (sticky header + scrollable content) // --------------------------------------------------------------------------- -function IntegrationsView({onSelect}: {onSelect: (integration: IntegrationItem) => void}) { +function IntegrationsView({onSelect}: {onSelect: (integration: CatalogIntegrationItem) => void}) { const setAtom = useSetAtom(integrationsSearchAtom) const search = useDebouncedAtomSearch(setAtom) const scrollRef = useRef(null) @@ -274,7 +286,7 @@ function ActionsView({ onBack, onConnect, }: { - integration: IntegrationItem + integration: CatalogIntegrationItem onBack: () => void onConnect: () => void }) { @@ -285,13 +297,13 @@ function ActionsView({ const {connections} = useIntegrationConnections(integration.key) const handleOpenConnection = useCallback( - (conn: ConnectionItem) => { + (conn: ToolConnection) => { setExecutionDrawer({ - connectionId: conn.id, - connectionSlug: conn.slug, + connectionId: conn.id ?? "", + connectionSlug: conn.slug ?? "", integrationKey: conn.integration_key, integrationName: integration.name, - integrationLogo: integration.logo, + integrationLogo: integration.logo ?? undefined, }) }, [setExecutionDrawer, integration.name, integration.logo], @@ -300,11 +312,11 @@ function ActionsView({ const connectMenuItems = useMemo( () => connections.map((conn) => ({ - key: conn.id, + key: conn.id ?? conn.slug ?? "", label: (
{conn.name || conn.slug} - {conn.flags?.is_active && ( + {isConnectionActive(conn) && ( )}
diff --git a/web/oss/src/features/gateway-tools/drawers/ConnectDrawer.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectDrawer.tsx similarity index 97% rename from web/oss/src/features/gateway-tools/drawers/ConnectDrawer.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectDrawer.tsx index b323c3bfa8..6d93a44c40 100644 --- a/web/oss/src/features/gateway-tools/drawers/ConnectDrawer.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectDrawer.tsx @@ -1,14 +1,12 @@ import {useCallback, useRef, useState} from "react" +import {createConnection, fetchConnection} from "@agenta/entities/gatewayTool" +import {getAgentaApiUrl, getAgentaWebUrl, queryClient} from "@agenta/shared/api" import {generateDefaultSlug, randomAlphanumeric} from "@agenta/shared/utils" import {EnhancedModal, ModalContent, ModalFooter} from "@agenta/ui" import {Divider, Form, Input, message, Select, Tooltip, Typography} from "antd" import Image from "next/image" -import {queryClient} from "@/oss/lib/api/queryClient" -import {getAgentaApiUrl, getAgentaWebUrl} from "@/oss/lib/helpers/api" -import {createConnection, fetchConnection} from "@/oss/services/tools/api" - const DEFAULT_PROVIDER = "composio" type AuthMode = "oauth" | "api_key" @@ -96,14 +94,14 @@ export default function ConnectDrawer({ if (redirectUrl) { // Composio handles all auth (OAuth and API key) via their redirect UI const popup = window.open( - redirectUrl, + redirectUrl as string, "tools_oauth", "width=600,height=700,popup=yes", ) if (!popup) { setLoading(false) message.warning("Popup blocked. Redirecting in this tab.") - window.location.assign(redirectUrl) + window.location.assign(redirectUrl as string) return } diff --git a/web/oss/src/features/gateway-tools/drawers/ConnectionManagerDrawer.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectionManagerDrawer.tsx similarity index 88% rename from web/oss/src/features/gateway-tools/drawers/ConnectionManagerDrawer.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectionManagerDrawer.tsx index 255f3c7000..833c97a2dd 100644 --- a/web/oss/src/features/gateway-tools/drawers/ConnectionManagerDrawer.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ConnectionManagerDrawer.tsx @@ -1,19 +1,28 @@ import {useCallback, useState} from "react" +import { + connectionDrawerAtom, + executionDrawerAtom, + isConnectionActive, + isConnectionValid, + useConnectionActions, + useConnectionQuery, + type ToolConnection, +} from "@agenta/entities/gatewayTool" +import {getAgentaApiUrl, getAgentaWebUrl, queryClient} from "@agenta/shared/api" +import {dayjs} from "@agenta/shared/utils" +import {modal} from "@agenta/ui/app-message" import {ArrowClockwise, Play, Trash, XCircle} from "@phosphor-icons/react" import {Button, Descriptions, Divider, Drawer, Spin, Typography} from "antd" import {useAtom, useSetAtom} from "jotai" -import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" -import ConnectionStatusBadge from "@/oss/components/pages/settings/Tools/components/ConnectionStatusBadge" -import {queryClient} from "@/oss/lib/api/queryClient" -import {getAgentaApiUrl, getAgentaWebUrl} from "@/oss/lib/helpers/api" -import {formatDay} from "@/oss/lib/helpers/dateTimeHelper" -import type {ConnectionItem} from "@/oss/services/tools/api/types" +import ConnectionStatusBadge from "../components/ConnectionStatusBadge" -import {useConnectionActions} from "../hooks/useConnectionActions" -import {useConnectionQuery} from "../hooks/useConnectionQuery" -import {connectionDrawerAtom, executionDrawerAtom} from "../state/atoms" +function formatCreatedAt(value: string | null | undefined): string { + if (!value) return "-" + const parsed = dayjs.utc(value) + return parsed.isValid() ? parsed.format("YYYY-MM-DD HH:mm") : "-" +} export default function ConnectionManagerDrawer() { const [state, setState] = useAtom(connectionDrawerAtom) @@ -30,7 +39,7 @@ export default function ConnectionManagerDrawer() { }, [setState]) const setConnectionInCache = useCallback( - (nextConnection: ConnectionItem | null) => { + (nextConnection: ToolConnection | null) => { if (!connectionId) return queryClient.setQueryData(["tools", "connections", connectionId], { count: nextConnection ? 1 : 0, @@ -107,10 +116,12 @@ export default function ConnectionManagerDrawer() { const onRevoke = useCallback(() => { if (!state?.connectionId) return - AlertPopup({ + modal.confirm({ title: "Revoke Connection", - message: + content: "This will mark the connection as invalid. You can refresh it later to reactivate.", + okText: "Yes", + cancelText: "Cancel", onOk: async () => { setActionLoading("revoke") try { @@ -125,10 +136,12 @@ export default function ConnectionManagerDrawer() { const onDelete = useCallback(() => { if (!state?.connectionId) return - AlertPopup({ + modal.confirm({ title: "Delete Connection", - message: + content: "Are you sure you want to delete this connection? This action is irreversible.", + okText: "Yes", + cancelText: "Cancel", onOk: async () => { setActionLoading("delete") try { @@ -144,14 +157,14 @@ export default function ConnectionManagerDrawer() { const onTest = useCallback(() => { if (!connection) return setExecution({ - connectionId: connection.id, - connectionSlug: connection.slug, + connectionId: connection.id ?? "", + connectionSlug: connection.slug ?? "", integrationKey: connection.integration_key, }) }, [connection, setExecution]) - const isActive = connection?.flags?.is_active ?? false - const isValid = connection?.flags?.is_valid ?? false + const isActive = isConnectionActive(connection) + const isValid = isConnectionValid(connection) return ( diff --git a/web/oss/src/features/gateway-tools/drawers/ToolExecutionDrawer.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ToolExecutionDrawer.tsx similarity index 93% rename from web/oss/src/features/gateway-tools/drawers/ToolExecutionDrawer.tsx rename to web/packages/agenta-entity-ui/src/gatewayTool/drawers/ToolExecutionDrawer.tsx index ca87ea66d8..90f8d3fdb4 100644 --- a/web/oss/src/features/gateway-tools/drawers/ToolExecutionDrawer.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/drawers/ToolExecutionDrawer.tsx @@ -1,5 +1,16 @@ import React, {useCallback, useMemo, useRef, useState} from "react" +import { + actionsSearchAtom, + executionDrawerAtom, + useActionDetail, + useCatalogActions, + useIntegrationDetail, + useToolExecution, + type ToolCatalogAction, + type ToolCatalogActionDetails, +} from "@agenta/entities/gatewayTool" +import {useDebouncedAtomSearch} from "@agenta/shared/hooks" import {ScrollSentinel, ScrollToTopButton} from "@agenta/ui" import { ArrowLeft, @@ -26,17 +37,11 @@ import { import {useAtom, useSetAtom} from "jotai" import Image from "next/image" -import type {ActionItem} from "@/oss/services/tools/api/types" - import ResultViewer from "../components/ResultViewer" import type {SchemaFormHandle} from "../components/SchemaForm" import SchemaForm from "../components/SchemaForm" -import {useActionDetail} from "../hooks/useActionDetail" -import {actionsSearchAtom, useCatalogActions} from "../hooks/useCatalogActions" -import {useDebouncedAtomSearch} from "../hooks/useDebouncedAtomSearch" -import {useIntegrationDetail} from "../hooks/useIntegrationDetail" -import {useToolExecution} from "../hooks/useToolExecution" -import {executionDrawerAtom} from "../state/atoms" + +type CatalogActionItem = ToolCatalogAction | ToolCatalogActionDetails const DEFAULT_PROVIDER = "composio" @@ -47,7 +52,7 @@ const DEFAULT_PROVIDER = "composio" export default function ToolExecutionDrawer() { const [state, setState] = useAtom(executionDrawerAtom) const open = !!state - const [selectedAction, setSelectedAction] = useState(null) + const [selectedAction, setSelectedAction] = useState(null) const setActionsSearch = useSetAtom(actionsSearchAtom) // Fetch integration info as fallback when name/logo not in state @@ -70,7 +75,7 @@ export default function ToolExecutionDrawer() { setActionsSearch("") }, [setActionsSearch]) - const handleSelectAction = useCallback((action: ActionItem) => { + const handleSelectAction = useCallback((action: CatalogActionItem) => { setSelectedAction(action) }, []) @@ -97,7 +102,7 @@ export default function ToolExecutionDrawer() { @@ -105,7 +110,7 @@ export default function ToolExecutionDrawer() { void + onSelectAction: (action: CatalogActionItem) => void }) { const setAtom = useSetAtom(actionsSearchAtom) const search = useDebouncedAtomSearch(setAtom) @@ -161,7 +166,7 @@ function ActionPickerStep({ {integrationLogo && ( {integrationName}("form") - const inputSchema = action?.schemas?.inputs ?? null - const outputSchema = action?.schemas?.outputs ?? null + // The fetch endpoint always returns the detailed variant; narrow so we + // can reach `schemas`. The wider union exists because Fern reuses the + // response wrapper between list/detail endpoints. + const detailedAction = + action && "schemas" in action ? (action as ToolCatalogActionDetails) : null + const inputSchema = detailedAction?.schemas?.inputs ?? null + const outputSchema = detailedAction?.schemas?.outputs ?? null const displayName = action?.name ?? actionName ?? actionKey const jsonMode = viewMode === "json" @@ -348,7 +358,7 @@ function ActionDetailStep({ {integrationLogo && ( {integrationName} { return apiUrl } + +/** + * Get the Agenta Web URL. + * Falls back to current origin if not configured. + */ +export const getAgentaWebUrl = (): string => { + const webUrl = getEnv("NEXT_PUBLIC_AGENTA_WEB_URL") + + if (!webUrl) { + const runtimeLocation = (globalThis as RuntimeGlobal).location + if (runtimeLocation?.protocol && runtimeLocation?.hostname) { + return `${runtimeLocation.protocol}//${runtimeLocation.hostname}` + } + } + + return webUrl +} diff --git a/web/packages/agenta-shared/src/api/index.ts b/web/packages/agenta-shared/src/api/index.ts index 268f7596c7..8108a750e1 100644 --- a/web/packages/agenta-shared/src/api/index.ts +++ b/web/packages/agenta-shared/src/api/index.ts @@ -2,7 +2,7 @@ * API utilities for Agenta packages. */ -export {getEnv, getAgentaApiUrl, processEnv} from "./env" +export {getEnv, getAgentaApiUrl, getAgentaWebUrl, processEnv} from "./env" export {axios, createAxiosInstance, configureAxios, resetAxiosConfig} from "./axios" export type {AxiosInterceptorConfig} from "./axios" export type { diff --git a/web/packages/agenta-shared/src/hooks/index.ts b/web/packages/agenta-shared/src/hooks/index.ts index 4fab3a6873..bcce1312f9 100644 --- a/web/packages/agenta-shared/src/hooks/index.ts +++ b/web/packages/agenta-shared/src/hooks/index.ts @@ -3,6 +3,7 @@ */ export {useDebounceInput} from "./useDebounceInput" +export {useDebouncedAtomSearch} from "./useDebouncedAtomSearch" export {default as useLazyEffect} from "./useLazyEffect" export {useSelectionState, type UseSelectionStateResult} from "./useSelectionState" export {useRunAllShortcut, type UseRunAllShortcutParams} from "./useRunAllShortcut" diff --git a/web/oss/src/features/gateway-tools/hooks/useDebouncedAtomSearch.ts b/web/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts similarity index 89% rename from web/oss/src/features/gateway-tools/hooks/useDebouncedAtomSearch.ts rename to web/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts index bb6fdb636b..8286673aeb 100644 --- a/web/oss/src/features/gateway-tools/hooks/useDebouncedAtomSearch.ts +++ b/web/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts @@ -2,7 +2,7 @@ import {useCallback, useEffect, useRef, useState} from "react" export function useDebouncedAtomSearch(setAtom: (v: string) => void, delay = 300) { const [local, setLocal] = useState("") - const timerRef = useRef>() + const timerRef = useRef | undefined>(undefined) const onChange = useCallback( (v: string) => { diff --git a/web/packages/agenta-shared/src/state/index.ts b/web/packages/agenta-shared/src/state/index.ts index 93634d72ca..5c8ef9c854 100644 --- a/web/packages/agenta-shared/src/state/index.ts +++ b/web/packages/agenta-shared/src/state/index.ts @@ -4,6 +4,7 @@ export {projectIdAtom, setProjectIdAtom} from "./project" export {sessionAtom, setSessionAtom} from "./session" +export {userAtom, setUserAtom} from "./user" export {atomWithRefresh} from "jotai/utils" export { atomWithCompare, diff --git a/web/packages/agenta-shared/src/state/user.ts b/web/packages/agenta-shared/src/state/user.ts new file mode 100644 index 0000000000..c47b6a8674 --- /dev/null +++ b/web/packages/agenta-shared/src/state/user.ts @@ -0,0 +1,43 @@ +/** + * User identity state atoms. + * + * These are primitive atoms that should be populated by the app. + * Entity packages read from `userAtom` to scope queries that depend + * on the authenticated user's identity (e.g., the secret entity's + * `["vault", "secrets", user?.id, projectId]` query key). + * + * The app is responsible for wiring its own profile/auth state + * into this atom via `setUserAtom`. See OSS `UserListener` for the + * canonical wiring pattern (paired with `SessionListener`). + * + * @example + * ```typescript + * // In app code + * import {setUserAtom} from "@agenta/shared/state" + * import {useSetAtom} from "jotai" + * + * const setSharedUser = useSetAtom(setUserAtom) + * useEffect(() => { setSharedUser(profileQuery.data ?? null) }, [profileQuery.data]) + * ``` + */ + +import {atom} from "jotai" + +import type {User} from "../types/user" + +/** + * Current authenticated user (or `null` if not authenticated). + * + * Default: `null`. Populated by app bootstrap. + * Entity packages read from this to gate queries behind authentication + * and to scope query keys by user identity. + */ +export const userAtom = atom(null) + +/** + * Set the authenticated user. + * Use from app code to push profile state into the shared atom. + */ +export const setUserAtom = atom(null, (_get, set, user: User | null) => { + set(userAtom, user) +}) diff --git a/web/packages/agenta-shared/src/types/index.ts b/web/packages/agenta-shared/src/types/index.ts index 8f0a52135d..64c9ac08a3 100644 --- a/web/packages/agenta-shared/src/types/index.ts +++ b/web/packages/agenta-shared/src/types/index.ts @@ -12,3 +12,9 @@ export type { ToolCall, SimpleChatMessage, } from "./chatMessage" + +// User identity +export type {User} from "./user" + +// LLM provider configuration +export type {LlmProvider} from "./llmProvider" diff --git a/web/packages/agenta-shared/src/types/llmProvider.ts b/web/packages/agenta-shared/src/types/llmProvider.ts new file mode 100644 index 0000000000..9f32cbd975 --- /dev/null +++ b/web/packages/agenta-shared/src/types/llmProvider.ts @@ -0,0 +1,33 @@ +/** + * LLM provider configuration shape. + * + * Cross-cutting type used by the secret entity (`@agenta/entities/secret`), + * provider-selection UI components (`@agenta/ui/select-llm-provider`), + * and OSS feature pages (ModelRegistry, settings/Secrets, prompts, evaluations). + * + * The `type` field is typed as `string` rather than the secret-domain enum + * (`SecretDTOKind`) to keep this package independent of `@agenta/entities/secret` + * — preventing a circular dependency. Consumers that need the enum import it + * directly from `@agenta/entities/secret`. + */ +export interface LlmProvider { + title?: string + key?: string + provider?: string + name?: string + apiKey?: string + apiBaseUrl?: string + version?: string + region?: string + vertexProject?: string + vertexLocation?: string + vertexCredentials?: string + accessKeyId?: string + accessKey?: string + sessionToken?: string + models?: string[] + modelKeys?: string[] + id?: string + type?: string + created_at?: string +} diff --git a/web/packages/agenta-shared/src/types/user.ts b/web/packages/agenta-shared/src/types/user.ts new file mode 100644 index 0000000000..2d615a2d88 --- /dev/null +++ b/web/packages/agenta-shared/src/types/user.ts @@ -0,0 +1,13 @@ +/** + * User identity type. + * + * The shape of an authenticated user across packages. + * Populated into the primitive `userAtom` in `@agenta/shared/state` + * by app-level bootstrap (see OSS `UserListener`). + */ +export interface User { + id: string + uid: string + username: string + email: string +} diff --git a/web/oss/src/features/gateway-tools/utils/schema.ts b/web/packages/agenta-shared/src/utils/gatewayToolSchema.ts similarity index 100% rename from web/oss/src/features/gateway-tools/utils/schema.ts rename to web/packages/agenta-shared/src/utils/gatewayToolSchema.ts diff --git a/web/packages/agenta-shared/src/utils/index.ts b/web/packages/agenta-shared/src/utils/index.ts index 3a02b28633..d154995c0a 100644 --- a/web/packages/agenta-shared/src/utils/index.ts +++ b/web/packages/agenta-shared/src/utils/index.ts @@ -194,6 +194,13 @@ export { } from "./connectionSlug" export {buildGatewayToolSlug, isGatewayToolSlug, parseGatewayToolSlug} from "./toolSlug" +// Gateway Tool JSON-Schema → form-field descriptor utilities +export { + buildFormFieldsFromData, + buildFormFieldsFromSchema, + type FormFieldDescriptor, +} from "./gatewayToolSchema" + // Polling utilities export {shortPoll} from "./shortPoll" @@ -202,3 +209,9 @@ export {removeTrailingSlash} from "./uriUtils" // Trace ID conversion utilities (UUID ↔ OpenTelemetry) export {uuidToSpanId, uuidToTraceId} from "./traceIds" + +// LLM provider catalog (cross-cutting between secret entity and provider UI) +export {llmAvailableProviders, llmAvailableProvidersToken} from "./llmProviders" + +// Object cleanup utilities +export {removeEmptyFromObjects} from "./objectUtils" diff --git a/web/packages/agenta-shared/src/utils/llmProviders.ts b/web/packages/agenta-shared/src/utils/llmProviders.ts new file mode 100644 index 0000000000..42ecaee8bb --- /dev/null +++ b/web/packages/agenta-shared/src/utils/llmProviders.ts @@ -0,0 +1,34 @@ +import type {LlmProvider} from "../types/llmProvider" + +/** + * LocalStorage key for legacy provider-key storage. + * + * Predates the vault-backed secret system. Used by the migration atom + * in `@agenta/entities/secret` to detect and migrate legacy keys into + * server-side vault storage on first authenticated load. + */ +export const llmAvailableProvidersToken = "llmAvailableProvidersToken" + +/** + * Canonical list of standard LLM providers supported by Agenta. + * + * Each entry maps a display title to its environment-variable name + * (which is also the key the vault uses to identify provider secrets + * via `SecretDTOProvider`). Used by the secret entity to seed the + * standard-provider list and by UI components for provider selection. + */ +export const llmAvailableProviders: LlmProvider[] = [ + {title: "OpenAI", key: "", name: "OPENAI_API_KEY"}, + {title: "Mistral AI", key: "", name: "MISTRAL_API_KEY"}, + {title: "Cohere", key: "", name: "COHERE_API_KEY"}, + {title: "Anthropic", key: "", name: "ANTHROPIC_API_KEY"}, + {title: "Anyscale", key: "", name: "ANYSCALE_API_KEY"}, + {title: "Perplexity AI", key: "", name: "PERPLEXITYAI_API_KEY"}, + {title: "DeepInfra", key: "", name: "DEEPINFRA_API_KEY"}, + {title: "Together AI", key: "", name: "TOGETHERAI_API_KEY"}, + {title: "Aleph Alpha", key: "", name: "ALEPHALPHA_API_KEY"}, + {title: "OpenRouter", key: "", name: "OPENROUTER_API_KEY"}, + {title: "Groq", key: "", name: "GROQ_API_KEY"}, + {title: "Google Gemini", key: "", name: "GEMINI_API_KEY"}, + {title: "MiniMax", key: "", name: "MINIMAX_API_KEY"}, +] diff --git a/web/packages/agenta-shared/src/utils/objectUtils.ts b/web/packages/agenta-shared/src/utils/objectUtils.ts new file mode 100644 index 0000000000..f48a59707d --- /dev/null +++ b/web/packages/agenta-shared/src/utils/objectUtils.ts @@ -0,0 +1,36 @@ +/** + * Recursively remove empty values from an object or array. + * + * Strips out: + * - `null` and `undefined` + * - Empty strings (`""`) + * - Empty objects (`{}`) + * - Empty arrays after recursive cleanup + * + * Used to clean up form payloads before sending to APIs that + * reject empty/null fields (e.g. vault custom secret payloads). + */ +export const removeEmptyFromObjects = (obj: T): T => { + if (Array.isArray(obj)) { + return obj + .map((item) => removeEmptyFromObjects(item)) + .filter( + (item) => + item != null && + (typeof item !== "object" || Object.keys(item as object).length > 0), + ) as unknown as T + } + if (obj && typeof obj === "object") { + return Object.entries(obj as Record).reduce( + (acc, [key, value]) => { + const cleaned = removeEmptyFromObjects(value) + if (cleaned !== null && cleaned !== undefined && cleaned !== "") { + acc[key] = cleaned + } + return acc + }, + {} as Record, + ) as unknown as T + } + return obj +} diff --git a/web/packages/agenta-ui/src/SelectLLMProvider/README.md b/web/packages/agenta-ui/src/SelectLLMProvider/README.md index 0290cb8b63..ff6ea5bbca 100644 --- a/web/packages/agenta-ui/src/SelectLLMProvider/README.md +++ b/web/packages/agenta-ui/src/SelectLLMProvider/README.md @@ -77,7 +77,7 @@ In the OSS codebase, you can wrap this component with vault integration: ```tsx import {SelectLLMProviderBase} from '@agenta/ui' -import {useVaultSecret} from '@/oss/hooks/useVaultSecret' +import {useVaultSecret} from '@agenta/entities/secret' function SelectLLMProvider(props) { const {customRowSecrets} = useVaultSecret() diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index fff5c3fe57..5543851dd0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -781,6 +781,9 @@ importers: '@agenta/ui': specifier: workspace:../agenta-ui version: link:../agenta-ui + '@agentaai/api-client': + specifier: workspace:../agenta-api-client + version: link:../agenta-api-client '@ant-design/icons': specifier: '>=5.0.0' version: 6.2.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)