From 48e09e87eb3fe34a5beb27ae48d57e753fbf6384 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Tue, 14 Oct 2025 11:57:35 +0200 Subject: [PATCH 01/14] fix: add foundation model endpoint --- src/ai/AkamaiAgentCR.test.ts | 9 ++-- src/ai/AkamaiAgentCR.ts | 81 +++++++++++++++++++++++++++--------- src/openapi/agent.yaml | 47 +++++++++++++++++---- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/ai/AkamaiAgentCR.test.ts b/src/ai/AkamaiAgentCR.test.ts index 633854d66..1da021bdb 100644 --- a/src/ai/AkamaiAgentCR.test.ts +++ b/src/ai/AkamaiAgentCR.test.ts @@ -60,7 +60,6 @@ describe('AkamaiAgentCR', () => { name: 'test-kb', description: 'Search the test-kb knowledge base for relevant information. Use this when you need factual information, documentation, or specific details stored in the knowledge base.', - endpoint: undefined, }, ]) }) @@ -111,7 +110,6 @@ describe('AkamaiAgentCR', () => { type: 'knowledgeBase', name: 'test-kb', description: 'Custom description for the knowledge base', - endpoint: undefined, }, ]) }) @@ -154,7 +152,6 @@ describe('AkamaiAgentCR', () => { name: 'test-kb', description: 'Search the test-kb knowledge base for relevant information. Use this when you need factual information, documentation, or specific details stored in the knowledge base.', - endpoint: undefined, }, ], }, @@ -186,7 +183,7 @@ describe('AkamaiAgentCR', () => { expect(response.spec.tools).toBeUndefined() }) - test('should preserve custom description and endpoint in response', () => { + test('should preserve custom description and apiUrl in response', () => { const requestWithDetails = { ...mockAgentRequest, spec: { @@ -196,7 +193,7 @@ describe('AkamaiAgentCR', () => { type: 'knowledgeBase', name: 'test-kb', description: 'Custom KB description', - endpoint: 'https://api.example.com/kb', + apiUrl: 'https://api.example.com/kb', }, ], }, @@ -210,7 +207,7 @@ describe('AkamaiAgentCR', () => { type: 'knowledgeBase', name: 'test-kb', description: 'Custom KB description', - endpoint: 'https://api.example.com/kb', + apiUrl: 'https://api.example.com/kb', }, ]) }) diff --git a/src/ai/AkamaiAgentCR.ts b/src/ai/AkamaiAgentCR.ts index 38c35b5a6..b8838c17e 100644 --- a/src/ai/AkamaiAgentCR.ts +++ b/src/ai/AkamaiAgentCR.ts @@ -18,12 +18,20 @@ export class AkamaiAgentCR { } public spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: Array<{ + agent: string + condition: string + apiUrl: string + apiKey?: string + }> tools?: Array<{ type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }> } @@ -42,17 +50,29 @@ export class AkamaiAgentCR { } this.spec = { foundationModel: request.spec.foundationModel, + ...(request.spec.foundationModelEndpoint && { foundationModelEndpoint: request.spec.foundationModelEndpoint }), agentInstructions: request.spec.agentInstructions, - tools: request.spec.tools?.map((tool) => ({ - type: tool.type, - name: tool.name, - description: - tool.description || - (tool.type === 'knowledgeBase' - ? `Search the ${tool.name} knowledge base for relevant information. Use this when you need factual information, documentation, or specific details stored in the knowledge base.` - : undefined), - endpoint: tool.endpoint, - })), + ...(request.spec.routes && { + routes: request.spec.routes.map((route) => ({ + agent: route.agent, + condition: route.condition, + apiUrl: route.apiUrl, + ...(route.apiKey && { apiKey: route.apiKey }), + })), + }), + ...(request.spec.tools && { + tools: request.spec.tools.map((tool) => ({ + type: tool.type, + name: tool.name, + description: + tool.description || + (tool.type === 'knowledgeBase' + ? `Search the ${tool.name} knowledge base for relevant information. Use this when you need factual information, documentation, or specific details stored in the knowledge base.` + : undefined), + ...(tool.apiUrl && { apiUrl: tool.apiUrl }), + ...(tool.apiKey && { apiKey: tool.apiKey }), + })), + }), } } @@ -79,13 +99,25 @@ export class AkamaiAgentCR { }, spec: { foundationModel: this.spec.foundationModel, + ...(this.spec.foundationModelEndpoint && { foundationModelEndpoint: this.spec.foundationModelEndpoint }), agentInstructions: this.spec.agentInstructions, - tools: this.spec.tools?.map((tool) => ({ - type: tool.type, - name: tool.name, - ...(tool.description && { description: tool.description }), - ...(tool.endpoint && { endpoint: tool.endpoint }), - })), + ...(this.spec.routes && { + routes: this.spec.routes.map((route) => ({ + agent: route.agent, + condition: route.condition, + apiUrl: route.apiUrl, + ...(route.apiKey && { apiKey: route.apiKey }), + })), + }), + ...(this.spec.tools && { + tools: this.spec.tools.map((tool) => ({ + type: tool.type, + name: tool.name, + ...(tool.description && { description: tool.description }), + ...(tool.apiUrl && { apiUrl: tool.apiUrl }), + ...(tool.apiKey && { apiKey: tool.apiKey }), + })), + }), }, status: { conditions: [ @@ -103,15 +135,24 @@ export class AkamaiAgentCR { // Static factory method static async create(teamId: string, agentName: string, request: AplAgentRequest): Promise { const aiModels = await getAIModels() - const embeddingModel = aiModels.find( + const foundationModel = aiModels.find( (model) => model.metadata.name === request.spec.foundationModel && model.spec.modelType === 'foundation', ) - if (!embeddingModel) { + if (!foundationModel) { throw new K8sResourceNotFound('Foundation model', `Foundation model '${request.spec.foundationModel}' not found`) } - return new AkamaiAgentCR(teamId, agentName, request) + // Create enriched request with foundationModelEndpoint from the model + const enrichedRequest: AplAgentRequest = { + ...request, + spec: { + ...request.spec, + foundationModelEndpoint: foundationModel.spec.modelEndpoint, + }, + } + + return new AkamaiAgentCR(teamId, agentName, enrichedRequest) } // Static method to create from existing CR (for transformation) diff --git a/src/openapi/agent.yaml b/src/openapi/agent.yaml index 55e11c97e..118962a4d 100644 --- a/src/openapi/agent.yaml +++ b/src/openapi/agent.yaml @@ -22,11 +22,40 @@ AplAgentSpec: foundationModel: type: string description: Name of the foundation model - example: "meta-llama-3" + example: "llama3-1" + foundationModelEndpoint: + type: string + description: HTTP endpoint URL for the foundation model + example: "http://llama-3-1-8b-predictor.team-admin.svc.cluster.local/openai/v1" agentInstructions: type: string description: Custom instructions for the agent - example: "You are a helpful assistant that provides concise answers." + example: "You are a helpful assistant for App Platform for LKE documentation. Give clear answers to the users." + routes: + type: array + description: Routing configuration to other agents based on conditions + items: + type: object + properties: + agent: + type: string + description: Name of the agent to route to + example: "kubernetes-expert" + condition: + type: string + description: Condition that triggers routing to this agent + example: "If the question is about Kubernetes" + apiUrl: + type: string + description: API URL of the target agent + example: "https://my-other-agent.lke496760.akamai-apl.net" + apiKey: + type: string + description: API key for authenticating with the target agent + required: + - agent + - condition + - apiUrl tools: type: array description: Tools available to the agent @@ -35,19 +64,23 @@ AplAgentSpec: properties: type: type: string - description: Type of the tool + description: Type of the tool (knowledgeBase, mcpServer, subWorkflow, function) example: "knowledgeBase" name: type: string description: Name of the tool resource - example: "company-docs" + example: "apl-techdocs" description: type: string description: Description of what the tool does - example: "Search the company-docs knowledge base for relevant information" - endpoint: + example: "Search the apl-techdocs knowledge base for relevant information. Use this when you need factual information, documentation, or specific details stored in the knowledge base." + apiUrl: + type: string + description: API URL for the tool (for mcpServer and subWorkflow types) + example: "https://my-mcp.com" + apiKey: type: string - description: Optional endpoint URL for the tool + description: API key for authenticating with the tool (for mcpServer and subWorkflow types) required: - type - name From d68a8350e8c324aa7d98b706ceeac8e3b85aa227 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Mon, 20 Oct 2025 09:56:55 +0200 Subject: [PATCH 02/14] fix: get correct endpointName --- src/ai/aiModelHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/aiModelHandler.ts b/src/ai/aiModelHandler.ts index 25bc133a5..b5db4cd61 100644 --- a/src/ai/aiModelHandler.ts +++ b/src/ai/aiModelHandler.ts @@ -16,7 +16,7 @@ export function transformK8sDeploymentToAplAIModel(deployment: V1Deployment): Ap const labels = deployment.metadata?.labels || {} const modelName = labels.modelName || deployment.metadata?.name || '' const modelNameTitle = labels.modelNameTitle || deployment.metadata?.name || '' - const endpointName = labels.app || deployment.metadata?.name || '' + const endpointName = labels['serving.knative.dev/service'] || deployment.metadata?.name || '' // Convert K8s deployment conditions to schema format const conditions = getConditions(deployment) From 81e5761b687b809d5d6eca03f108524701d39458 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Mon, 20 Oct 2025 16:32:32 +0200 Subject: [PATCH 03/14] feat: fetch statefullsets for ai models --- src/ai/aiModelHandler.test.ts | 49 +++++++++++++++++++++-------------- src/ai/aiModelHandler.ts | 41 +++++++++++++++++------------ src/ai/k8s.ts | 24 ++++++++++++++++- 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/ai/aiModelHandler.test.ts b/src/ai/aiModelHandler.test.ts index 8e74c9d7f..9f07d0bf1 100644 --- a/src/ai/aiModelHandler.test.ts +++ b/src/ai/aiModelHandler.test.ts @@ -1,5 +1,5 @@ import { V1Deployment } from '@kubernetes/client-node' -import { getAIModels, transformK8sDeploymentToAplAIModel } from './aiModelHandler' +import { getAIModels, transformK8sWorkloadToAplAIModel } from './aiModelHandler' import * as k8s from './k8s' // Mock the k8s module @@ -7,6 +7,9 @@ jest.mock('./k8s') const mockedGetDeploymentsWithAIModelLabels = k8s.getDeploymentsWithAIModelLabels as jest.MockedFunction< typeof k8s.getDeploymentsWithAIModelLabels > +const mockedGetStatefulSetsWithAIModelLabels = k8s.getStatefulSetsWithAIModelLabels as jest.MockedFunction< + typeof k8s.getStatefulSetsWithAIModelLabels +> describe('aiModelHandler', () => { const mockDeployment: V1Deployment = { @@ -47,9 +50,9 @@ describe('aiModelHandler', () => { jest.clearAllMocks() }) - describe('transformK8sDeploymentToAplAIModel', () => { + describe('transformK8sWorkloadToAplAIModel', () => { test('should transform K8s deployment to AplAIModel with all fields', () => { - const result = transformK8sDeploymentToAplAIModel(mockDeployment) + const result = transformK8sWorkloadToAplAIModel(mockDeployment) expect(result).toEqual({ kind: 'AplAIModel', @@ -57,8 +60,8 @@ describe('aiModelHandler', () => { name: 'gpt-4', }, spec: { - displayName: 'GPT-4o-mini', - modelEndpoint: 'http://gpt-4.ai-models.svc.cluster.local/openai/v1', + displayName: 'gpt-4', + modelEndpoint: 'http://gpt-4-deployment.ai-models.svc.cluster.local/openai/v1', modelType: 'foundation', modelDimension: 1536, }, @@ -97,10 +100,10 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithModelName) + const result = transformK8sWorkloadToAplAIModel(deploymentWithModelName) expect(result.metadata.name).toBe('custom-model-name') - expect(result.spec.displayName).toBe('GPT-4o-mini') + expect(result.spec.displayName).toBe('custom-model-name') }) test('should use modelName from labels when deployment name is missing', () => { @@ -116,10 +119,10 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutName) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutName) expect(result.metadata.name).toBe('custom-model-name') - expect(result.spec.displayName).toBe('GPT-4o-mini') + expect(result.spec.displayName).toBe('custom-model-name') }) test('should handle deployment without labels', () => { @@ -131,7 +134,7 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutLabels) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutLabels) expect(result.metadata.name).toBe('test-deployment') expect(result.spec.modelType).toBeUndefined() @@ -150,7 +153,7 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutDimension) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutDimension) expect(result.spec.modelDimension).toBeUndefined() }) @@ -164,9 +167,9 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutNamespace) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutNamespace) - expect(result.spec.modelEndpoint).toBe('http://gpt-4.undefined.svc.cluster.local/openai/v1') + expect(result.spec.modelEndpoint).toBe('http://test-deployment.undefined.svc.cluster.local/openai/v1') }) test('should handle deployment without status conditions', () => { @@ -178,7 +181,7 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutConditions) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutConditions) expect(result.status.conditions).toEqual([]) expect(result.status.phase).toBe('NotReady') @@ -193,13 +196,13 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(notReadyDeployment) + const result = transformK8sWorkloadToAplAIModel(notReadyDeployment) expect(result.status.phase).toBe('NotReady') }) test('should set phase to Ready when has ready replicas', () => { - const result = transformK8sDeploymentToAplAIModel(mockDeployment) + const result = transformK8sWorkloadToAplAIModel(mockDeployment) expect(result.status.phase).toBe('Ready') }) @@ -221,7 +224,7 @@ describe('aiModelHandler', () => { }, } - const result = transformK8sDeploymentToAplAIModel(deploymentWithFalseCondition) + const result = transformK8sWorkloadToAplAIModel(deploymentWithFalseCondition) expect(result.status.conditions?.[0]?.status).toBe(false) }) @@ -231,7 +234,7 @@ describe('aiModelHandler', () => { status: mockDeployment.status, } as V1Deployment - const result = transformK8sDeploymentToAplAIModel(deploymentWithoutMetadata) + const result = transformK8sWorkloadToAplAIModel(deploymentWithoutMetadata) expect(result.metadata.name).toBe('') expect(result.spec.modelEndpoint).toBe('http://.undefined.svc.cluster.local/openai/v1') @@ -241,6 +244,7 @@ describe('aiModelHandler', () => { describe('getAIModels', () => { test('should return transformed AI models from deployments', async () => { mockedGetDeploymentsWithAIModelLabels.mockResolvedValue([mockDeployment]) + mockedGetStatefulSetsWithAIModelLabels.mockResolvedValue([]) const result = await getAIModels() @@ -248,18 +252,21 @@ describe('aiModelHandler', () => { expect(result[0].kind).toBe('AplAIModel') expect(result[0].metadata.name).toBe('gpt-4') expect(mockedGetDeploymentsWithAIModelLabels).toHaveBeenCalledTimes(1) + expect(mockedGetStatefulSetsWithAIModelLabels).toHaveBeenCalledTimes(1) }) - test('should return empty array when no deployments found', async () => { + test('should return empty array when no deployments or statefulsets found', async () => { mockedGetDeploymentsWithAIModelLabels.mockResolvedValue([]) + mockedGetStatefulSetsWithAIModelLabels.mockResolvedValue([]) const result = await getAIModels() expect(result).toEqual([]) expect(mockedGetDeploymentsWithAIModelLabels).toHaveBeenCalledTimes(1) + expect(mockedGetStatefulSetsWithAIModelLabels).toHaveBeenCalledTimes(1) }) - test('should handle multiple deployments', async () => { + test('should handle multiple deployments and statefulsets', async () => { const secondDeployment = { ...mockDeployment, metadata: { @@ -274,6 +281,7 @@ describe('aiModelHandler', () => { } mockedGetDeploymentsWithAIModelLabels.mockResolvedValue([mockDeployment, secondDeployment]) + mockedGetStatefulSetsWithAIModelLabels.mockResolvedValue([]) const result = await getAIModels() @@ -285,6 +293,7 @@ describe('aiModelHandler', () => { test('should propagate errors from k8s module', async () => { const error = new Error('K8s API error') mockedGetDeploymentsWithAIModelLabels.mockRejectedValue(error) + mockedGetStatefulSetsWithAIModelLabels.mockResolvedValue([]) await expect(getAIModels()).rejects.toThrow('K8s API error') }) diff --git a/src/ai/aiModelHandler.ts b/src/ai/aiModelHandler.ts index b5db4cd61..d5ed7525a 100644 --- a/src/ai/aiModelHandler.ts +++ b/src/ai/aiModelHandler.ts @@ -1,9 +1,11 @@ -import { V1Deployment } from '@kubernetes/client-node' +import { V1Deployment, V1StatefulSet } from '@kubernetes/client-node' import { AplAIModelResponse } from 'src/otomi-models' -import { getDeploymentsWithAIModelLabels } from './k8s' +import { getDeploymentsWithAIModelLabels, getStatefulSetsWithAIModelLabels } from './k8s' -function getConditions(deployment: V1Deployment) { - return (deployment.status?.conditions || []).map((condition) => ({ +type K8sWorkload = V1Deployment | V1StatefulSet + +function getConditions(workload: K8sWorkload) { + return (workload.status?.conditions || []).map((condition) => ({ lastTransitionTime: condition.lastTransitionTime?.toISOString(), message: condition.message, reason: condition.reason, @@ -12,14 +14,16 @@ function getConditions(deployment: V1Deployment) { })) } -export function transformK8sDeploymentToAplAIModel(deployment: V1Deployment): AplAIModelResponse { - const labels = deployment.metadata?.labels || {} - const modelName = labels.modelName || deployment.metadata?.name || '' - const modelNameTitle = labels.modelNameTitle || deployment.metadata?.name || '' - const endpointName = labels['serving.knative.dev/service'] || deployment.metadata?.name || '' +export function transformK8sWorkloadToAplAIModel(workload: K8sWorkload): AplAIModelResponse { + const labels = workload.metadata?.labels || {} + const modelName = labels.modelName || workload.metadata?.name || '' + const endpointName = labels['serving.knative.dev/service'] || workload.metadata?.name || '' + + // Use /openai/v1 for Knative services, /v1 for regular deployments + const endpointPath = labels['serving.knative.dev/service'] ? '/openai/v1' : '/v1' - // Convert K8s deployment conditions to schema format - const conditions = getConditions(deployment) + // Convert K8s workload conditions to schema format + const conditions = getConditions(workload) return { kind: 'AplAIModel', @@ -27,19 +31,24 @@ export function transformK8sDeploymentToAplAIModel(deployment: V1Deployment): Ap name: modelName, }, spec: { - displayName: modelNameTitle, - modelEndpoint: `http://${endpointName}.${deployment.metadata?.namespace}.svc.cluster.local/openai/v1`, + displayName: modelName, + modelEndpoint: `http://${endpointName}.${workload.metadata?.namespace}.svc.cluster.local${endpointPath}`, modelType: labels.modelType as 'foundation' | 'embedding', ...(labels.modelDimension && { modelDimension: parseInt(labels.modelDimension, 10) }), }, status: { conditions, - phase: deployment.status?.readyReplicas && deployment.status.readyReplicas > 0 ? 'Ready' : 'NotReady', + phase: workload.status?.readyReplicas && workload.status.readyReplicas > 0 ? 'Ready' : 'NotReady', }, } } export async function getAIModels(): Promise { - const deployments = await getDeploymentsWithAIModelLabels() - return deployments.map(transformK8sDeploymentToAplAIModel) + const [deployments, statefulSets] = await Promise.all([ + getDeploymentsWithAIModelLabels(), + getStatefulSetsWithAIModelLabels(), + ]) + + const allWorkloads: K8sWorkload[] = [...deployments, ...statefulSets] + return allWorkloads.map(transformK8sWorkloadToAplAIModel) } diff --git a/src/ai/k8s.ts b/src/ai/k8s.ts index 0a5de010b..cd70ea3b4 100644 --- a/src/ai/k8s.ts +++ b/src/ai/k8s.ts @@ -1,4 +1,11 @@ -import { AppsV1Api, CustomObjectsApi, KubeConfig, KubernetesObject, V1Deployment } from '@kubernetes/client-node' +import { + AppsV1Api, + CustomObjectsApi, + KubeConfig, + KubernetesObject, + V1Deployment, + V1StatefulSet, +} from '@kubernetes/client-node' import Debug from 'debug' import { KubernetesListObject } from '@kubernetes/client-node/dist/types' @@ -50,6 +57,21 @@ export async function getDeploymentsWithAIModelLabels(): Promise } } +export async function getStatefulSetsWithAIModelLabels(): Promise { + const appsApi = getAppsApiClient() + + try { + const labelSelector = 'modelType,modelName' + const result = await appsApi.listStatefulSetForAllNamespaces({ labelSelector }) + + debug(`Found ${result.items.length} AI model statefulsets`) + return result.items + } catch (e) { + debug('Error fetching statefulsets from Kubernetes:', e) + return [] + } +} + export async function getKnowledgeBaseCNPGClusters(): Promise { const customObjectsApi = getCustomObjectsApiClient() From 6b63d7d4f436dc9d7ff0362e7a3502b7b20e0aa9 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Tue, 21 Oct 2025 09:17:50 +0200 Subject: [PATCH 04/14] fix: tests --- src/ai/aiModelHandler.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ai/aiModelHandler.test.ts b/src/ai/aiModelHandler.test.ts index 9f07d0bf1..23bd89ea3 100644 --- a/src/ai/aiModelHandler.test.ts +++ b/src/ai/aiModelHandler.test.ts @@ -22,6 +22,7 @@ describe('aiModelHandler', () => { modelNameTitle: 'GPT-4o-mini', modelType: 'foundation', modelDimension: '1536', + 'serving.knative.dev/service': 'gpt-4-deployment', }, }, status: { @@ -169,7 +170,7 @@ describe('aiModelHandler', () => { const result = transformK8sWorkloadToAplAIModel(deploymentWithoutNamespace) - expect(result.spec.modelEndpoint).toBe('http://test-deployment.undefined.svc.cluster.local/openai/v1') + expect(result.spec.modelEndpoint).toBe('http://gpt-4-deployment.undefined.svc.cluster.local/openai/v1') }) test('should handle deployment without status conditions', () => { @@ -237,7 +238,7 @@ describe('aiModelHandler', () => { const result = transformK8sWorkloadToAplAIModel(deploymentWithoutMetadata) expect(result.metadata.name).toBe('') - expect(result.spec.modelEndpoint).toBe('http://.undefined.svc.cluster.local/openai/v1') + expect(result.spec.modelEndpoint).toBe('http://.undefined.svc.cluster.local/v1') }) }) From 05b7dacfaad003957961445b6c4696c0bc32fb4e Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Wed, 22 Oct 2025 09:12:23 +0200 Subject: [PATCH 05/14] fix: editAplAgent --- src/otomi-stack.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 7462223bc..c71abb571 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -2490,7 +2490,9 @@ export default class OtomiStack { }, spec: { foundationModel: data.spec?.foundationModel ?? existingAgent.spec.foundationModel, + foundationModelEndpoint: data.spec?.foundationModelEndpoint ?? existingAgent.spec.foundationModelEndpoint, agentInstructions: data.spec?.agentInstructions ?? existingAgent.spec.agentInstructions, + routes: (data.spec?.routes ?? existingAgent.spec.routes) as typeof existingAgent.spec.routes, tools: (data.spec?.tools ?? existingAgent.spec.tools) as typeof existingAgent.spec.tools, }, }) From 5b5303d0aecc33cd7b1279e0418fbf7d3e443392 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Thu, 23 Oct 2025 15:01:57 +0200 Subject: [PATCH 06/14] fix: prefix embedding model with nvidea --- src/ai/AkamaiKnowledgeBaseCR.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/AkamaiKnowledgeBaseCR.ts b/src/ai/AkamaiKnowledgeBaseCR.ts index 85d6d14c6..58e5c7b2d 100644 --- a/src/ai/AkamaiKnowledgeBaseCR.ts +++ b/src/ai/AkamaiKnowledgeBaseCR.ts @@ -63,7 +63,7 @@ export class AkamaiKnowledgeBaseCR { pipelineParameters: { url: request.spec.sourceUrl, table_name: knowledgeBaseName, - embedding_model: embeddingModel.metadata.name, + embedding_model: `nvidia/${embeddingModel.metadata.name}`, embedding_api_base: embeddingModel.spec.modelEndpoint, embed_dim: embeddingModel.spec.modelDimension || env.EMBED_DIM_DEFAULT, embed_batch_size: env.EMBED_BATCH_SIZE, From 8d7ec93d1ea9356d6943dadfefd400e398d94ed7 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Thu, 23 Oct 2025 15:47:46 +0200 Subject: [PATCH 07/14] fix: tests --- src/ai/AkamaiKnowledgeBaseCR.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ai/AkamaiKnowledgeBaseCR.test.ts b/src/ai/AkamaiKnowledgeBaseCR.test.ts index 8ab62479d..316dd08e0 100644 --- a/src/ai/AkamaiKnowledgeBaseCR.test.ts +++ b/src/ai/AkamaiKnowledgeBaseCR.test.ts @@ -68,7 +68,7 @@ describe('AkamaiKnowledgeBaseCR', () => { expect(kbCR.spec.pipelineParameters).toEqual({ url: 'https://docs.example.com', table_name: 'test-kb', - embedding_model: 'text-embedding-ada-002', + embedding_model: 'nvidia/text-embedding-ada-002', embedding_api_base: 'http://embedding-model.ai.svc.cluster.local', embed_dim: 1536, embed_batch_size: expect.any(Number), @@ -133,7 +133,7 @@ describe('AkamaiKnowledgeBaseCR', () => { expect(response.kind).toBe('AkamaiKnowledgeBase') expect(response.metadata.name).toBe('test-kb') expect(response.metadata.labels?.['apl.io/teamId']).toBe('team-123') - expect(response.spec.modelName).toBe('text-embedding-ada-002') + expect(response.spec.modelName).toBe('nvidia/text-embedding-ada-002') expect(response.spec.sourceUrl).toBe('https://docs.example.com') expect(response.status).toEqual({}) }) @@ -216,7 +216,7 @@ describe('AkamaiKnowledgeBaseCR', () => { const result = await AkamaiKnowledgeBaseCR.create('team-123', 'test-kb', 'cluster-name', mockKnowledgeBaseRequest) expect(result).toBeInstanceOf(AkamaiKnowledgeBaseCR) - expect(result.spec.pipelineParameters.embedding_model).toBe('text-embedding-ada-002') + expect(result.spec.pipelineParameters.embedding_model).toBe('nvidia/text-embedding-ada-002') }) test('should handle AI models fetch error', async () => { From 19af23683f9b4561677515ddad140a165afaf923 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Fri, 24 Oct 2025 07:57:07 +0200 Subject: [PATCH 08/14] feat: fetch CR from cluster --- src/ai/AkamaiAgentCR.ts | 13 ++++- src/ai/AkamaiKnowledgeBaseCR.ts | 6 +-- src/ai/k8s.ts | 86 +++++++++++++++++++++++++++++++++ src/api.authz.test.ts | 12 ++--- src/openapi/agent.yaml | 12 +++++ src/otomi-stack.ts | 62 ++++++++++++++++++++---- 6 files changed, 171 insertions(+), 20 deletions(-) diff --git a/src/ai/AkamaiAgentCR.ts b/src/ai/AkamaiAgentCR.ts index b8838c17e..c5e9d1b48 100644 --- a/src/ai/AkamaiAgentCR.ts +++ b/src/ai/AkamaiAgentCR.ts @@ -19,6 +19,9 @@ export class AkamaiAgentCR { public spec: { foundationModel: string foundationModelEndpoint?: string + temperature?: string + topP?: string + maxTokens?: string agentInstructions: string routes?: Array<{ agent: string @@ -51,6 +54,9 @@ export class AkamaiAgentCR { this.spec = { foundationModel: request.spec.foundationModel, ...(request.spec.foundationModelEndpoint && { foundationModelEndpoint: request.spec.foundationModelEndpoint }), + ...(request.spec.temperature && { temperature: request.spec.temperature }), + ...(request.spec.topP && { topP: request.spec.topP }), + ...(request.spec.maxTokens && { maxTokens: request.spec.maxTokens }), agentInstructions: request.spec.agentInstructions, ...(request.spec.routes && { routes: request.spec.routes.map((route) => ({ @@ -87,7 +93,7 @@ export class AkamaiAgentCR { } // Transform to API response format - toApiResponse(teamId: string): AplAgentResponse { + toApiResponse(teamId: string, status?: any): AplAgentResponse { return { kind: 'AkamaiAgent', metadata: { @@ -100,6 +106,9 @@ export class AkamaiAgentCR { spec: { foundationModel: this.spec.foundationModel, ...(this.spec.foundationModelEndpoint && { foundationModelEndpoint: this.spec.foundationModelEndpoint }), + ...(this.spec.temperature && { temperature: this.spec.temperature }), + ...(this.spec.topP && { topP: this.spec.topP }), + ...(this.spec.maxTokens && { maxTokens: this.spec.maxTokens }), agentInstructions: this.spec.agentInstructions, ...(this.spec.routes && { routes: this.spec.routes.map((route) => ({ @@ -119,7 +128,7 @@ export class AkamaiAgentCR { })), }), }, - status: { + status: status || { conditions: [ { type: 'AgentDeployed', diff --git a/src/ai/AkamaiKnowledgeBaseCR.ts b/src/ai/AkamaiKnowledgeBaseCR.ts index 58e5c7b2d..5f0c646d3 100644 --- a/src/ai/AkamaiKnowledgeBaseCR.ts +++ b/src/ai/AkamaiKnowledgeBaseCR.ts @@ -63,7 +63,7 @@ export class AkamaiKnowledgeBaseCR { pipelineParameters: { url: request.spec.sourceUrl, table_name: knowledgeBaseName, - embedding_model: `nvidia/${embeddingModel.metadata.name}`, + embedding_model: embeddingModel.metadata.name, embedding_api_base: embeddingModel.spec.modelEndpoint, embed_dim: embeddingModel.spec.modelDimension || env.EMBED_DIM_DEFAULT, embed_batch_size: env.EMBED_BATCH_SIZE, @@ -84,7 +84,7 @@ export class AkamaiKnowledgeBaseCR { } // Transform to API response format - toApiResponse(teamId: string): AplKnowledgeBaseResponse { + toApiResponse(teamId: string, status?: any): AplKnowledgeBaseResponse { return { kind: env.KNOWLEDGE_BASE_KIND as 'AkamaiKnowledgeBase', metadata: { @@ -97,7 +97,7 @@ export class AkamaiKnowledgeBaseCR { modelName: this.spec.pipelineParameters.embedding_model, sourceUrl: this.spec.pipelineParameters.url, }, - status: {}, + status: status || {}, } } diff --git a/src/ai/k8s.ts b/src/ai/k8s.ts index cd70ea3b4..4b46e282b 100644 --- a/src/ai/k8s.ts +++ b/src/ai/k8s.ts @@ -92,3 +92,89 @@ export async function getKnowledgeBaseCNPGClusters(): Promise { + const customObjectsApi = getCustomObjectsApiClient() + + try { + const result = (await customObjectsApi.getNamespacedCustomObject({ + group: 'akamai.io', + version: 'v1alpha1', + namespace, + plural: 'akamaiagents', + name, + })) as KubernetesObject + + debug(`Found AkamaiAgent CR: ${namespace}/${name}`) + return result + } catch (e: any) { + if (e.statusCode === 404) { + debug(`AkamaiAgent CR not found: ${namespace}/${name}`) + return null + } + debug(`Error fetching AkamaiAgent CR ${namespace}/${name}:`, e) + throw e + } +} + +export async function getAkamaiKnowledgeBaseCR(namespace: string, name: string): Promise { + const customObjectsApi = getCustomObjectsApiClient() + + try { + const result = (await customObjectsApi.getNamespacedCustomObject({ + group: 'akamai.io', + version: 'v1alpha1', + namespace, + plural: 'akamaiknowledgebases', + name, + })) as KubernetesObject + + debug(`Found AkamaiKnowledgeBase CR: ${namespace}/${name}`) + return result + } catch (e: any) { + if (e.statusCode === 404) { + debug(`AkamaiKnowledgeBase CR not found: ${namespace}/${name}`) + return null + } + debug(`Error fetching AkamaiKnowledgeBase CR ${namespace}/${name}:`, e) + throw e + } +} + +export async function listAkamaiAgentCRs(namespace: string): Promise { + const customObjectsApi = getCustomObjectsApiClient() + + try { + const result = (await customObjectsApi.listNamespacedCustomObject({ + group: 'akamai.io', + version: 'v1alpha1', + namespace, + plural: 'akamaiagents', + })) as KubernetesListObject + + debug(`Found ${result.items.length} AkamaiAgent CRs in namespace ${namespace}`) + return result.items + } catch (e: any) { + debug(`Error listing AkamaiAgent CRs in ${namespace}:`, e) + return [] + } +} + +export async function listAkamaiKnowledgeBaseCRs(namespace: string): Promise { + const customObjectsApi = getCustomObjectsApiClient() + + try { + const result = (await customObjectsApi.listNamespacedCustomObject({ + group: 'akamai.io', + version: 'v1alpha1', + namespace, + plural: 'akamaiknowledgebases', + })) as KubernetesListObject + + debug(`Found ${result.items.length} AkamaiKnowledgeBase CRs in namespace ${namespace}`) + return result.items + } catch (e: any) { + debug(`Error listing AkamaiKnowledgeBase CRs in ${namespace}:`, e) + return [] + } +} diff --git a/src/api.authz.test.ts b/src/api.authz.test.ts index 4d9aea212..549f05b8c 100644 --- a/src/api.authz.test.ts +++ b/src/api.authz.test.ts @@ -841,7 +841,7 @@ describe('API authz tests', () => { }) test('platform admin can get knowledge bases', async () => { - jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockResolvedValue([]) await agent .get('/alpha/teams/team1/kb') .set('Authorization', `Bearer ${platformAdminToken}`) @@ -850,7 +850,7 @@ describe('API authz tests', () => { }) test('team admin can get knowledge bases', async () => { - jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockResolvedValue([]) await agent .get('/alpha/teams/team1/kb') .set('Authorization', `Bearer ${teamAdminToken}`) @@ -859,7 +859,7 @@ describe('API authz tests', () => { }) test('team member can get knowledge bases', async () => { - jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplKnowledgeBases').mockResolvedValue([]) await agent .get('/alpha/teams/team1/kb') .set('Authorization', `Bearer ${teamMemberToken}`) @@ -1002,7 +1002,7 @@ describe('API authz tests', () => { }) test('platform admin can get agents', async () => { - jest.spyOn(otomiStack, 'getAplAgents').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplAgents').mockResolvedValue([]) await agent .get('/alpha/teams/team1/agents') .set('Authorization', `Bearer ${platformAdminToken}`) @@ -1011,7 +1011,7 @@ describe('API authz tests', () => { }) test('team admin can get agents', async () => { - jest.spyOn(otomiStack, 'getAplAgents').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplAgents').mockResolvedValue([]) await agent .get('/alpha/teams/team1/agents') .set('Authorization', `Bearer ${teamAdminToken}`) @@ -1020,7 +1020,7 @@ describe('API authz tests', () => { }) test('team member can get agents', async () => { - jest.spyOn(otomiStack, 'getAplAgents').mockReturnValue([]) + jest.spyOn(otomiStack, 'getAplAgents').mockResolvedValue([]) await agent .get('/alpha/teams/team1/agents') .set('Authorization', `Bearer ${teamMemberToken}`) diff --git a/src/openapi/agent.yaml b/src/openapi/agent.yaml index 118962a4d..2350d45c7 100644 --- a/src/openapi/agent.yaml +++ b/src/openapi/agent.yaml @@ -27,6 +27,18 @@ AplAgentSpec: type: string description: HTTP endpoint URL for the foundation model example: "http://llama-3-1-8b-predictor.team-admin.svc.cluster.local/openai/v1" + temperature: + type: string + description: Temperature parameter for model inference (controls randomness) + example: "0.7" + topP: + type: string + description: Top-p parameter for model inference (nucleus sampling) + example: "0.9" + maxTokens: + type: string + description: Maximum number of tokens to generate + example: "2048" agentInstructions: type: string description: Custom instructions for the agent diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index c71abb571..84965332f 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -8,7 +8,15 @@ import { readdir, readFile, writeFile } from 'fs/promises' import { generate as generatePassword } from 'generate-password' import { cloneDeep, filter, isEmpty, map, mapValues, merge, omit, pick, set, unset } from 'lodash' import { getAppList, getAppSchema, getSpec } from 'src/app' -import { AlreadyExists, ForbiddenError, HttpError, OtomiError, PublicUrlExists, ValidationError } from 'src/error' +import { + AlreadyExists, + ForbiddenError, + HttpError, + NotExistError, + OtomiError, + PublicUrlExists, + ValidationError, +} from 'src/error' import getRepo, { getWorktreeRepo, Git } from 'src/git' import { cleanSession, getSessionStack } from 'src/middleware' import { @@ -125,6 +133,12 @@ import { getAIModels } from './ai/aiModelHandler' import { AkamaiKnowledgeBaseCR } from './ai/AkamaiKnowledgeBaseCR' import { AkamaiAgentCR } from './ai/AkamaiAgentCR' import { DatabaseCR } from './ai/DatabaseCR' +import { + getAkamaiAgentCR, + getAkamaiKnowledgeBaseCR, + listAkamaiAgentCRs, + listAkamaiKnowledgeBaseCRs, +} from './ai/k8s' interface ExcludedApp extends App { managed: boolean @@ -2422,12 +2436,27 @@ export default class OtomiStack { } async getAplKnowledgeBase(teamId: string, name: string): Promise { - const knowledgeBase = this.repoService.getTeamConfigService(teamId).getKnowledgeBase(name) - return knowledgeBase + // Fetch directly from Kubernetes cluster + const namespace = `team-${teamId}` + const cr = await getAkamaiKnowledgeBaseCR(namespace, name) + + if (!cr) { + throw new NotExistError(`KnowledgeBase[${name}] does not exist.`) + } + + const kbCR = AkamaiKnowledgeBaseCR.fromCR(cr) + return kbCR.toApiResponse(teamId, (cr as any).status) } - getAplKnowledgeBases(teamId: string): AplKnowledgeBaseResponse[] { - return this.repoService.getTeamConfigService(teamId).getKnowledgeBases() + async getAplKnowledgeBases(teamId: string): Promise { + // Fetch directly from Kubernetes cluster + const namespace = `team-${teamId}` + const crs = await listAkamaiKnowledgeBaseCRs(namespace) + + return crs.map((cr) => { + const kbCR = AkamaiKnowledgeBaseCR.fromCR(cr) + return kbCR.toApiResponse(teamId, (cr as any).status) + }) } getAllAplKnowledgeBases(): AplKnowledgeBaseResponse[] { @@ -2523,12 +2552,27 @@ export default class OtomiStack { } async getAplAgent(teamId: string, name: string): Promise { - const agent = this.repoService.getTeamConfigService(teamId).getAgent(name) - return agent + // Fetch directly from Kubernetes cluster + const namespace = `team-${teamId}` + const cr = await getAkamaiAgentCR(namespace, name) + + if (!cr) { + throw new NotExistError(`Agent[${name}] does not exist.`) + } + + const agentCR = AkamaiAgentCR.fromCR(cr) + return agentCR.toApiResponse(teamId, (cr as any).status) } - getAplAgents(teamId: string): AplAgentResponse[] { - return this.repoService.getTeamConfigService(teamId).getAgents() + async getAplAgents(teamId: string): Promise { + // Fetch directly from Kubernetes cluster + const namespace = `team-${teamId}` + const crs = await listAkamaiAgentCRs(namespace) + + return crs.map((cr) => { + const agentCR = AkamaiAgentCR.fromCR(cr) + return agentCR.toApiResponse(teamId, (cr as any).status) + }) } private async saveTeamAgent(teamId: string, agent: AplAgentResponse): Promise { From 42374416dbcf5e85af26b542da70576e542bf161 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Fri, 24 Oct 2025 08:22:16 +0200 Subject: [PATCH 09/14] feat: fetch CR from cluster --- src/otomi-stack.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 84965332f..fea09d9d8 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -133,12 +133,7 @@ import { getAIModels } from './ai/aiModelHandler' import { AkamaiKnowledgeBaseCR } from './ai/AkamaiKnowledgeBaseCR' import { AkamaiAgentCR } from './ai/AkamaiAgentCR' import { DatabaseCR } from './ai/DatabaseCR' -import { - getAkamaiAgentCR, - getAkamaiKnowledgeBaseCR, - listAkamaiAgentCRs, - listAkamaiKnowledgeBaseCRs, -} from './ai/k8s' +import { getAkamaiAgentCR, getAkamaiKnowledgeBaseCR, listAkamaiAgentCRs, listAkamaiKnowledgeBaseCRs } from './ai/k8s' interface ExcludedApp extends App { managed: boolean From 30f17cefd6e4a8c9bf239efcb5f547ff89fe0ce6 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Fri, 24 Oct 2025 08:27:12 +0200 Subject: [PATCH 10/14] fix: tests --- src/ai/AkamaiKnowledgeBaseCR.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ai/AkamaiKnowledgeBaseCR.test.ts b/src/ai/AkamaiKnowledgeBaseCR.test.ts index 316dd08e0..8ab62479d 100644 --- a/src/ai/AkamaiKnowledgeBaseCR.test.ts +++ b/src/ai/AkamaiKnowledgeBaseCR.test.ts @@ -68,7 +68,7 @@ describe('AkamaiKnowledgeBaseCR', () => { expect(kbCR.spec.pipelineParameters).toEqual({ url: 'https://docs.example.com', table_name: 'test-kb', - embedding_model: 'nvidia/text-embedding-ada-002', + embedding_model: 'text-embedding-ada-002', embedding_api_base: 'http://embedding-model.ai.svc.cluster.local', embed_dim: 1536, embed_batch_size: expect.any(Number), @@ -133,7 +133,7 @@ describe('AkamaiKnowledgeBaseCR', () => { expect(response.kind).toBe('AkamaiKnowledgeBase') expect(response.metadata.name).toBe('test-kb') expect(response.metadata.labels?.['apl.io/teamId']).toBe('team-123') - expect(response.spec.modelName).toBe('nvidia/text-embedding-ada-002') + expect(response.spec.modelName).toBe('text-embedding-ada-002') expect(response.spec.sourceUrl).toBe('https://docs.example.com') expect(response.status).toEqual({}) }) @@ -216,7 +216,7 @@ describe('AkamaiKnowledgeBaseCR', () => { const result = await AkamaiKnowledgeBaseCR.create('team-123', 'test-kb', 'cluster-name', mockKnowledgeBaseRequest) expect(result).toBeInstanceOf(AkamaiKnowledgeBaseCR) - expect(result.spec.pipelineParameters.embedding_model).toBe('nvidia/text-embedding-ada-002') + expect(result.spec.pipelineParameters.embedding_model).toBe('text-embedding-ada-002') }) test('should handle AI models fetch error', async () => { From 3e22975f643272e52f436acadfe15c3fd85d8724 Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Fri, 24 Oct 2025 09:00:06 +0200 Subject: [PATCH 11/14] feat: add await --- src/api/alpha/teams/{teamId}/agents.ts | 4 ++-- src/api/alpha/teams/{teamId}/kb.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/alpha/teams/{teamId}/agents.ts b/src/api/alpha/teams/{teamId}/agents.ts index c3d011f96..71d8bea38 100644 --- a/src/api/alpha/teams/{teamId}/agents.ts +++ b/src/api/alpha/teams/{teamId}/agents.ts @@ -6,9 +6,9 @@ const debug = Debug('otomi:api:alpha:teams:agents') export default function (): OperationHandlerArray { const get: Operation = [ - ({ otomi, params: { teamId } }: OpenApiRequestExt, res): void => { + async ({ otomi, params: { teamId } }: OpenApiRequestExt, res): Promise => { debug(`getAplAgents(${teamId})`) - const v = otomi.getAplAgents(decodeURIComponent(teamId)) + const v = await otomi.getAplAgents(decodeURIComponent(teamId)) res.json(v) }, ] diff --git a/src/api/alpha/teams/{teamId}/kb.ts b/src/api/alpha/teams/{teamId}/kb.ts index b152de774..9b7b430c4 100644 --- a/src/api/alpha/teams/{teamId}/kb.ts +++ b/src/api/alpha/teams/{teamId}/kb.ts @@ -6,9 +6,9 @@ const debug = Debug('otomi:api:alpha:teams:kb') export default function (): OperationHandlerArray { const get: Operation = [ - ({ otomi, params: { teamId } }: OpenApiRequestExt, res): void => { + async ({ otomi, params: { teamId } }: OpenApiRequestExt, res): Promise => { debug(`getAplKnowledgeBases(${teamId})`) - const v = otomi.getAplKnowledgeBases(decodeURIComponent(teamId)) + const v = await otomi.getAplKnowledgeBases(decodeURIComponent(teamId)) res.json(v) }, ] From 9509eb7573284332882d4a600324a4fcbf47f76d Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Fri, 24 Oct 2025 09:27:19 +0200 Subject: [PATCH 12/14] feat: add addtributes --- src/otomi-stack.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index fea09d9d8..c43af6912 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -2515,6 +2515,9 @@ export default class OtomiStack { spec: { foundationModel: data.spec?.foundationModel ?? existingAgent.spec.foundationModel, foundationModelEndpoint: data.spec?.foundationModelEndpoint ?? existingAgent.spec.foundationModelEndpoint, + temperature: data.spec?.temperature ?? existingAgent.spec.temperature, + topP: data.spec?.topP ?? existingAgent.spec.topP, + maxTokens: data.spec?.maxTokens ?? existingAgent.spec.maxTokens, agentInstructions: data.spec?.agentInstructions ?? existingAgent.spec.agentInstructions, routes: (data.spec?.routes ?? existingAgent.spec.routes) as typeof existingAgent.spec.routes, tools: (data.spec?.tools ?? existingAgent.spec.tools) as typeof existingAgent.spec.tools, From 3a7957b66952c035f47ead4c9fb2596f7d9cb17e Mon Sep 17 00:00:00 2001 From: otomi-admin <63190600+ferruhcihan@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:58:08 +0200 Subject: [PATCH 13/14] fix: agent schema types --- src/ai/AkamaiAgentCR.ts | 6 +++--- src/openapi/agent.yaml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ai/AkamaiAgentCR.ts b/src/ai/AkamaiAgentCR.ts index c5e9d1b48..a98a82808 100644 --- a/src/ai/AkamaiAgentCR.ts +++ b/src/ai/AkamaiAgentCR.ts @@ -19,9 +19,9 @@ export class AkamaiAgentCR { public spec: { foundationModel: string foundationModelEndpoint?: string - temperature?: string - topP?: string - maxTokens?: string + temperature?: number + topP?: number + maxTokens?: number agentInstructions: string routes?: Array<{ agent: string diff --git a/src/openapi/agent.yaml b/src/openapi/agent.yaml index 2350d45c7..8a5ce0c39 100644 --- a/src/openapi/agent.yaml +++ b/src/openapi/agent.yaml @@ -28,17 +28,17 @@ AplAgentSpec: description: HTTP endpoint URL for the foundation model example: "http://llama-3-1-8b-predictor.team-admin.svc.cluster.local/openai/v1" temperature: - type: string + type: number description: Temperature parameter for model inference (controls randomness) - example: "0.7" + example: 0.7 topP: - type: string + type: number description: Top-p parameter for model inference (nucleus sampling) - example: "0.9" + example: 0.9 maxTokens: - type: string + type: number description: Maximum number of tokens to generate - example: "2048" + example: 2048 agentInstructions: type: string description: Custom instructions for the agent From 55c7298759b6ff1a9c63c2340620dd659b4b39bf Mon Sep 17 00:00:00 2001 From: Cas Lubbers Date: Mon, 27 Oct 2025 17:26:00 +0100 Subject: [PATCH 14/14] fix: get agents and kb from cache and only get status from cluster --- src/otomi-stack.ts | 96 +++++++++++++++++++++++++++---------- src/services/RepoService.ts | 5 ++ 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index c43af6912..74b618cb5 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -2431,27 +2431,51 @@ export default class OtomiStack { } async getAplKnowledgeBase(teamId: string, name: string): Promise { - // Fetch directly from Kubernetes cluster - const namespace = `team-${teamId}` - const cr = await getAkamaiKnowledgeBaseCR(namespace, name) + // First fetch from repository (Git) + const kbFromRepo = this.repoService.getTeamConfigService(teamId).getKnowledgeBase(name) - if (!cr) { + if (!kbFromRepo) { throw new NotExistError(`KnowledgeBase[${name}] does not exist.`) } - const kbCR = AkamaiKnowledgeBaseCR.fromCR(cr) - return kbCR.toApiResponse(teamId, (cr as any).status) + // Then try to update with cluster status + const namespace = `team-${teamId}` + try { + const cr = await getAkamaiKnowledgeBaseCR(namespace, name) + if (cr) { + // Update the knowledgebase with the cluster status + return { ...kbFromRepo, status: (cr as any).status } + } + } catch (error) { + // If cluster fetch fails, just use the repo data with default status + } + + return kbFromRepo } async getAplKnowledgeBases(teamId: string): Promise { - // Fetch directly from Kubernetes cluster + // First fetch from repository (Git) + const knowledgeBasesFromRepo = this.repoService.getTeamConfigService(teamId).getKnowledgeBases() + + // Then update with cluster status const namespace = `team-${teamId}` - const crs = await listAkamaiKnowledgeBaseCRs(namespace) + const knowledgeBasesWithStatus = await Promise.all( + knowledgeBasesFromRepo.map(async (kb) => { + try { + // Try to fetch the CR from cluster to get the actual status + const cr = await getAkamaiKnowledgeBaseCR(namespace, kb.metadata.name) + if (cr) { + // Update the knowledgebase with the cluster status + return { ...kb, status: (cr as any).status } + } + } catch (error) { + // If cluster fetch fails, just use the repo data with default status + } + return kb + }), + ) - return crs.map((cr) => { - const kbCR = AkamaiKnowledgeBaseCR.fromCR(cr) - return kbCR.toApiResponse(teamId, (cr as any).status) - }) + return knowledgeBasesWithStatus } getAllAplKnowledgeBases(): AplKnowledgeBaseResponse[] { @@ -2550,27 +2574,51 @@ export default class OtomiStack { } async getAplAgent(teamId: string, name: string): Promise { - // Fetch directly from Kubernetes cluster - const namespace = `team-${teamId}` - const cr = await getAkamaiAgentCR(namespace, name) + // First fetch from repository (Git) + const agentFromRepo = this.repoService.getTeamConfigService(teamId).getAgent(name) - if (!cr) { + if (!agentFromRepo) { throw new NotExistError(`Agent[${name}] does not exist.`) } - const agentCR = AkamaiAgentCR.fromCR(cr) - return agentCR.toApiResponse(teamId, (cr as any).status) + // Then try to update with cluster status + const namespace = `team-${teamId}` + try { + const cr = await getAkamaiAgentCR(namespace, name) + if (cr) { + // Update the agent with the cluster status + return { ...agentFromRepo, status: (cr as any).status } + } + } catch (error) { + // If cluster fetch fails, just use the repo data with default status + } + + return agentFromRepo } async getAplAgents(teamId: string): Promise { - // Fetch directly from Kubernetes cluster + // First fetch from repository (Git) + const agentsFromRepo = this.repoService.getTeamConfigService(teamId).getAgents() + + // Then update with cluster status const namespace = `team-${teamId}` - const crs = await listAkamaiAgentCRs(namespace) + const agentsWithStatus = await Promise.all( + agentsFromRepo.map(async (agent) => { + try { + // Try to fetch the CR from cluster to get the actual status + const cr = await getAkamaiAgentCR(namespace, agent.metadata.name) + if (cr) { + // Update the agent with the cluster status + return { ...agent, status: (cr as any).status } + } + } catch (error) { + // If cluster fetch fails, just use the repo data with default status + } + return agent + }), + ) - return crs.map((cr) => { - const agentCR = AkamaiAgentCR.fromCR(cr) - return agentCR.toApiResponse(teamId, (cr as any).status) - }) + return agentsWithStatus } private async saveTeamAgent(teamId: string, agent: AplAgentResponse): Promise { diff --git a/src/services/RepoService.ts b/src/services/RepoService.ts index 1df3f2e04..a9bf50e06 100644 --- a/src/services/RepoService.ts +++ b/src/services/RepoService.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid' import { AlreadyExists } from '../error' import { Alerts, + AplAgentResponse, AplBackupResponse, AplBuildResponse, AplCodeRepoResponse, @@ -266,6 +267,10 @@ export class RepoService { return this.getTeamIds().flatMap((teamName) => this.getTeamConfigService(teamName).getKnowledgeBases()) } + public getAllAgents(): AplAgentResponse[] { + return this.getTeamIds().flatMap((teamName) => this.getTeamConfigService(teamName).getAgents()) + } + public getAllBackups(): AplBackupResponse[] { return this.getTeamIds().flatMap((teamName) => this.getTeamConfigService(teamName).getBackups()) }