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
7 changes: 5 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@smythos/sre",
"version": "1.5.43",
"version": "1.5.44",
"description": "Smyth Runtime Environment",
"author": "Alaa-eddine KADDOURI",
"license": "MIT",
Expand All @@ -19,6 +19,9 @@
"CHANGELOG"
],
"type": "module",
"engines": {
"node": ">=20"
},
"scripts": {
"gen:barrel": "ctix build",
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist/types -p tsconfig.dts.json",
Expand Down Expand Up @@ -88,7 +91,7 @@
"mime": "^4.0.3",
"mysql2": "^3.11.3",
"oauth-1.0a": "^2.2.6",
"openai": "^4.103.0",
"openai": "^5.12.2",
"p-limit": "^6.1.0",
"qs": "^6.13.0",
"readline-sync": "^1.4.10",
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/Components/GenAILLM.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ export class GenAILLM extends Component {
description: 'If true, the component will use reasoning capabilities for complex problem-solving',
label: 'Use Reasoning',
},
verbosity: {
type: 'string',
valid: ['low', 'medium', 'high'],
label: 'Verbosity',
allowEmpty: true,
},
reasoningEffort: {
type: 'string',
valid: ['none', 'default', 'low', 'medium', 'high'],
Expand Down Expand Up @@ -283,6 +289,7 @@ export class GenAILLM extends Component {
useSystemPrompt: Joi.boolean().optional().label('Use System Prompt'),
useContextWindow: Joi.boolean().optional().label('Use Context Window'),
maxContextWindowLength: Joi.number().optional().min(0).label('Maximum Context Window Length'),
verbosity: Joi.string().valid('low', 'medium', 'high').optional().allow('').allow(null).label('Verbosity'),

// #region Web Search
useWebSearch: Joi.boolean().optional().label('Use Web Search'),
Expand Down Expand Up @@ -322,7 +329,7 @@ export class GenAILLM extends Component {

// #region Reasoning
useReasoning: Joi.boolean().optional().label('Use Reasoning'),
reasoningEffort: Joi.string().valid('none', 'default', 'low', 'medium', 'high').optional().allow('').label('Reasoning Effort'),
reasoningEffort: Joi.string().valid('none', 'default', 'minimal', 'low', 'medium', 'high').optional().allow('').label('Reasoning Effort'),
maxThinkingTokens: Joi.number().min(1).optional().label('Maximum Thinking Tokens'),
// #endregion
});
Expand Down
104 changes: 82 additions & 22 deletions packages/core/src/helpers/AWSLambdaCode.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from 'crypto';
import { ConnectorService } from '@sre/Core/ConnectorsService';
import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
import zl from 'zip-lib';
import { InvokeCommand, Runtime, LambdaClient, UpdateFunctionCodeCommand, CreateFunctionCommand, GetFunctionCommand, GetFunctionCommandOutput, InvokeCommandOutput } from '@aws-sdk/client-lambda';
import { InvokeCommand, Runtime, LambdaClient, UpdateFunctionCodeCommand, CreateFunctionCommand, GetFunctionCommand, GetFunctionCommandOutput, InvokeCommandOutput, UpdateFunctionConfigurationCommand } from '@aws-sdk/client-lambda';
import { GetRoleCommand, CreateRoleCommand, IAMClient, GetRoleCommandOutput, CreateRoleCommandOutput } from '@aws-sdk/client-iam';
import fs from 'fs';
import { AWSConfig, AWSCredentials, AWSRegionConfig } from '@sre/types/AWS.types';
Expand All @@ -20,13 +20,15 @@ export function getLambdaFunctionName(agentId: string, componentId: string) {
}


export function generateCodeHash(code_body: string, codeInputs: string[]) {
export function generateCodeHash(code_body: string, codeInputs: string[], envVariables: string[]) {
const bodyHash = getSanitizeCodeHash(code_body);
const inputsHash = getSanitizeCodeHash(JSON.stringify(codeInputs));
return `body-${bodyHash}__inputs-${inputsHash}`;
const envVariablesHash = getSanitizeCodeHash(JSON.stringify(envVariables));
return `body-${bodyHash}__inputs-${inputsHash}__env-${envVariablesHash}`;
}

export function getSanitizeCodeHash(code: string) {
export function getSanitizeCodeHash(rawCode: string) {
const code = replaceVaultKeysTemplateVars(rawCode, {});
let output = '';
let isSingleQuote = false;
let isDoubleQuote = false;
Expand Down Expand Up @@ -82,9 +84,15 @@ export async function setDeployedCodeHash(agentId: string, componentId: string,
.set(`${cachePrefix}_${agentId}-${componentId}`, codeHash, null, null, cacheTTL);
}

export function generateLambdaCode(code: string, parameters: string[]) {
function replaceVaultKeysTemplateVars(code: string, envVariables: Record<string, string>) {
const regex = /\{\{KEY\((.*?)\)\}\}/g;
return code.replaceAll(regex, (match, p1) => `process.env.${p1}`);
}

export function generateLambdaCode(code: string, parameters: string[], envVariables: Record<string, string>) {
const codeWithEnvVariables = envVariables && Object.keys(envVariables).length ? replaceVaultKeysTemplateVars(code, envVariables) : code;
const lambdaCode = `
${code}
${codeWithEnvVariables}
export const handler = async (event, context) => {
try {
context.callbackWaitsForEmptyEventLoop = false;
Expand Down Expand Up @@ -118,7 +126,7 @@ export async function zipCode(directory: string) {
});
}

export async function createOrUpdateLambdaFunction(functionName, zipFilePath, awsConfigs) {
export async function createOrUpdateLambdaFunction(functionName, zipFilePath, awsConfigs, envVariables: Record<string, string>) {
const client = new LambdaClient({
region: awsConfigs.region,
credentials: {
Expand All @@ -142,9 +150,12 @@ export async function createOrUpdateLambdaFunction(functionName, zipFilePath, aw
};
const updateFunctionCodeCommand = new UpdateFunctionCodeCommand(updateCodeParams);
await client.send(updateFunctionCodeCommand);
// Update function configuration to attach layer
await verifyFunctionDeploymentStatus(functionName, client);
// console.log('Lambda function code and configuration updated successfully!');

if (envVariables && Object.keys(envVariables).length) {
await updateLambdaFunctionConfiguration(client, functionName, envVariables);
await verifyFunctionDeploymentStatus(functionName, client);
}
} else {
// Create function if it does not exist
let roleArn = '';
Expand Down Expand Up @@ -188,6 +199,7 @@ export async function createOrUpdateLambdaFunction(functionName, zipFilePath, aw
'auto-delete': 'true',
},
MemorySize: 256,
...(envVariables && Object.keys(envVariables).length ? { Environment: { Variables: envVariables } } : {}),
};

const functionCreateCommand = new CreateFunctionCommand(functionParams);
Expand All @@ -200,6 +212,17 @@ export async function createOrUpdateLambdaFunction(functionName, zipFilePath, aw
}
}

function updateLambdaFunctionConfiguration(client: LambdaClient, functionName: string, envVariables: Record<string, string>) {
const updateFunctionConfigurationParams = {
FunctionName: functionName,
Environment: {
Variables: envVariables,
},
};
const updateFunctionConfigurationCommand = new UpdateFunctionConfigurationCommand(updateFunctionConfigurationParams);
return client.send(updateFunctionConfigurationCommand);
}

export async function waitForRoleDeploymentStatus(roleName, client): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
Expand Down Expand Up @@ -356,10 +379,11 @@ export function reportUsage({ cost, agentId, teamId }: { cost: number; agentId:
});
}

export function validateAsyncMainFunction(code: string): { isValid: boolean; error?: string; parameters?: string[]; dependencies?: string[] } {
export function validateAsyncMainFunction(rawCode: string): { isValid: boolean; error?: string; parameters?: string[]; dependencies?: string[] } {
try {
// Parse the code using acorn
const ast = acorn.parse(code, {
const code = replaceVaultKeysTemplateVars(rawCode, {});
// Parse the code using acorn
const ast = acorn.parse(code, {
ecmaVersion: 'latest',
sourceType: 'module'
});
Expand Down Expand Up @@ -387,8 +411,8 @@ export function validateAsyncMainFunction(code: string): { isValid: boolean; err
}

// Handle CallExpression (require() calls)
if (node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
if (node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length > 0 &&
node.arguments[0].type === 'Literal') {
Expand All @@ -399,7 +423,7 @@ export function validateAsyncMainFunction(code: string): { isValid: boolean; err
}

// Handle dynamic import() calls
if (node.type === 'CallExpression' &&
if (node.type === 'CallExpression' &&
node.callee.type === 'Import' &&
node.arguments.length > 0 &&
node.arguments[0].type === 'Literal') {
Expand Down Expand Up @@ -473,26 +497,26 @@ export function validateAsyncMainFunction(code: string): { isValid: boolean; err
}

if (!hasMain) {
return {
isValid: false,
return {
isValid: false,
error: 'No main function found at root level',
dependencies
};
}

if (!hasAsyncMain) {
return {
isValid: false,
return {
isValid: false,
error: 'Main function exists but is not async',
dependencies
};
}

return { isValid: true, parameters: mainParameters, dependencies };
} catch (error) {
return {
isValid: false,
error: `Failed to parse code: ${error.message}`
return {
isValid: false,
error: `Failed to parse code: ${error.message}`
};
}
}
Expand Down Expand Up @@ -526,3 +550,39 @@ export function generateCodeFromLegacyComponent(code_body: string, code_imports:
return code;
}

export function extractAllKeyNamesFromTemplateVars(input: string): string[] {
const regex = /\{\{KEY\((.*?)\)\}\}/g;
const matches = [];
let match;
while ((match = regex.exec(input)) !== null) {
if (match[1]) {
matches.push(match[1]);
}
}
return matches;
}


async function fetchVaultSecret(keyName: string, agentTeamId: string): Promise<{ value: string, key: string }> {
const vaultSecret = await VaultHelper.getAgentKey(keyName, agentTeamId);
return {
value: vaultSecret,
key: keyName,
};

}

export async function getCurrentEnvironmentVariables(agentTeamId: string, code: string): Promise<Record<string, string>> {
const allKeyNames = extractAllKeyNamesFromTemplateVars(code);
const envVariables: Record<string, string> = {};
const vaultSecrets = await Promise.all(allKeyNames.map((keyName) => fetchVaultSecret(keyName, agentTeamId)));
vaultSecrets.forEach((secret) => {
envVariables[secret.key] = secret.value;
});
return envVariables;
}

export function getSortedObjectValues(obj: Record<string, string>): string[] {
const sortedKeys = Object.keys(obj).sort();
return sortedKeys.map((key) => obj[key]);
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ export * from './subsystems/Security/Vault.service/connectors/SecretsManager.cla
export * from './subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class';
export * from './subsystems/LLMManager/LLM.service/connectors/openai/types';
export * from './subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/constants';
export * from './subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/utils';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AccessRequest } from '@sre/Security/AccessControl/AccessRequest.class';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { cacheTTL, createOrUpdateLambdaFunction, generateCodeHash, generateLambdaCode, getDeployedCodeHash, getDeployedFunction, getLambdaFunctionName, invokeLambdaFunction, setDeployedCodeHash, updateDeployedCodeTTL, validateAsyncMainFunction, zipCode } from '@sre/helpers/AWSLambdaCode.helper';
import { cacheTTL, createOrUpdateLambdaFunction, generateCodeHash, generateLambdaCode, getCurrentEnvironmentVariables, getDeployedCodeHash, getDeployedFunction, getLambdaFunctionName, getSortedObjectValues, invokeLambdaFunction, setDeployedCodeHash, updateDeployedCodeTTL, validateAsyncMainFunction, zipCode } from '@sre/helpers/AWSLambdaCode.helper';
import { AWSCredentials, AWSRegionConfig } from '@sre/types/AWS.types';
import { Logger } from '@sre/helpers/Log.helper';

Expand All @@ -31,14 +31,16 @@ export class AWSLambdaCode extends CodeConnector {
public async deploy(acRequest: AccessRequest, codeUID: string, input: CodeInput, config: CodeConfig): Promise<CodeDeployment> {
const agentId = acRequest.candidate.id;
const functionName = getLambdaFunctionName(agentId, codeUID);


const [isLambdaExists, exisitingCodeHash] = await Promise.all([
const [isLambdaExists, exisitingCodeHash, currentEnvVariables] = await Promise.all([
getDeployedFunction(functionName, this.awsConfigs),
getDeployedCodeHash(agentId, codeUID),
getCurrentEnvironmentVariables(acRequest.candidate.id, input.code),
]);

const codeHash = generateCodeHash(input.code, Object.keys(input.inputs));
// sorting values to get the same hash for the same env variables
const envValues = getSortedObjectValues(currentEnvVariables);

const codeHash = generateCodeHash(input.code, Object.keys(input.inputs), envValues);
if (isLambdaExists && exisitingCodeHash === codeHash) {
return {
id: functionName,
Expand All @@ -57,7 +59,7 @@ export class AWSLambdaCode extends CodeConnector {
if (!isValid) {
throw new Error(error || 'Invalid Code')
}
const lambdaCode = generateLambdaCode(input.code, parameters);
const lambdaCode = generateLambdaCode(input.code, parameters, currentEnvVariables);
// create folder
fs.mkdirSync(directory);
// create index.js file
Expand All @@ -68,7 +70,7 @@ export class AWSLambdaCode extends CodeConnector {
execSync(`npm install ${dependencies.join(' ')}`, { cwd: directory });

const zipFilePath = await zipCode(directory);
await createOrUpdateLambdaFunction(functionName, zipFilePath, this.awsConfigs);
await createOrUpdateLambdaFunction(functionName, zipFilePath, this.awsConfigs, currentEnvVariables);
await setDeployedCodeHash(agentId, codeUID, codeHash);
console.debug('Lambda function updated successfully!');
return {
Expand All @@ -83,7 +85,7 @@ export class AWSLambdaCode extends CodeConnector {
try {
fs.rmSync(`${directory}`, { recursive: true, force: true });
fs.unlinkSync(`${directory}.zip`);
} catch (error) {}
} catch (error) { }
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type ChatCompletionCreateParams = {
reasoning_effort?: 'none' | 'default' | 'low' | 'medium' | 'high';
};

const MODELS_WITHOUT_REASONING_EFFORT_SUPPORT = ['deepseek-r1-distill-llama-70b'];

export class GroqConnector extends LLMConnector {
public name = 'LLM:Groq';

Expand Down Expand Up @@ -166,10 +168,10 @@ export class GroqConnector extends LLMConnector {
}
//#endregion Handle JSON response format

const isReasoningModel = params.useReasoning && params.capabilities?.reasoning;
const allowReasoning = params.useReasoning && params.capabilities?.reasoning;

if (params.maxTokens !== undefined) {
if (isReasoningModel) {
if (allowReasoning) {
body.max_completion_tokens = params.maxTokens;
} else {
body.max_tokens = params.maxTokens;
Expand All @@ -183,7 +185,11 @@ export class GroqConnector extends LLMConnector {
if (params.toolsConfig?.tool_choice) body.tool_choice = params.toolsConfig?.tool_choice as any;

// Apply user-specified reasoning parameters
if (isReasoningModel) {
if (
allowReasoning &&
isValidGroqReasoningEffort(params?.reasoningEffort) &&
!MODELS_WITHOUT_REASONING_EFFORT_SUPPORT.includes(params?.modelEntryName)
) {
if (params.reasoningEffort !== undefined) body.reasoning_effort = params.reasoningEffort;
}

Expand Down Expand Up @@ -289,3 +295,10 @@ export class GroqConnector extends LLMConnector {
});
}
}
/**
* Type guard to check if a value is a valid OpenAI reasoning effort.
* Uses array includes for better maintainability when OpenAI adds new values.
*/
export function isValidGroqReasoningEffort(value: unknown): value is 'low' | 'medium' | 'high' | 'none' | 'default' {
return ['none', 'default', 'low', 'medium', 'high'].includes(value as string);
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class OpenAIConnector extends LLMConnector {
const openai = await this.getClient(context);
const response = await openai.images.generate(body as OpenAI.Images.ImageGenerateParams);

return response;
return response as OpenAI.ImagesResponse;
}

protected async imageEditRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<OpenAI.ImagesResponse> {
Expand All @@ -182,7 +182,7 @@ export class OpenAIConnector extends LLMConnector {
const openai = await this.getClient(context);
const response = await openai.images.edit(_body);

return response;
return response as OpenAI.ImagesResponse;
}
// #endregion

Expand Down
Loading