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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions src/ai/AkamaiAgentCR.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { AkamaiAgentCR } from './AkamaiAgentCR'
import { AplAgentRequest } from 'src/otomi-models'
import { K8sResourceNotFound } from '../error'
import * as aiModelHandler from './aiModelHandler'

// Mock the aiModelHandler module
jest.mock('./aiModelHandler')
const mockedGetAIModels = aiModelHandler.getAIModels as jest.MockedFunction<typeof aiModelHandler.getAIModels>

describe('AkamaiAgentCR', () => {
const mockFoundationModel = {
kind: 'AplAIModel',
metadata: { name: 'gpt-4' },
spec: {
displayName: 'GPT-4',
modelEndpoint: 'http://gpt-4.ai.svc.cluster.local',
modelType: 'foundation' as const,
},
status: {
conditions: [],
phase: 'Ready' as const,
},
}

const mockAgentRequest: AplAgentRequest = {
kind: 'AkamaiAgent',
metadata: {
name: 'test-agent',
},
spec: {
foundationModel: 'gpt-4',
agentInstructions: 'You are a helpful assistant',
knowledgeBase: 'test-kb',
},
}

beforeEach(() => {
jest.clearAllMocks()
})

describe('constructor', () => {
test('should create AkamaiAgentCR with all properties', () => {
const agentCR = new AkamaiAgentCR('team-123', 'test-agent', mockAgentRequest)

expect(agentCR.apiVersion).toBeDefined()
expect(agentCR.kind).toBeDefined()
expect(agentCR.metadata.name).toBe('test-agent')
expect(agentCR.metadata.namespace).toBe('team-team-123')
expect(agentCR.metadata.labels?.['apl.io/teamId']).toBe('team-123')
expect(agentCR.spec.foundationModel).toBe('gpt-4')
expect(agentCR.spec.systemPrompt).toBe('You are a helpful assistant')
expect(agentCR.spec.knowledgeBase).toBe('test-kb')
})

test('should set teamId label and not merge custom labels', () => {
const agentCR = new AkamaiAgentCR('team-123', 'test-agent', mockAgentRequest)

expect(agentCR.metadata.labels).toEqual({
'apl.io/teamId': 'team-123',
})
// Custom labels from request are not merged in constructor
expect(agentCR.metadata.labels?.['custom-label']).toBeUndefined()
})

test('should handle request without knowledgeBase', () => {
const requestWithoutKB = {
...mockAgentRequest,
spec: {
...mockAgentRequest.spec,
knowledgeBase: undefined,
},
}

const agentCR = new AkamaiAgentCR('team-123', 'test-agent', requestWithoutKB)

expect(agentCR.spec.knowledgeBase).toBeUndefined()
})
})

describe('toRecord', () => {
test('should return serializable record', () => {
const agentCR = new AkamaiAgentCR('team-123', 'test-agent', mockAgentRequest)
const record = agentCR.toRecord()

expect(record).toEqual({
apiVersion: agentCR.apiVersion,
kind: agentCR.kind,
metadata: agentCR.metadata,
spec: agentCR.spec,
})
})
})

describe('toApiResponse', () => {
test('should transform to API response format', () => {
const agentCR = new AkamaiAgentCR('team-123', 'test-agent', mockAgentRequest)
const response = agentCR.toApiResponse('team-123')

expect(response).toEqual({
kind: 'AkamaiAgent',
metadata: {
name: 'test-agent',
namespace: 'team-team-123',
labels: {
'apl.io/teamId': 'team-123',
},
},
spec: {
foundationModel: 'gpt-4',
agentInstructions: 'You are a helpful assistant',
knowledgeBase: 'test-kb',
},
status: {
conditions: [
{
type: 'AgentDeployed',
status: true,
reason: 'Scheduled',
message: 'Successfully deployed the Agent',
},
],
},
})
})

test('should handle empty knowledgeBase in response', () => {
const requestWithoutKB = {
...mockAgentRequest,
spec: {
...mockAgentRequest.spec,
knowledgeBase: undefined,
},
}

const agentCR = new AkamaiAgentCR('team-123', 'test-agent', requestWithoutKB)
const response = agentCR.toApiResponse('team-123')

expect(response.spec.knowledgeBase).toBe('')
})
})

describe('create', () => {
test('should create AkamaiAgentCR when foundation model exists', async () => {
mockedGetAIModels.mockResolvedValue([mockFoundationModel as any])

const result = await AkamaiAgentCR.create('team-123', 'test-agent', mockAgentRequest)

expect(result).toBeInstanceOf(AkamaiAgentCR)
expect(result.metadata.name).toBe('test-agent')
expect(mockedGetAIModels).toHaveBeenCalledTimes(1)
})

test('should throw K8sResourceNotFound when foundation model does not exist', async () => {
mockedGetAIModels.mockResolvedValue([])

await expect(AkamaiAgentCR.create('team-123', 'test-agent', mockAgentRequest)).rejects.toThrow(
K8sResourceNotFound,
)
await expect(AkamaiAgentCR.create('team-123', 'test-agent', mockAgentRequest)).rejects.toThrow(
"Foundation model 'gpt-4' not found",
)
})

test('should throw K8sResourceNotFound when foundation model has wrong type', async () => {
const embeddingModel = {
...mockFoundationModel,
spec: {
...mockFoundationModel.spec,
modelType: 'embedding' as const,
},
}
mockedGetAIModels.mockResolvedValue([embeddingModel as any])

await expect(AkamaiAgentCR.create('team-123', 'test-agent', mockAgentRequest)).rejects.toThrow(
K8sResourceNotFound,
)
})

test('should throw error when foundationModel is undefined', async () => {
const requestWithoutModel = {
...mockAgentRequest,
spec: {
...mockAgentRequest.spec,
foundationModel: undefined,
},
}

mockedGetAIModels.mockResolvedValue([mockFoundationModel as any])

await expect(AkamaiAgentCR.create('team-123', 'test-agent', requestWithoutModel as any)).rejects.toThrow(
K8sResourceNotFound,
)
await expect(AkamaiAgentCR.create('team-123', 'test-agent', requestWithoutModel as any)).rejects.toThrow(
"Foundation model 'undefined' not found",
)
})
})

describe('fromCR', () => {
test('should create instance from existing CR object', () => {
const crObject = {
apiVersion: 'akamai.com/v1',
kind: 'Agent',
metadata: { name: 'existing-agent', namespace: 'team-456' },
spec: { foundationModel: 'gpt-3.5', systemPrompt: 'Test prompt' },
}

const result = AkamaiAgentCR.fromCR(crObject)

expect(result).toBeInstanceOf(AkamaiAgentCR)
expect(result.metadata.name).toBe('existing-agent')
expect(result.spec.foundationModel).toBe('gpt-3.5')
})
})
})
103 changes: 103 additions & 0 deletions src/ai/AkamaiAgentCR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { AplAgentRequest, AplAgentResponse } from 'src/otomi-models'
import { AGENT_API_VERSION, AGENT_KIND, cleanEnv } from '../validators'
import { K8sResourceNotFound } from '../error'
import { getAIModels } from './aiModelHandler'

const env = cleanEnv({
AGENT_API_VERSION,
AGENT_KIND,
})

export class AkamaiAgentCR {
public apiVersion: string
public kind: string
public metadata: {
name: string
namespace: string
labels?: Record<string, string>
}
public spec: {
foundationModel: string
systemPrompt: string
knowledgeBase?: string
}

constructor(teamId: string, agentName: string, request: AplAgentRequest) {
const namespace = `team-${teamId}`

this.apiVersion = env.AGENT_API_VERSION
this.kind = env.AGENT_KIND
this.metadata = {
...request.metadata,
name: agentName,
namespace,
labels: {
'apl.io/teamId': teamId,
},
}
this.spec = {
foundationModel: request.spec.foundationModel,
systemPrompt: request.spec.agentInstructions,
knowledgeBase: request.spec.knowledgeBase,
}
}

// Convert to plain object for serialization
toRecord(): Record<string, any> {
return {
apiVersion: this.apiVersion,
kind: this.kind,
metadata: this.metadata,
spec: this.spec,
}
}

// Transform to API response format
toApiResponse(teamId: string): AplAgentResponse {
return {
kind: 'AkamaiAgent',
metadata: {
...this.metadata,
labels: {
'apl.io/teamId': teamId,
...(this.metadata.labels || {}),
},
},
spec: {
foundationModel: this.spec.foundationModel,
agentInstructions: this.spec.systemPrompt,
knowledgeBase: this.spec.knowledgeBase || '',
},
status: {
conditions: [
{
type: 'AgentDeployed',
status: true,
reason: 'Scheduled',
message: 'Successfully deployed the Agent',
},
],
},
}
}

// Static factory method
static async create(teamId: string, agentName: string, request: AplAgentRequest): Promise<AkamaiAgentCR> {
const aiModels = await getAIModels()
const embeddingModel = aiModels.find(
(model) => model.metadata.name === request.spec.foundationModel && model.spec.modelType === 'foundation',
)

if (!embeddingModel) {
throw new K8sResourceNotFound('Foundation model', `Foundation model '${request.spec.foundationModel}' not found`)
}

return new AkamaiAgentCR(teamId, agentName, request)
}

// Static method to create from existing CR (for transformation)
static fromCR(cr: any): AkamaiAgentCR {
const instance = Object.create(AkamaiAgentCR.prototype)
return Object.assign(instance, cr)
}
}
Loading
Loading