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 (
+
+
+
+
+
+
+
+
+ )
+}
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 (
+
+
+
+
+
+
+
+
+ )
+}
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 })),