From 0deabf09386940f4cba18aae0175a319008112cf Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:20:58 +0200 Subject: [PATCH 01/20] feat: add knowledge base pages --- public/assets/knowledgebases_icon.svg | 1 + public/i18n/en/common.json | 4 + src/App.tsx | 18 + src/components/NavConfig.tsx | 5 + .../KnowledgeBasesCreateEditPage.tsx | 203 ++++++++++ .../create-edit-knowledgeBases.validator.ts | 24 ++ src/pages/knowledge-bases/index.tsx | 7 + .../overview/KnowledgeBasesOverviewPage.tsx | 105 +++++ src/redux/otomiApi.ts | 374 ++++++++++++++++++ 9 files changed, 741 insertions(+) create mode 100644 public/assets/knowledgebases_icon.svg create mode 100644 src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx create mode 100644 src/pages/knowledge-bases/create-edit/create-edit-knowledgeBases.validator.ts create mode 100644 src/pages/knowledge-bases/index.tsx create mode 100644 src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx diff --git a/public/assets/knowledgebases_icon.svg b/public/assets/knowledgebases_icon.svg new file mode 100644 index 00000000..23f69af3 --- /dev/null +++ b/public/assets/knowledgebases_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/i18n/en/common.json b/public/i18n/en/common.json index d47653db..eb6b6447 100644 --- a/public/i18n/en/common.json +++ b/public/i18n/en/common.json @@ -95,6 +95,10 @@ "Code-repository_plural": "Code Repositories", "TITLE_CODE_REPOSITORY": "Code repository details", "TITLE_CODE_REPOSITORIES": "Code repositories - {{scope}}", + "Knowledge-base": "Knowledge Base", + "Knowledge-base_plural": "Knowledge Bases", + "TITLE_KNOWLEDGE_BASE": "Knowledge base details", + "TITLE_KNOWLEDGE_BASES": "Knowledge bases - {{scope}}", "TITLE_NETWORK_POLICY": "Network policy details", "TITLE_NETWORK_POLICIES": "Network policies - {{scope}}" } diff --git a/src/App.tsx b/src/App.tsx index a9db2697..c3c96b90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,8 @@ import BuildsCreateEditPage from 'pages/builds/create-edit/BuildsCreateEditPage' import BuildsOverviewPage from 'pages/builds/overview/BuildsOverviewPage' import CodeRepositoriesCreateEditPage from 'pages/code-repositories/create-edit/CodeRepositoriesCreateEditPage' import CodeRepositoriesOverviewPage from 'pages/code-repositories/overview/CodeRepositoriesOverviewPage' +import KnowledgeBasesCreateEditPage from 'pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage' +import KnowledgeBasesOverviewPage from 'pages/knowledge-bases/overview/KnowledgeBasesOverviewPage' import NetworkPoliciesOverviewPage from 'pages/network-policies/overview/NetworkPoliciesOverviewPage' import NetworkPoliciesIngressCreateEditPage from 'pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage' import NetworkPoliciesEgressCreateEditPage from 'pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage' @@ -106,6 +108,22 @@ function App() { exact /> + + + + diff --git a/src/components/NavConfig.tsx b/src/components/NavConfig.tsx index 55dc8fbc..f7a94ddf 100644 --- a/src/components/NavConfig.tsx +++ b/src/components/NavConfig.tsx @@ -51,6 +51,11 @@ export default function NavConfig() { path: `/catalogs/${oboTeamId}`, icon: getIcon('developer_guide_icon.svg'), }, + { + title: 'Knowledge Bases', + path: `/teams/${oboTeamId}/knowledge-bases`, + icon: getIcon('knowledgebases_icon.svg'), + }, { title: 'Code Repositories', path: `/teams/${oboTeamId}/code-repositories`, diff --git a/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx b/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx new file mode 100644 index 00000000..96ee4d33 --- /dev/null +++ b/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx @@ -0,0 +1,203 @@ +import { Grid } from '@mui/material' +import { TextField } from 'components/forms/TextField' +import { Autocomplete } from 'components/forms/Autocomplete' +import { LandingHeader } from 'components/LandingHeader' +import PaperLayout from 'layouts/Paper' +import React, { useEffect } from 'react' +import { FormProvider, Resolver, useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import { Redirect, RouteComponentProps } from 'react-router-dom' +import { isEqual } from 'lodash' +import { + CreateAplKnowledgeBaseApiArg, + useCreateAplKnowledgeBaseMutation, + useDeleteAplKnowledgeBaseMutation, + useEditAplKnowledgeBaseMutation, + useGetAiModelsQuery, + useGetAplKnowledgeBaseQuery, +} from 'redux/otomiApi' +import { useTranslation } from 'react-i18next' +import FormRow from 'components/forms/FormRow' +import { useAppSelector } from 'redux/hooks' +import Section from 'components/Section' +import DeleteButton from 'components/DeleteButton' +import { LoadingButton } from '@mui/lab' +import { Divider } from 'components/Divider' +import { knowledgeBaseSchema } from './create-edit-knowledgeBases.validator' + +interface Params { + teamId: string + knowledgeBaseName?: string +} + +export default function KnowledgeBasesCreateEditPage({ + match: { + params: { teamId, knowledgeBaseName }, + }, +}: RouteComponentProps): React.ReactElement { + const { t } = useTranslation() + + const [create, { isLoading: isLoadingCreate, isSuccess: isSuccessCreate }] = useCreateAplKnowledgeBaseMutation() + const [update, { isLoading: isLoadingUpdate, isSuccess: isSuccessUpdate }] = useEditAplKnowledgeBaseMutation() + const [del, { isLoading: isLoadingDelete, isSuccess: isSuccessDelete }] = useDeleteAplKnowledgeBaseMutation() + const { data, isLoading, isFetching, isError, refetch } = useGetAplKnowledgeBaseQuery( + { teamId, knowledgeBaseName }, + { skip: !knowledgeBaseName }, + ) + const { data: aiModels } = useGetAiModelsQuery() + + const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) + useEffect(() => { + if (isDirty !== false) return + if (!isFetching) refetch() + }, [isDirty]) + + type FormType = CreateAplKnowledgeBaseApiArg['body'] + + const defaultValues: FormType = { + kind: 'AkamaiKnowledgeBase' as const, + metadata: { + name: '', + }, + spec: { + sourceUrl: '', + modelName: '', + }, + } + + const methods = useForm({ + resolver: yupResolver(knowledgeBaseSchema) as unknown as Resolver, + defaultValues, + }) + + const { + register, + reset, + handleSubmit, + watch, + formState: { errors }, + setValue, + } = methods + + useEffect(() => { + if (data) reset(data) + }, [data, reset]) + + const onSubmit = (formData: FormType) => { + const body = { ...formData } + + if (knowledgeBaseName) update({ teamId, knowledgeBaseName, body }) + else create({ teamId, body }) + } + + const mutating = isLoadingCreate || isLoadingUpdate || isLoadingDelete + if (!mutating && (isSuccessCreate || isSuccessUpdate || isSuccessDelete)) + return + + const loading = isLoading + + if (loading) return + + return ( + + + + +
+
+ + { + const value = e.target.value + setValue('metadata.name', value) + }} + error={!!errors.metadata?.name} + helperText={errors.metadata?.name?.message?.toString()} + /> + + + + + + { + const value = e.target.value + setValue('spec.sourceUrl', value) + }} + error={!!errors.spec?.sourceUrl} + helperText={errors.spec?.sourceUrl?.message?.toString()} + value={watch('spec.sourceUrl') || ''} + /> + + + + + + model.spec.modelType === 'embedding') + .map((model) => model.metadata.name) || [] + } + getOptionLabel={(option) => { + const model = aiModels?.find((m) => m.metadata.name === option) + return model?.spec.displayName || option + }} + value={watch('spec.modelName') || null} + onChange={(_, value) => { + setValue('spec.modelName', value || '') + }} + errorText={errors.spec?.modelName?.message?.toString()} + helperText={errors.spec?.modelName?.message?.toString()} + /> + +
+ + {knowledgeBaseName && ( + del({ teamId, knowledgeBaseName })} + resourceName={watch('metadata.name')} + resourceType='knowledgebase' + data-cy='button-delete-knowledgebase' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} + /> + )} + + {knowledgeBaseName ? 'Save Changes' : 'Create Knowledge Base'} + + +
+
+
+ ) +} diff --git a/src/pages/knowledge-bases/create-edit/create-edit-knowledgeBases.validator.ts b/src/pages/knowledge-bases/create-edit/create-edit-knowledgeBases.validator.ts new file mode 100644 index 00000000..6d429cf9 --- /dev/null +++ b/src/pages/knowledge-bases/create-edit/create-edit-knowledgeBases.validator.ts @@ -0,0 +1,24 @@ +import * as yup from 'yup' + +// Schema for Knowledge Base form validation +export const knowledgeBaseSchema = yup.object({ + kind: yup.string().oneOf(['AkamaiKnowledgeBase']).required(), + metadata: yup.object({ + name: yup + .string() + .required('Knowledge base name is required') + .min(2, 'Knowledge base name must be at least 2 characters') + .matches( + /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, + 'Name must start and end with a lowercase letter or number, and can only contain lowercase letters, numbers, and hyphens', + ), + }), + spec: yup.object({ + sourceUrl: yup + .string() + .required('Data source URL is required') + .url('Please enter a valid URL (e.g., https://example.com/vector-db.zip)') + .matches(/^https?:\/\//, 'Data source must be a valid HTTP or HTTPS URL'), + modelName: yup.string().required('Please select an embedding model'), + }), +}) diff --git a/src/pages/knowledge-bases/index.tsx b/src/pages/knowledge-bases/index.tsx new file mode 100644 index 00000000..78bc814a --- /dev/null +++ b/src/pages/knowledge-bases/index.tsx @@ -0,0 +1,7 @@ +import KnowledgeBasesOverviewPage from './overview/KnowledgeBasesOverviewPage' +import KnowledgeBasesCreateEditPage from './create-edit/KnowledgeBasesCreateEditPage' + +export default { + KnowledgeBasesOverviewPage, + KnowledgeBasesCreateEditPage, +} diff --git a/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx b/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx new file mode 100644 index 00000000..a642a40c --- /dev/null +++ b/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx @@ -0,0 +1,105 @@ +import PaperLayout from 'layouts/Paper' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { RouteComponentProps } from 'react-router-dom' +import { getRole } from 'utils/data' +import { useGetAplKnowledgeBasesQuery } from 'redux/otomiApi' +import { useAppSelector } from 'redux/hooks' +import { HeadCell } from '../../../components/EnhancedTable' +import RLink from '../../../components/Link' +import ListTable from '../../../components/ListTable' + +const getKnowledgeBaseName = (): CallableFunction => + function (row: any): string | React.ReactElement { + const { teamId, name }: { teamId: string; name: string } = row + const path = `/teams/${teamId}/knowledge-bases/${encodeURIComponent(name)}` + return ( + + {name} + + ) + } + +const getStatus = (): CallableFunction => + function (row: any): string { + const { status } = row + return status?.phase || 'Unknown' + } + +const getDataSource = (): CallableFunction => + function (row: any): string | React.ReactElement { + const { sourceUrl }: { sourceUrl: string } = row + return sourceUrl || 'N/A' + } + +const getEmbeddingModel = (): CallableFunction => + function (row: any): string { + const { modelName } = row + return modelName || 'N/A' + } + +interface Params { + teamId: string +} + +export default function KnowledgeBasesOverviewPage({ + match: { + params: { teamId }, + }, +}: RouteComponentProps): React.ReactElement { + const { t } = useTranslation() + + const { data: knowledgeBases, isLoading, isFetching, refetch } = useGetAplKnowledgeBasesQuery({ teamId }) + + const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) + useEffect(() => { + if (isDirty !== false) return + if (!isFetching) refetch() + }, [isDirty]) + + // Transform API response to match table format + const transformedData = + knowledgeBases?.map((kb) => ({ + name: kb.metadata.name, + teamId, + status: kb.status, + sourceUrl: kb.spec.sourceUrl, + modelName: kb.spec.modelName, + })) || [] + + const headCells: HeadCell[] = [ + { + id: 'name', + label: t('Name'), + renderer: getKnowledgeBaseName(), + }, + { + id: 'status', + label: t('Status'), + renderer: getStatus(), + }, + { + id: 'datasource', + label: t('Data Source'), + renderer: getDataSource(), + }, + { + id: 'embeddingModel', + label: t('Embedding Model'), + renderer: getEmbeddingModel(), + }, + ] + + const customButtonText = () => Create Knowledge Base + + const comp = ( + + ) + return +} diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 108a15b4..8be71274 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -533,6 +533,50 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + getAiModels: build.query({ + query: () => ({ url: `/alpha/ai/models` }), + }), + getAplKnowledgeBases: build.query({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/kb` }), + }), + createAplKnowledgeBase: build.mutation({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/kb`, method: 'POST', body: queryArg.body }), + }), + getAplKnowledgeBase: build.query({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/kb/${queryArg.knowledgeBaseName}` }), + }), + editAplKnowledgeBase: build.mutation({ + query: (queryArg) => ({ + url: `/alpha/teams/${queryArg.teamId}/kb/${queryArg.knowledgeBaseName}`, + method: 'PUT', + body: queryArg.body, + }), + }), + deleteAplKnowledgeBase: build.mutation({ + query: (queryArg) => ({ + url: `/alpha/teams/${queryArg.teamId}/kb/${queryArg.knowledgeBaseName}`, + method: 'DELETE', + }), + }), + getAplAgents: build.query({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents` }), + }), + createAplAgent: build.mutation({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents`, method: 'POST', body: queryArg.body }), + }), + getAplAgent: build.query({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}` }), + }), + editAplAgent: build.mutation({ + query: (queryArg) => ({ + url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}`, + method: 'PUT', + body: queryArg.body, + }), + }), + deleteAplAgent: build.mutation({ + query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}`, method: 'DELETE' }), + }), }), overrideExisting: false, }) @@ -6226,6 +6270,325 @@ export type EditAppApiArg = { values?: object } } +export type GetAiModelsApiResponse = /** status 200 Successfully obtained shared AI models */ ({ + kind: 'AplAIModel' + spec: { + displayName?: string + modelEndpoint: string + modelType: 'foundation' | 'embedding' + modelDimension?: number + } +} & { + metadata: { + name: string + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +})[] +export type GetAiModelsApiArg = void +export type GetAplKnowledgeBasesApiResponse = /** status 200 Successfully obtained knowledge bases */ ({ + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +})[] +export type GetAplKnowledgeBasesApiArg = { + /** ID of team */ + teamId: string +} +export type CreateAplKnowledgeBaseApiResponse = /** status 200 Successfully stored knowledge base configuration */ { + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +} +export type CreateAplKnowledgeBaseApiArg = { + /** ID of team */ + teamId: string + /** KnowledgeBase object */ + body: { + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } + } & { + metadata: { + name: string + } + } +} +export type GetAplKnowledgeBaseApiResponse = /** status 200 Successfully obtained knowledge base configuration */ { + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } +} & { + metadata: { + name: string + } +} +export type GetAplKnowledgeBaseApiArg = { + /** ID of team */ + teamId: string + /** Name of the knowledge base */ + knowledgeBaseName: string +} +export type EditAplKnowledgeBaseApiResponse = /** status 200 Successfully edited a team knowledge base */ { + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +} +export type EditAplKnowledgeBaseApiArg = { + /** ID of team */ + teamId: string + /** Name of the knowledge base */ + knowledgeBaseName: string + /** KnowledgeBase object that contains updated values */ + body: { + kind: 'AkamaiKnowledgeBase' + spec: { + modelName: string + sourceUrl: string + } + } & { + metadata: { + name: string + } + } +} +export type DeleteAplKnowledgeBaseApiResponse = /** status 200 Successfully deleted a team knowledge base */ undefined +export type DeleteAplKnowledgeBaseApiArg = { + /** ID of team */ + teamId: string + /** Name of the knowledge base */ + knowledgeBaseName: string +} +export type GetAplAgentsApiResponse = /** status 200 Successfully obtained agents */ ({ + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +})[] +export type GetAplAgentsApiArg = { + /** ID of team */ + teamId: string +} +export type CreateAplAgentApiResponse = /** status 200 Successfully stored agent configuration */ { + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +} +export type CreateAplAgentApiArg = { + /** ID of team */ + teamId: string + /** Agent object */ + body: { + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } + } & { + metadata: { + name: string + } + } +} +export type GetAplAgentApiResponse = /** status 200 Successfully obtained agent configuration */ { + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +} +export type GetAplAgentApiArg = { + /** ID of team */ + teamId: string + /** Name of the agent */ + agentName: string +} +export type EditAplAgentApiResponse = /** status 200 Successfully edited a team agent */ { + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } +} & { + metadata: { + name: string + labels: { + 'apl.io/teamId': string + } + } +} & { + status: { + conditions?: { + lastTransitionTime?: string + message?: string + reason?: string + status?: boolean + type?: string + }[] + phase?: string + } +} +export type EditAplAgentApiArg = { + /** ID of team */ + teamId: string + /** Name of the agent */ + agentName: string + /** Agent object that contains updated values */ + body: { + kind: 'AkamaiAgent' + spec: { + knowledgeBase?: string + foundationModel: string + agentInstructions: string + } + } & { + metadata: { + name: string + } + } +} +export type DeleteAplAgentApiResponse = /** status 200 Successfully deleted a team agent */ undefined +export type DeleteAplAgentApiArg = { + /** ID of team */ + teamId: string + /** Name of the agent */ + agentName: string +} export const { useGetValuesQuery, useGetTeamsQuery, @@ -6368,4 +6731,15 @@ export const { useToggleAppsMutation, useGetAppQuery, useEditAppMutation, + useGetAiModelsQuery, + useGetAplKnowledgeBasesQuery, + useCreateAplKnowledgeBaseMutation, + useGetAplKnowledgeBaseQuery, + useEditAplKnowledgeBaseMutation, + useDeleteAplKnowledgeBaseMutation, + useGetAplAgentsQuery, + useCreateAplAgentMutation, + useGetAplAgentQuery, + useEditAplAgentMutation, + useDeleteAplAgentMutation, } = injectedRtkApi From 29bc35ac4d35f047c2b3530aff6f0918e7cee34f Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:56:59 +0200 Subject: [PATCH 02/20] fix: disable edit knowledge base name --- .../knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx b/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx index 96ee4d33..15e3b77c 100644 --- a/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx +++ b/src/pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage.tsx @@ -122,6 +122,7 @@ export default function KnowledgeBasesCreateEditPage({ }} error={!!errors.metadata?.name} helperText={errors.metadata?.name?.message?.toString()} + disabled={!!knowledgeBaseName} /> From 423fc062251be948a863b8bf8b320ef099c21743 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:09:59 +0200 Subject: [PATCH 03/20] feat: add agent pages --- public/assets/agents_icon.svg | 1 + public/i18n/en/common.json | 4 + src/App.tsx | 14 ++ src/components/NavConfig.tsx | 5 + src/components/forms/TextArea.tsx | 5 + .../create-edit/AgentsCreateEditPage.tsx | 223 ++++++++++++++++++ .../create-edit-agents.validator.ts | 21 ++ src/pages/agents/index.tsx | 7 + .../agents/overview/AgentsOverviewPage.tsx | 105 +++++++++ 9 files changed, 385 insertions(+) create mode 100644 public/assets/agents_icon.svg create mode 100644 src/pages/agents/create-edit/AgentsCreateEditPage.tsx create mode 100644 src/pages/agents/create-edit/create-edit-agents.validator.ts create mode 100644 src/pages/agents/index.tsx create mode 100644 src/pages/agents/overview/AgentsOverviewPage.tsx diff --git a/public/assets/agents_icon.svg b/public/assets/agents_icon.svg new file mode 100644 index 00000000..33621541 --- /dev/null +++ b/public/assets/agents_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/i18n/en/common.json b/public/i18n/en/common.json index eb6b6447..47b616a9 100644 --- a/public/i18n/en/common.json +++ b/public/i18n/en/common.json @@ -99,6 +99,10 @@ "Knowledge-base_plural": "Knowledge Bases", "TITLE_KNOWLEDGE_BASE": "Knowledge base details", "TITLE_KNOWLEDGE_BASES": "Knowledge bases - {{scope}}", + "Agent": "Agent", + "Agent_plural": "Agents", + "TITLE_AGENT": "Agent details", + "TITLE_AGENTS": "Agents - {{scope}}", "TITLE_NETWORK_POLICY": "Network policy details", "TITLE_NETWORK_POLICIES": "Network policies - {{scope}}" } diff --git a/src/App.tsx b/src/App.tsx index c3c96b90..e0e55f6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,8 @@ import CodeRepositoriesCreateEditPage from 'pages/code-repositories/create-edit/ import CodeRepositoriesOverviewPage from 'pages/code-repositories/overview/CodeRepositoriesOverviewPage' import KnowledgeBasesCreateEditPage from 'pages/knowledge-bases/create-edit/KnowledgeBasesCreateEditPage' import KnowledgeBasesOverviewPage from 'pages/knowledge-bases/overview/KnowledgeBasesOverviewPage' +import AgentsCreateEditPage from 'pages/agents/create-edit/AgentsCreateEditPage' +import AgentsOverviewPage from 'pages/agents/overview/AgentsOverviewPage' import NetworkPoliciesOverviewPage from 'pages/network-policies/overview/NetworkPoliciesOverviewPage' import NetworkPoliciesIngressCreateEditPage from 'pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage' import NetworkPoliciesEgressCreateEditPage from 'pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage' @@ -124,6 +126,18 @@ function App() { exact /> + + + + diff --git a/src/components/NavConfig.tsx b/src/components/NavConfig.tsx index f7a94ddf..0f5a644b 100644 --- a/src/components/NavConfig.tsx +++ b/src/components/NavConfig.tsx @@ -51,6 +51,11 @@ export default function NavConfig() { path: `/catalogs/${oboTeamId}`, icon: getIcon('developer_guide_icon.svg'), }, + { + title: 'Agents', + path: `/teams/${oboTeamId}/agents`, + icon: getIcon('agents_icon.svg'), + }, { title: 'Knowledge Bases', path: `/teams/${oboTeamId}/knowledge-bases`, diff --git a/src/components/forms/TextArea.tsx b/src/components/forms/TextArea.tsx index 7d6e41e2..854cadbf 100644 --- a/src/components/forms/TextArea.tsx +++ b/src/components/forms/TextArea.tsx @@ -168,6 +168,11 @@ export function AutoResizableTextarea({ adjustSize() }, [rest.value]) + // Sync internal value state with external value prop + useEffect(() => { + if (!showLock && rest.value !== value) setValue(rest.value) + }, [rest.value, showLock]) + return ( ): React.ReactElement { + const { t } = useTranslation() + + const [create, { isLoading: isLoadingCreate, isSuccess: isSuccessCreate }] = useCreateAplAgentMutation() + const [update, { isLoading: isLoadingUpdate, isSuccess: isSuccessUpdate }] = useEditAplAgentMutation() + const [del, { isLoading: isLoadingDelete, isSuccess: isSuccessDelete }] = useDeleteAplAgentMutation() + const { data, isLoading, isFetching, isError, refetch } = useGetAplAgentQuery( + { teamId, agentName }, + { skip: !agentName }, + ) + const { data: aiModels } = useGetAiModelsQuery() + const { data: knowledgeBases } = useGetAplKnowledgeBasesQuery({ teamId }) + + const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) + useEffect(() => { + if (isDirty !== false) return + if (!isFetching) refetch() + }, [isDirty]) + + type FormType = CreateAplAgentApiArg['body'] + + const defaultValues: FormType = { + kind: 'AkamaiAgent' as const, + metadata: { + name: '', + }, + spec: { + foundationModel: '', + knowledgeBase: '', + agentInstructions: '', + }, + } + + const methods = useForm({ + resolver: yupResolver(agentSchema) as unknown as Resolver, + defaultValues, + }) + + const { + register, + reset, + handleSubmit, + watch, + formState: { errors }, + setValue, + } = methods + + useEffect(() => { + if (data) reset(data) + }, [data, reset]) + + const onSubmit = (formData: FormType) => { + const body = { ...formData } + + if (agentName) update({ teamId, agentName, body }) + else create({ teamId, body }) + } + + const mutating = isLoadingCreate || isLoadingUpdate || isLoadingDelete + if (!mutating && (isSuccessCreate || isSuccessUpdate || isSuccessDelete)) + return + + const loading = isLoading + + if (loading) return + + return ( + + + + +
+
+ + { + const value = e.target.value + setValue('metadata.name', value) + }} + error={!!errors.metadata?.name} + helperText={errors.metadata?.name?.message?.toString()} + disabled={!!agentName} + /> + + + + + + model.spec.modelType === 'foundation') + .map((model) => model.metadata.name) || [] + } + getOptionLabel={(option) => { + const model = aiModels?.find((m) => m.metadata.name === option) + return model?.spec.displayName || option + }} + value={watch('spec.foundationModel') || null} + onChange={(_, value) => { + setValue('spec.foundationModel', value || '') + }} + errorText={errors.spec?.foundationModel?.message?.toString()} + helperText={errors.spec?.foundationModel?.message?.toString()} + /> + + + + + + kb.metadata.name) || []} + value={watch('spec.knowledgeBase') || null} + onChange={(_, value) => { + setValue('spec.knowledgeBase', value || '') + }} + errorText={errors.spec?.knowledgeBase?.message?.toString()} + helperText={errors.spec?.knowledgeBase?.message?.toString()} + /> + + + + + + { + const value = e.target.value + setValue('spec.agentInstructions', value) + }} + error={!!errors.spec?.agentInstructions} + value={watch('spec.agentInstructions') || ''} + /> + +
+ + {agentName && ( + del({ teamId, agentName })} + resourceName={watch('metadata.name')} + resourceType='agent' + data-cy='button-delete-agent' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} + /> + )} + + {agentName ? 'Save Changes' : 'Create Agent'} + + +
+
+
+ ) +} diff --git a/src/pages/agents/create-edit/create-edit-agents.validator.ts b/src/pages/agents/create-edit/create-edit-agents.validator.ts new file mode 100644 index 00000000..5a8f5e3b --- /dev/null +++ b/src/pages/agents/create-edit/create-edit-agents.validator.ts @@ -0,0 +1,21 @@ +import * as yup from 'yup' + +// Schema for Agent form validation +export const agentSchema = yup.object({ + kind: yup.string().oneOf(['AkamaiAgent']).required(), + metadata: yup.object({ + name: yup + .string() + .required('Agent name is required') + .min(2, 'Agent name must be at least 2 characters') + .matches( + /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, + 'Name must start and end with a lowercase letter or number, and can only contain lowercase letters, numbers, and hyphens', + ), + }), + spec: yup.object({ + foundationModel: yup.string().required('Please select a foundation model'), + knowledgeBase: yup.string().optional(), + agentInstructions: yup.string().optional(), + }), +}) diff --git a/src/pages/agents/index.tsx b/src/pages/agents/index.tsx new file mode 100644 index 00000000..7e029d6b --- /dev/null +++ b/src/pages/agents/index.tsx @@ -0,0 +1,7 @@ +import AgentsOverviewPage from './overview/AgentsOverviewPage' +import AgentsCreateEditPage from './create-edit/AgentsCreateEditPage' + +export default { + AgentsOverviewPage, + AgentsCreateEditPage, +} diff --git a/src/pages/agents/overview/AgentsOverviewPage.tsx b/src/pages/agents/overview/AgentsOverviewPage.tsx new file mode 100644 index 00000000..862764c6 --- /dev/null +++ b/src/pages/agents/overview/AgentsOverviewPage.tsx @@ -0,0 +1,105 @@ +import PaperLayout from 'layouts/Paper' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { RouteComponentProps } from 'react-router-dom' +import { getRole } from 'utils/data' +import { useGetAplAgentsQuery } from 'redux/otomiApi' +import { useAppSelector } from 'redux/hooks' +import { HeadCell } from '../../../components/EnhancedTable' +import RLink from '../../../components/Link' +import ListTable from '../../../components/ListTable' + +const getAgentName = (): CallableFunction => + function (row: any): string | React.ReactElement { + const { teamId, name }: { teamId: string; name: string } = row + const path = `/teams/${teamId}/agents/${encodeURIComponent(name)}` + return ( + + {name} + + ) + } + +const getStatus = (): CallableFunction => + function (row: any): string { + const { status } = row + return status?.phase || 'Unknown' + } + +const getRegion = (): CallableFunction => + function (row: any): string { + const { region } = row + return region || 'N/A' + } + +const getExposure = (): CallableFunction => + function (row: any): string { + const { exposure } = row + return exposure || 'N/A' + } + +interface Params { + teamId: string +} + +export default function AgentsOverviewPage({ + match: { + params: { teamId }, + }, +}: RouteComponentProps): React.ReactElement { + const { t } = useTranslation() + + const { data: agents, isLoading, isFetching, refetch } = useGetAplAgentsQuery({ teamId }) + + const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) + useEffect(() => { + if (isDirty !== false) return + if (!isFetching) refetch() + }, [isDirty]) + + // Transform API response to match table format + const transformedData = + agents?.map((agent) => ({ + name: agent.metadata.name, + teamId, + status: agent.status, + region: 'dummy region', // Replace with actual region if available in the API response + exposure: 'dummy exposure', // Replace with actual exposure if available in the API response + })) || [] + + const headCells: HeadCell[] = [ + { + id: 'name', + label: t('Name'), + renderer: getAgentName(), + }, + { + id: 'status', + label: t('Status'), + renderer: getStatus(), + }, + { + id: 'region', + label: t('Region'), + renderer: getRegion(), + }, + { + id: 'exposure', + label: t('Exposure'), + renderer: getExposure(), + }, + ] + + const customButtonText = () => Create Agent + + const comp = ( + + ) + return +} From bb351c2ef5b3e326d2c5aac8bdbfbaf49b57327a Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:55:52 +0200 Subject: [PATCH 04/20] feat: add agent playground --- src/components/AgentPlayground.tsx | 231 ++++++++++++++++++ .../create-edit/AgentsCreateEditPage.tsx | 2 + src/redux/otomiApi.ts | 33 +++ 3 files changed, 266 insertions(+) create mode 100644 src/components/AgentPlayground.tsx diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx new file mode 100644 index 00000000..a8848298 --- /dev/null +++ b/src/components/AgentPlayground.tsx @@ -0,0 +1,231 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Alert, Box, CircularProgress, IconButton, TextField, Typography } from '@mui/material' +import SendIcon from '@mui/icons-material/Send' +import DeleteIcon from '@mui/icons-material/Delete' +import { Paper } from './Paper' +import Iconify from './Iconify' + +interface Message { + role: 'user' | 'assistant' + content: string + id: string +} + +interface AgentPlaygroundProps { + teamId: string + agentName: string +} + +export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): React.ReactElement { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const messagesEndRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + + useEffect(() => { + scrollToBottom() + }, [messages]) + + const handleSend = async () => { + if (!input.trim() || loading) return + + const userMessage: Message = { role: 'user', content: input.trim(), id: `user-${Date.now()}` } + const newMessages = [...messages, userMessage] + setMessages(newMessages) + setInput('') + setLoading(true) + setError(null) + + try { + const response = await fetch(`/api/alpha/teams/${teamId}/agents/${agentName}/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), + stream: true, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + const errorMessage = errorData?.error?.message || `Error: ${response.status}` + throw new Error(typeof errorMessage === 'string' ? errorMessage : 'Unknown error') + } + + // Handle streaming response + const reader = response.body?.getReader() + const decoder = new TextDecoder() + const assistantId = `assistant-${Date.now()}` + + if (reader) { + // Add empty assistant message that we'll update + setMessages([...newMessages, { role: 'assistant', content: '', id: assistantId }]) + + // Process streaming response + const processStream = async () => { + let accumulatedContent = '' + // eslint-disable-next-line no-constant-condition + while (true) { + // eslint-disable-next-line no-await-in-loop + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n') + + // eslint-disable-next-line no-loop-func + lines.forEach((line) => { + if (line.startsWith('data: ')) { + const data = line.slice(6) + if (data === '[DONE]') return + + try { + const parsed = JSON.parse(data) + const content = parsed.choices?.[0]?.delta?.content || '' + if (content) { + accumulatedContent += content + const currentContent = accumulatedContent + setMessages((prev) => { + const updated = [...prev] + const lastIndex = updated.length - 1 + if (lastIndex >= 0 && updated[lastIndex].id === assistantId) + updated[lastIndex] = { ...updated[lastIndex], content: currentContent } + return updated + }) + } + } catch { + // Skip invalid JSON + } + } + }) + } + } + + await processStream() + } else { + // Non-streaming response + const data = await response.json() + const assistantContent = data.choices?.[0]?.message?.content || 'No response' + setMessages([...newMessages, { role: 'assistant', content: assistantContent, id: assistantId }]) + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to send message' + setError(errorMessage) + // eslint-disable-next-line no-console + console.error('Chat error:', err) + } finally { + setLoading(false) + } + } + + const handleClear = () => { + setMessages([]) + setError(null) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( + + + {/* Header */} + + Agent Playground + + + + + + {/* Messages Area */} + + {messages.length === 0 ? ( + + + + Ask your agent a question to start evaluating. + + + ) : ( + messages.map((message) => ( + + + + {message.role === 'user' ? 'You' : 'Agent'} + + + {message.content} + + + + )) + )} +
+ + + {/* Error Display */} + {error && ( + setError(null)}> + {error} + + )} + + {/* Input Area */} + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder='Type your message...' + disabled={loading} + variant='outlined' + size='small' + /> + + {loading ? : } + + + + + ) +} diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 18649ae7..be2b09b0 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -25,6 +25,7 @@ import Section from 'components/Section' import DeleteButton from 'components/DeleteButton' import { LoadingButton } from '@mui/lab' import { Divider } from 'components/Divider' +import { AgentPlayground } from 'components/AgentPlayground' import { agentSchema } from './create-edit-agents.validator' interface Params { @@ -217,6 +218,7 @@ export default function AgentsCreateEditPage({ + {agentName && } ) diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 5d7817f6..133f4710 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -577,6 +577,13 @@ const injectedRtkApi = api.injectEndpoints({ deleteAplAgent: build.mutation({ query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}`, method: 'DELETE' }), }), + sendAgentChat: build.mutation({ + query: (queryArg) => ({ + url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}/chat`, + method: 'POST', + body: queryArg.body, + }), + }), }), overrideExisting: false, }) @@ -6592,6 +6599,31 @@ export type DeleteAplAgentApiArg = { /** Name of the agent */ agentName: string } +export type SendAgentChatApiResponse = /** status 200 Successfully received chat response */ { + choices?: { + message?: { + role: 'assistant' + content: string + } + delta?: { + content?: string + } + }[] +} +export type SendAgentChatApiArg = { + /** ID of team */ + teamId: string + /** Name of the agent */ + agentName: string + /** Chat request with messages array */ + body: { + messages: { + role: 'system' | 'user' | 'assistant' + content: string + }[] + stream?: boolean + } +} export const { useGetValuesQuery, useGetTeamsQuery, @@ -6745,4 +6777,5 @@ export const { useGetAplAgentQuery, useEditAplAgentMutation, useDeleteAplAgentMutation, + useSendAgentChatMutation, } = injectedRtkApi From 281cd995dfe091192fb95560a82d1f9c96a2034c Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:25:40 +0200 Subject: [PATCH 05/20] test: agent playground --- src/components/AgentPlayground.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index a8848298..7c16f752 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -42,15 +42,21 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re setError(null) try { - const response = await fetch(`/api/alpha/teams/${teamId}/agents/${agentName}/chat`, { + // Call agent service directly using internal Kubernetes service + const agentServiceUrl = `http://${agentName}.team-${teamId}.svc.cluster.local:9099/v1/chat/completions` + + const requestBody = { + messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), + stream: true, + model: 'rag-pipeline', // Required by the agent + } + + const response = await fetch(agentServiceUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), - stream: true, - }), + body: JSON.stringify(requestBody), }) if (!response.ok) { From 9393dfe178fdaf00ae2ac54b0aa58c2ae734ba50 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:46:02 +0200 Subject: [PATCH 06/20] test: agent playground --- nginx/nginx.tmpl | 12 ++++++++++++ src/components/AgentPlayground.tsx | 9 +++++++-- src/setupProxy.js | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index e359f2da..ba5c76e2 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -35,6 +35,18 @@ http { location /v1/healthz { return 200 'OK'; } + location ~ ^/agent/([^/]+)/team-([^/]+)/(.*)$ { + proxy_pass http://$1.team-$2.svc.cluster.local:9099/$3$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Support for streaming responses + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + } location ~* \.(?:css|js|eot|woff|woff2|ttf|svg|otf) { # Enable GZip for static files gzip_static on; diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index 7c16f752..eebfcc24 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -42,8 +42,13 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re setError(null) try { - // Call agent service directly using internal Kubernetes service - const agentServiceUrl = `http://${agentName}.team-${teamId}.svc.cluster.local:9099/v1/chat/completions` + // Call agent service through nginx proxy + // In development: use /agent-direct proxy (port-forward to localhost:9100) + // In cluster: use /agent/{name}/team-{id} proxy (nginx routes to internal service) + const isDev = process.env.NODE_ENV === 'development' + const agentServiceUrl = isDev + ? `/agent-direct/v1/chat/completions` + : `/agent/${agentName}/team-${teamId}/v1/chat/completions` const requestBody = { messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), diff --git a/src/setupProxy.js b/src/setupProxy.js index aa8f95ec..42566fc9 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -9,6 +9,12 @@ module.exports = function (app) { logLevel: 'debug', pathRewrite: { '^/api/ws': '/ws' }, }), + // Direct proxy to agent services in Kubernetes + proxy('/agent-direct', { + target: 'http://localhost:9100', // Port-forward fc-agent service to this port + pathRewrite: { '^/agent-direct': '' }, + changeOrigin: true, + }), proxy('/api', { target: 'http://localhost:8080', pathRewrite: { '^/api/': '/' }, From 3673a4aae8a66863ca6e9e8d76861288f42972f1 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:23:47 +0200 Subject: [PATCH 07/20] test: agent playground --- nginx/nginx.tmpl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index ba5c76e2..96d8637b 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -36,7 +36,11 @@ http { return 200 'OK'; } location ~ ^/agent/([^/]+)/team-([^/]+)/(.*)$ { - proxy_pass http://$1.team-$2.svc.cluster.local:9099/$3$is_args$args; + set $agent_name $1; + set $team_id $2; + set $agent_path $3; + resolver kube-dns.kube-system.svc.cluster.local valid=10s; + proxy_pass http://$agent_name.team-$team_id.svc.cluster.local:9099/$agent_path$is_args$args; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; From 5eda3ceaaa63ce849f69fbff6476efb482073d36 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:56:27 +0200 Subject: [PATCH 08/20] test: agent playground --- nginx/nginx.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index 96d8637b..4b5a6202 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -39,7 +39,7 @@ http { set $agent_name $1; set $team_id $2; set $agent_path $3; - resolver kube-dns.kube-system.svc.cluster.local valid=10s; + resolver ${KUBE_DNS_SERVICE_HOST} valid=10s; proxy_pass http://$agent_name.team-$team_id.svc.cluster.local:9099/$agent_path$is_args$args; proxy_http_version 1.1; proxy_set_header Host $host; From 1e63b245794c9c5de198f1eb774680ff7beef0b6 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:10:43 +0200 Subject: [PATCH 09/20] test: agent playground --- nginx/nginx.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index 4b5a6202..063a1bb6 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -39,7 +39,7 @@ http { set $agent_name $1; set $team_id $2; set $agent_path $3; - resolver ${KUBE_DNS_SERVICE_HOST} valid=10s; + resolver local=on valid=10s; proxy_pass http://$agent_name.team-$team_id.svc.cluster.local:9099/$agent_path$is_args$args; proxy_http_version 1.1; proxy_set_header Host $host; From 728b45e69e04e01ca605c6e3add4aeec9218d8f6 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:21:34 +0200 Subject: [PATCH 10/20] test: agent playground --- nginx/nginx.tmpl | 2 +- nginx/run.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index 063a1bb6..4b5a6202 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -39,7 +39,7 @@ http { set $agent_name $1; set $team_id $2; set $agent_path $3; - resolver local=on valid=10s; + resolver ${KUBE_DNS_SERVICE_HOST} valid=10s; proxy_pass http://$agent_name.team-$team_id.svc.cluster.local:9099/$agent_path$is_args$args; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/nginx/run.sh b/nginx/run.sh index 538084e3..e64045ca 100755 --- a/nginx/run.sh +++ b/nginx/run.sh @@ -2,6 +2,9 @@ CONTEXT_PATH=${CONTEXT_PATH:-''} +# Ensure KUBE_DNS_SERVICE_HOST is available for envsubst +export KUBE_DNS_SERVICE_HOST + find build -type f -print0 | xargs -0 sed -i -e "s/##CONTEXT_PATH##/$CONTEXT_PATH/g" envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" /etc/nginx/nginx.conf From 832fe0c628483843fc9531b51ec5c6d664965d69 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:26:44 +0200 Subject: [PATCH 11/20] test: agent playground --- nginx/run.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nginx/run.sh b/nginx/run.sh index e64045ca..7c29c466 100755 --- a/nginx/run.sh +++ b/nginx/run.sh @@ -2,11 +2,9 @@ CONTEXT_PATH=${CONTEXT_PATH:-''} -# Ensure KUBE_DNS_SERVICE_HOST is available for envsubst -export KUBE_DNS_SERVICE_HOST - find build -type f -print0 | xargs -0 sed -i -e "s/##CONTEXT_PATH##/$CONTEXT_PATH/g" -envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" /etc/nginx/nginx.conf +# Generate nginx.conf with proper DNS resolver +sed "s/\${KUBE_DNS_SERVICE_HOST}/$KUBE_DNS_SERVICE_HOST/g" /app/nginx.tmpl > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' From 9ecbdcc05a5d8fb72581652dc31c890446f385ab Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:34:06 +0200 Subject: [PATCH 12/20] test: agent playground --- nginx/run.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nginx/run.sh b/nginx/run.sh index 7c29c466..09b9be2c 100755 --- a/nginx/run.sh +++ b/nginx/run.sh @@ -4,7 +4,10 @@ CONTEXT_PATH=${CONTEXT_PATH:-''} find build -type f -print0 | xargs -0 sed -i -e "s/##CONTEXT_PATH##/$CONTEXT_PATH/g" +# Get DNS nameserver from /etc/resolv.conf or use KUBE_DNS_SERVICE_HOST if available +DNS_SERVER=${KUBE_DNS_SERVICE_HOST:-$(grep -m1 '^nameserver' /etc/resolv.conf | awk '{print $2}')} + # Generate nginx.conf with proper DNS resolver -sed "s/\${KUBE_DNS_SERVICE_HOST}/$KUBE_DNS_SERVICE_HOST/g" /app/nginx.tmpl > /etc/nginx/nginx.conf +sed "s/\${KUBE_DNS_SERVICE_HOST}/$DNS_SERVER/g" /app/nginx.tmpl > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' From f9d73881c80d7643e178efda4662cd2ff7907aed Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:25:20 +0200 Subject: [PATCH 13/20] test: agent playground --- nginx/run.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nginx/run.sh b/nginx/run.sh index 09b9be2c..e802c172 100755 --- a/nginx/run.sh +++ b/nginx/run.sh @@ -4,10 +4,10 @@ CONTEXT_PATH=${CONTEXT_PATH:-''} find build -type f -print0 | xargs -0 sed -i -e "s/##CONTEXT_PATH##/$CONTEXT_PATH/g" -# Get DNS nameserver from /etc/resolv.conf or use KUBE_DNS_SERVICE_HOST if available -DNS_SERVER=${KUBE_DNS_SERVICE_HOST:-$(grep -m1 '^nameserver' /etc/resolv.conf | awk '{print $2}')} +# Extract DNS nameserver for nginx resolver (fallback to resolv.conf if env var not set) +DNS_SERVER=${KUBE_DNS_SERVICE_HOST:-$(awk '/^nameserver/{print $2; exit}' /etc/resolv.conf)} -# Generate nginx.conf with proper DNS resolver +# Generate nginx.conf with DNS resolver substituted sed "s/\${KUBE_DNS_SERVICE_HOST}/$DNS_SERVER/g" /app/nginx.tmpl > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' From 15500378abf69f8f27151f00cba3490243faa0b4 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:22:27 +0200 Subject: [PATCH 14/20] feat: update agent playground --- src/components/AgentPlayground.tsx | 111 +++++++++++++++--- .../agents/overview/AgentsOverviewPage.tsx | 26 ++-- .../overview/KnowledgeBasesOverviewPage.tsx | 12 -- src/setupProxy.js | 7 +- 4 files changed, 103 insertions(+), 53 deletions(-) diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index eebfcc24..da16421f 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -1,10 +1,20 @@ import React, { useEffect, useRef, useState } from 'react' -import { Alert, Box, CircularProgress, IconButton, TextField, Typography } from '@mui/material' +import { Alert, Box, IconButton, TextField, Typography, keyframes } from '@mui/material' import SendIcon from '@mui/icons-material/Send' import DeleteIcon from '@mui/icons-material/Delete' +import StopIcon from '@mui/icons-material/StopCircle' import { Paper } from './Paper' import Iconify from './Iconify' +const thinkingAnimation = keyframes` + 0%, 60%, 100% { + opacity: 0.3; + } + 30% { + opacity: 1; + } +` + interface Message { role: 'user' | 'assistant' content: string @@ -22,38 +32,61 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const messagesEndRef = useRef(null) + const messagesContainerRef = useRef(null) + const abortControllerRef = useRef(null) const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + if (messagesContainerRef.current) messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight } useEffect(() => { scrollToBottom() }, [messages]) + const handleStop = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + abortControllerRef.current = null + setLoading(false) + // Remove the empty assistant message if it exists + setMessages((prev) => { + const lastMessage = prev[prev.length - 1] + if (lastMessage?.role === 'assistant' && !lastMessage.content) return prev.slice(0, -1) + + return prev + }) + } + } + const handleSend = async () => { if (!input.trim() || loading) return const userMessage: Message = { role: 'user', content: input.trim(), id: `user-${Date.now()}` } + const assistantId = `assistant-${Date.now()}` const newMessages = [...messages, userMessage] - setMessages(newMessages) + + // Immediately add user message and empty assistant message for thinking animation + setMessages([...newMessages, { role: 'assistant', content: '', id: assistantId }]) setInput('') setLoading(true) setError(null) + // Create new abort controller for this request + abortControllerRef.current = new AbortController() + try { - // Call agent service through nginx proxy - // In development: use /agent-direct proxy (port-forward to localhost:9100) + // Call agent service through nginx proxy to handle http and mixed-content issues + // In development: use /agent proxy (port-forward to localhost:9100) // In cluster: use /agent/{name}/team-{id} proxy (nginx routes to internal service) const isDev = process.env.NODE_ENV === 'development' const agentServiceUrl = isDev - ? `/agent-direct/v1/chat/completions` + ? `/agent/v1/chat/completions` : `/agent/${agentName}/team-${teamId}/v1/chat/completions` const requestBody = { messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), stream: true, - model: 'rag-pipeline', // Required by the agent + model: 'rag-pipeline', } const response = await fetch(agentServiceUrl, { @@ -62,6 +95,7 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), + signal: abortControllerRef.current.signal, }) if (!response.ok) { @@ -73,12 +107,8 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re // Handle streaming response const reader = response.body?.getReader() const decoder = new TextDecoder() - const assistantId = `assistant-${Date.now()}` if (reader) { - // Add empty assistant message that we'll update - setMessages([...newMessages, { role: 'assistant', content: '', id: assistantId }]) - // Process streaming response const processStream = async () => { let accumulatedContent = '' @@ -127,12 +157,18 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re setMessages([...newMessages, { role: 'assistant', content: assistantContent, id: assistantId }]) } } catch (err) { + // Don't show error if request was aborted by user + if (err instanceof Error && err.name === 'AbortError') { + // Request was cancelled, don't show error + return + } const errorMessage = err instanceof Error ? err.message : 'Failed to send message' setError(errorMessage) // eslint-disable-next-line no-console console.error('Chat error:', err) } finally { setLoading(false) + abortControllerRef.current = null } } @@ -161,6 +197,7 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re {/* Messages Area */} {message.role === 'user' ? 'You' : 'Agent'} - - {message.content} - + {message.role === 'assistant' && !message.content && loading ? ( + + + + + + ) : ( + + {message.content} + + )} )) @@ -219,7 +288,7 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re )} {/* Input Area */} - + - - {loading ? : } - + {loading ? ( + + + + ) : ( + + + + )} diff --git a/src/pages/agents/overview/AgentsOverviewPage.tsx b/src/pages/agents/overview/AgentsOverviewPage.tsx index 862764c6..915cc62a 100644 --- a/src/pages/agents/overview/AgentsOverviewPage.tsx +++ b/src/pages/agents/overview/AgentsOverviewPage.tsx @@ -26,16 +26,10 @@ const getStatus = (): CallableFunction => return status?.phase || 'Unknown' } -const getRegion = (): CallableFunction => +const getFoundationModel = (): CallableFunction => function (row: any): string { - const { region } = row - return region || 'N/A' - } - -const getExposure = (): CallableFunction => - function (row: any): string { - const { exposure } = row - return exposure || 'N/A' + const { foundationModel } = row + return foundationModel || 'N/A' } interface Params { @@ -63,8 +57,7 @@ export default function AgentsOverviewPage({ name: agent.metadata.name, teamId, status: agent.status, - region: 'dummy region', // Replace with actual region if available in the API response - exposure: 'dummy exposure', // Replace with actual exposure if available in the API response + foundationModel: agent.spec.foundationModel, })) || [] const headCells: HeadCell[] = [ @@ -79,14 +72,9 @@ export default function AgentsOverviewPage({ renderer: getStatus(), }, { - id: 'region', - label: t('Region'), - renderer: getRegion(), - }, - { - id: 'exposure', - label: t('Exposure'), - renderer: getExposure(), + id: 'foundationModel', + label: t('Foundation Model'), + renderer: getFoundationModel(), }, ] diff --git a/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx b/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx index a642a40c..9ec4e3d4 100644 --- a/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx +++ b/src/pages/knowledge-bases/overview/KnowledgeBasesOverviewPage.tsx @@ -26,12 +26,6 @@ const getStatus = (): CallableFunction => return status?.phase || 'Unknown' } -const getDataSource = (): CallableFunction => - function (row: any): string | React.ReactElement { - const { sourceUrl }: { sourceUrl: string } = row - return sourceUrl || 'N/A' - } - const getEmbeddingModel = (): CallableFunction => function (row: any): string { const { modelName } = row @@ -63,7 +57,6 @@ export default function KnowledgeBasesOverviewPage({ name: kb.metadata.name, teamId, status: kb.status, - sourceUrl: kb.spec.sourceUrl, modelName: kb.spec.modelName, })) || [] @@ -78,11 +71,6 @@ export default function KnowledgeBasesOverviewPage({ label: t('Status'), renderer: getStatus(), }, - { - id: 'datasource', - label: t('Data Source'), - renderer: getDataSource(), - }, { id: 'embeddingModel', label: t('Embedding Model'), diff --git a/src/setupProxy.js b/src/setupProxy.js index 42566fc9..bf3d4312 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -9,10 +9,9 @@ module.exports = function (app) { logLevel: 'debug', pathRewrite: { '^/api/ws': '/ws' }, }), - // Direct proxy to agent services in Kubernetes - proxy('/agent-direct', { - target: 'http://localhost:9100', // Port-forward fc-agent service to this port - pathRewrite: { '^/agent-direct': '' }, + proxy('/agent', { + target: 'http://localhost:9099', + pathRewrite: { '^/agent': '' }, changeOrigin: true, }), proxy('/api', { From 2ead6355171efca2fa98d88d4ed1b1468adcf6a0 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:06:12 +0200 Subject: [PATCH 15/20] feat: improve agent playground with markdown and thinking animation --- src/components/AgentPlayground.tsx | 88 ++++++++++++++++++------------ src/components/Markdown.tsx | 67 +++++++++++++++++++---- 2 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index da16421f..e2126d2d 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -3,6 +3,7 @@ import { Alert, Box, IconButton, TextField, Typography, keyframes } from '@mui/m import SendIcon from '@mui/icons-material/Send' import DeleteIcon from '@mui/icons-material/Delete' import StopIcon from '@mui/icons-material/StopCircle' +import Markdown from './Markdown' import { Paper } from './Paper' import Iconify from './Iconify' @@ -238,41 +239,58 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re {message.role === 'user' ? 'You' : 'Agent'} - {message.role === 'assistant' && !message.content && loading ? ( - - - - - - ) : ( - - {message.content} - - )} + {(() => { + // Show thinking animation for empty assistant message while loading + if (message.role === 'assistant' && !message.content && loading) { + return ( + + + + + + ) + } + + // Render assistant message with markdown + if (message.role === 'assistant') { + return ( + + ) + } + + return ( + + {message.content} + + ) + })()} )) diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 0e651c5c..345dcc4a 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -1,4 +1,7 @@ -import { Card, styled } from '@mui/material' +import React, { useState } from 'react' +import { Box, Card, IconButton, Tooltip, styled } from '@mui/material' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import CheckIcon from '@mui/icons-material/Check' import MarkdownJSX from 'markdown-to-jsx' // Higher-order component to generate markdown components @@ -30,15 +33,57 @@ const Code = createMDComp('code', { backgroundColor: '#6e768164', textWrap: 'pretty', }) -const Pre = createMDComp('pre', { - ...mb, - backgroundColor: '#6e768164', - padding: '12px', - borderRadius: '6px', - '& > code': { - backgroundColor: 'transparent', - }, -}) + +// Code block with copy button +function CodeBlock({ children }: any) { + const [copied, setCopied] = useState(false) + + const handleCopy = () => { + const codeText = children?.props?.children || children + const textToCopy = typeof codeText === 'string' ? codeText : String(codeText) + navigator.clipboard.writeText(textToCopy.trim()) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + code': { + backgroundColor: 'transparent', + }, + }} + > + {children} + + + + {copied ? : } + + + + ) +} const Ol = createMDComp('ol', { ...mb, paddingLeft: '32px' }) const Li = createMDComp('li', { ...lh }) const Table = createMDComp('table', { ...mb, borderCollapse: 'collapse' }) @@ -77,7 +122,7 @@ export default function Markdown({ readme, sx }: Props) { component: Code, }, pre: { - component: Pre, + component: CodeBlock, }, li: { component: Li, From cc7ae7ad78b963a453c4d220f8fe84ec9ecda4fa Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:08:15 +0200 Subject: [PATCH 16/20] fix: update agent pages --- src/components/AgentPlayground.tsx | 12 +++---- src/components/Setting.tsx | 1 + .../create-edit-agents.validator.ts | 2 +- src/redux/otomiApi.ts | 33 ------------------- 4 files changed, 6 insertions(+), 42 deletions(-) diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index e2126d2d..417c82e1 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -77,7 +77,7 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re try { // Call agent service through nginx proxy to handle http and mixed-content issues - // In development: use /agent proxy (port-forward to localhost:9100) + // In development: use /agent proxy (port-forward to localhost:9099) // In cluster: use /agent/{name}/team-{id} proxy (nginx routes to internal service) const isDev = process.env.NODE_ENV === 'development' const agentServiceUrl = isDev @@ -143,7 +143,7 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re }) } } catch { - // Skip invalid JSON + // Ignore JSON parse errors for malformed chunks } } }) @@ -159,14 +159,10 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re } } catch (err) { // Don't show error if request was aborted by user - if (err instanceof Error && err.name === 'AbortError') { - // Request was cancelled, don't show error - return - } + if (err instanceof Error && err.name === 'AbortError') return + const errorMessage = err instanceof Error ? err.message : 'Failed to send message' setError(errorMessage) - // eslint-disable-next-line no-console - console.error('Chat error:', err) } finally { setLoading(false) abortControllerRef.current = null diff --git a/src/components/Setting.tsx b/src/components/Setting.tsx index 1c3f158d..ed5e4f90 100644 --- a/src/components/Setting.tsx +++ b/src/components/Setting.tsx @@ -67,6 +67,7 @@ export const getSettingUiSchema = (settings: GetSettingsInfoApiResponse, setting isPreInstalled: { 'ui:widget': 'hidden' }, adminPassword: { 'ui:widget': 'hidden' }, useORCS: { 'ui:widget': 'hidden' }, + aiEnabled: { 'ui:widget': 'hidden' }, }, kms: { sops: { diff --git a/src/pages/agents/create-edit/create-edit-agents.validator.ts b/src/pages/agents/create-edit/create-edit-agents.validator.ts index 5a8f5e3b..44a6c15f 100644 --- a/src/pages/agents/create-edit/create-edit-agents.validator.ts +++ b/src/pages/agents/create-edit/create-edit-agents.validator.ts @@ -16,6 +16,6 @@ export const agentSchema = yup.object({ spec: yup.object({ foundationModel: yup.string().required('Please select a foundation model'), knowledgeBase: yup.string().optional(), - agentInstructions: yup.string().optional(), + agentInstructions: yup.string().required('Please enter agent instructions'), }), }) diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 133f4710..5d7817f6 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -577,13 +577,6 @@ const injectedRtkApi = api.injectEndpoints({ deleteAplAgent: build.mutation({ query: (queryArg) => ({ url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}`, method: 'DELETE' }), }), - sendAgentChat: build.mutation({ - query: (queryArg) => ({ - url: `/alpha/teams/${queryArg.teamId}/agents/${queryArg.agentName}/chat`, - method: 'POST', - body: queryArg.body, - }), - }), }), overrideExisting: false, }) @@ -6599,31 +6592,6 @@ export type DeleteAplAgentApiArg = { /** Name of the agent */ agentName: string } -export type SendAgentChatApiResponse = /** status 200 Successfully received chat response */ { - choices?: { - message?: { - role: 'assistant' - content: string - } - delta?: { - content?: string - } - }[] -} -export type SendAgentChatApiArg = { - /** ID of team */ - teamId: string - /** Name of the agent */ - agentName: string - /** Chat request with messages array */ - body: { - messages: { - role: 'system' | 'user' | 'assistant' - content: string - }[] - stream?: boolean - } -} export const { useGetValuesQuery, useGetTeamsQuery, @@ -6777,5 +6745,4 @@ export const { useGetAplAgentQuery, useEditAplAgentMutation, useDeleteAplAgentMutation, - useSendAgentChatMutation, } = injectedRtkApi From e5cde912e9f8683d6f1fe05be1aa4e5794c6ff63 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:38:24 +0200 Subject: [PATCH 17/20] feat: update agent page using tools array instead of knowledge base field --- .../create-edit/AgentsCreateEditPage.tsx | 32 ++++++++++---- src/redux/otomiApi.ts | 42 ++++++++++++++++--- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index be2b09b0..9de7a893 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -4,7 +4,7 @@ import { Autocomplete } from 'components/forms/Autocomplete' import { AutoResizableTextarea } from 'components/forms/TextArea' import { LandingHeader } from 'components/LandingHeader' import PaperLayout from 'layouts/Paper' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { FormProvider, Resolver, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { Redirect, RouteComponentProps } from 'react-router-dom' @@ -39,6 +39,7 @@ export default function AgentsCreateEditPage({ }, }: RouteComponentProps): React.ReactElement { const { t } = useTranslation() + const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState('') const [create, { isLoading: isLoadingCreate, isSuccess: isSuccessCreate }] = useCreateAplAgentMutation() const [update, { isLoading: isLoadingUpdate, isSuccess: isSuccessUpdate }] = useEditAplAgentMutation() @@ -65,8 +66,8 @@ export default function AgentsCreateEditPage({ }, spec: { foundationModel: '', - knowledgeBase: '', agentInstructions: '', + tools: [], }, } @@ -85,12 +86,29 @@ export default function AgentsCreateEditPage({ } = methods useEffect(() => { - if (data) reset(data) + if (data) { + reset(data) + const kbTool = data.spec.tools?.find((tool) => tool.type === 'knowledgeBase') + setSelectedKnowledgeBase((kbTool?.name as string) || '') + } }, [data, reset]) const onSubmit = (formData: FormType) => { const body = { ...formData } + // Transform knowledge base selection into tools array + const tools = body.spec.tools || [] + const nonKbTools = tools.filter((tool) => tool.type !== 'knowledgeBase') + + if (selectedKnowledgeBase) { + nonKbTools.push({ + type: 'knowledgeBase', + name: selectedKnowledgeBase, + }) + } + + body.spec.tools = nonKbTools + if (agentName) update({ teamId, agentName, body }) else create({ teamId, body }) } @@ -160,16 +178,14 @@ export default function AgentsCreateEditPage({ kb.metadata.name) || []} - value={watch('spec.knowledgeBase') || null} + value={selectedKnowledgeBase} onChange={(_, value) => { - setValue('spec.knowledgeBase', value || '') + setSelectedKnowledgeBase(value) }} - errorText={errors.spec?.knowledgeBase?.message?.toString()} - helperText={errors.spec?.knowledgeBase?.message?.toString()} /> diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 5d7817f6..5c538b64 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -6438,9 +6438,14 @@ export type DeleteAplKnowledgeBaseApiArg = { export type GetAplAgentsApiResponse = /** status 200 Successfully obtained agents */ ({ kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { @@ -6468,9 +6473,14 @@ export type GetAplAgentsApiArg = { export type CreateAplAgentApiResponse = /** status 200 Successfully stored agent configuration */ { kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { @@ -6498,9 +6508,14 @@ export type CreateAplAgentApiArg = { body: { kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { @@ -6511,9 +6526,14 @@ export type CreateAplAgentApiArg = { export type GetAplAgentApiResponse = /** status 200 Successfully obtained agent configuration */ { kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { @@ -6543,9 +6563,14 @@ export type GetAplAgentApiArg = { export type EditAplAgentApiResponse = /** status 200 Successfully edited a team agent */ { kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { @@ -6575,9 +6600,14 @@ export type EditAplAgentApiArg = { body: { kind: 'AkamaiAgent' spec: { - knowledgeBase?: string foundationModel: string agentInstructions: string + tools?: { + type: string + name: string + description?: string + endpoint?: string + }[] } } & { metadata: { From eb276ae42bfa85f4a53e4b78e7005604838989fa Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:47:49 +0200 Subject: [PATCH 18/20] fix: lint error --- src/pages/agents/create-edit/AgentsCreateEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 9de7a893..42241e6f 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -89,7 +89,7 @@ export default function AgentsCreateEditPage({ if (data) { reset(data) const kbTool = data.spec.tools?.find((tool) => tool.type === 'knowledgeBase') - setSelectedKnowledgeBase((kbTool?.name as string) || '') + setSelectedKnowledgeBase(kbTool?.name || '') } }, [data, reset]) From a5b754d5d8208175ba91d3f4556891cf448570e0 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:21:24 +0200 Subject: [PATCH 19/20] fix: agent page knowledge base field --- .../create-edit/AgentsCreateEditPage.tsx | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 42241e6f..80116f6b 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -4,7 +4,7 @@ import { Autocomplete } from 'components/forms/Autocomplete' import { AutoResizableTextarea } from 'components/forms/TextArea' import { LandingHeader } from 'components/LandingHeader' import PaperLayout from 'layouts/Paper' -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { FormProvider, Resolver, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { Redirect, RouteComponentProps } from 'react-router-dom' @@ -39,7 +39,6 @@ export default function AgentsCreateEditPage({ }, }: RouteComponentProps): React.ReactElement { const { t } = useTranslation() - const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState('') const [create, { isLoading: isLoadingCreate, isSuccess: isSuccessCreate }] = useCreateAplAgentMutation() const [update, { isLoading: isLoadingUpdate, isSuccess: isSuccessUpdate }] = useEditAplAgentMutation() @@ -86,29 +85,12 @@ export default function AgentsCreateEditPage({ } = methods useEffect(() => { - if (data) { - reset(data) - const kbTool = data.spec.tools?.find((tool) => tool.type === 'knowledgeBase') - setSelectedKnowledgeBase(kbTool?.name || '') - } + if (data) reset(data) }, [data, reset]) const onSubmit = (formData: FormType) => { const body = { ...formData } - // Transform knowledge base selection into tools array - const tools = body.spec.tools || [] - const nonKbTools = tools.filter((tool) => tool.type !== 'knowledgeBase') - - if (selectedKnowledgeBase) { - nonKbTools.push({ - type: 'knowledgeBase', - name: selectedKnowledgeBase, - }) - } - - body.spec.tools = nonKbTools - if (agentName) update({ teamId, agentName, body }) else create({ teamId, body }) } @@ -182,9 +164,12 @@ export default function AgentsCreateEditPage({ width='large' placeholder='Select a knowledge base' options={knowledgeBases?.map((kb) => kb.metadata.name) || []} - value={selectedKnowledgeBase} + value={watch('spec.tools')?.find((tool) => tool.type === 'knowledgeBase')?.name || ''} onChange={(_, value) => { - setSelectedKnowledgeBase(value) + const currentTools = watch('spec.tools') || [] + const nonKbTools = currentTools.filter((tool) => tool.type !== 'knowledgeBase') + const updatedTools = value ? [...nonKbTools, { type: 'knowledgeBase', name: value }] : nonKbTools + setValue('spec.tools', updatedTools) }} /> From 6ee4b6551eaa79abbcc166ff19826a781fe81370 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:29:48 +0200 Subject: [PATCH 20/20] fix: agent chat endpoint --- nginx/nginx.tmpl | 6 +++--- src/components/AgentPlayground.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nginx/nginx.tmpl b/nginx/nginx.tmpl index 4b5a6202..2f748773 100644 --- a/nginx/nginx.tmpl +++ b/nginx/nginx.tmpl @@ -35,9 +35,9 @@ http { location /v1/healthz { return 200 'OK'; } - location ~ ^/agent/([^/]+)/team-([^/]+)/(.*)$ { - set $agent_name $1; - set $team_id $2; + location ~ ^/teams/([^/]+)/agents/([^/]+)/(.*)$ { + set $team_id $1; + set $agent_name $2; set $agent_path $3; resolver ${KUBE_DNS_SERVICE_HOST} valid=10s; proxy_pass http://$agent_name.team-$team_id.svc.cluster.local:9099/$agent_path$is_args$args; diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx index 417c82e1..a0895c9a 100644 --- a/src/components/AgentPlayground.tsx +++ b/src/components/AgentPlayground.tsx @@ -78,11 +78,11 @@ export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): Re try { // Call agent service through nginx proxy to handle http and mixed-content issues // In development: use /agent proxy (port-forward to localhost:9099) - // In cluster: use /agent/{name}/team-{id} proxy (nginx routes to internal service) + // In cluster: use /teams/{teamId}/agents/{agentName} proxy (nginx routes to internal service) const isDev = process.env.NODE_ENV === 'development' const agentServiceUrl = isDev ? `/agent/v1/chat/completions` - : `/agent/${agentName}/team-${teamId}/v1/chat/completions` + : `/teams/${teamId}/agents/${agentName}/v1/chat/completions` const requestBody = { messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })),