Skip to main content

Overview

The loadNavaiFunctions function dynamically loads frontend function modules and registers them for use by the NAVAI voice agent. It supports functions, classes, and object exports, automatically extracting callable members and generating normalized function names.

Import

import { loadNavaiFunctions } from '@navai/voice-frontend';

Signature

async function loadNavaiFunctions(
  functionModuleLoaders: NavaiFunctionModuleLoaders
): Promise<NavaiFunctionsRegistry>

Parameters

functionModuleLoaders
NavaiFunctionModuleLoaders
required
Object mapping module paths to dynamic import loaders.
type NavaiFunctionModuleLoaders = Record<string, () => Promise<unknown>>;
Example:
{
  './functions/getUserProfile.ts': () => import('./functions/getUserProfile'),
  './functions/sendMessage.ts': () => import('./functions/sendMessage'),
  './functions/UserService.ts': () => import('./functions/UserService')
}
Note: Files ending in .d.ts are automatically filtered out.

Return Value

registry
NavaiFunctionsRegistry
Registry object containing loaded functions and metadata.
type NavaiFunctionDefinition = {
  name: string;         // Normalized function name
  description: string;  // Generated description
  source: string;       // Source path (e.g., "./functions/getUser.ts#default")
  run: (                // Execution function
    payload: NavaiFunctionPayload,
    context: NavaiFunctionContext
  ) => Promise<unknown> | unknown;
};

type NavaiFunctionPayload = Record<string, unknown>;

type NavaiFunctionContext = {
  navigate: (path: string) => void;
};

Supported Export Types

1. Default or Named Function Export

// getUserProfile.ts
export default async function getUserProfile(userId: string) {
  return await api.get(`/users/${userId}`);
}

// Or named export
export async function getUserProfile(userId: string) {
  return await api.get(`/users/${userId}`);
}
Registered as: get_user_profile Description: "Call exported function getUserProfile." or "Call exported function default."

2. Class Export

// UserService.ts
export class UserService {
  constructor(private apiClient: ApiClient) {}
  
  async getProfile(userId: string) {
    return this.apiClient.get(`/users/${userId}`);
  }
  
  async updateProfile(userId: string, data: UpdateData) {
    return this.apiClient.put(`/users/${userId}`, data);
  }
}
Registered as:
  • user_service_get_profile
  • user_service_update_profile
Descriptions:
  • "Call class method UserService.getProfile()."
  • "Call class method UserService.updateProfile()."

3. Object with Methods

// userUtils.ts
export const userUtils = {
  formatName(user: User) {
    return `${user.firstName} ${user.lastName}`;
  },
  
  isActive(user: User) {
    return user.status === 'active';
  }
};
Registered as:
  • user_utils_format_name
  • user_utils_is_active
Descriptions:
  • "Call exported object member userUtils.formatName()."
  • "Call exported object member userUtils.isActive()."

Name Normalization

Function names are normalized using the following rules:
  1. Convert camelCase to snake_case: getUserProfileget_user_profile
  2. Replace non-alphanumeric characters with underscore: get-userget_user
  3. Convert to lowercase: GetUserget_user
  4. Strip leading/trailing underscores: _getUser_get_user
Examples:
  • getUserProfileget_user_profile
  • sendEmailNotificationsend_email_notification
  • UserService.getProfileuser_service_get_profile
  • api-clientapi_client

Duplicate Name Handling

When multiple functions normalize to the same name, subsequent occurrences are suffixed with incrementing numbers:
// First occurrence
getUserget_user

// Second occurrence
get_user (from different module) → get_user_2

// Third occurrence
getUser (from another module) → get_user_3
Warnings are emitted for renamed functions:
[navai] Renamed duplicated function "get_user" to "get_user_2".

Payload Structure

Functions receive payloads with flexible argument passing:

Simple Arguments

{
  args: [arg1, arg2, arg3]
}

// Or alternative key
{
  arguments: [arg1, arg2, arg3]
}

Single Value

{
  value: someValue
}

Object Payload

If no args, arguments, or value key exists, the entire payload is passed:
{
  userId: '123',
  email: '[email protected]'
}
// Function receives this entire object

Class Methods

{
  constructorArgs: [apiClient, config],  // For class constructor
  methodArgs: [userId, data]              // For method invocation
}

Context Injection

Functions can optionally receive a context object as their last parameter:
export async function navigateToProfile(
  userId: string,
  context: NavaiFunctionContext
) {
  // Fetch profile data
  const profile = await getProfile(userId);
  
  // Navigate using provided context
  context.navigate(`/profile/${userId}`);
  
  return profile;
}
The context is automatically appended if the function has more parameters than provided arguments.

Example Usage

Basic Function Loading

import { loadNavaiFunctions } from '@navai/voice-frontend';

const registry = await loadNavaiFunctions({
  './functions/getUser.ts': () => import('./functions/getUser'),
  './functions/sendMessage.ts': () => import('./functions/sendMessage'),
  './functions/UserService.ts': () => import('./functions/UserService')
});

// Check for warnings
if (registry.warnings.length > 0) {
  console.warn('Loading warnings:', registry.warnings);
}

// List loaded functions
console.log('Loaded functions:');
for (const fn of registry.ordered) {
  console.log(`- ${fn.name}: ${fn.description}`);
}

// Execute a function by name
const getUserFn = registry.byName.get('get_user');
if (getUserFn) {
  const result = await getUserFn.run(
    { args: ['user-123'] },
    { navigate: (path) => router.push(path) }
  );
  console.log('User data:', result);
}

Integration with buildNavaiAgent

import { loadNavaiFunctions, buildNavaiAgent } from '@navai/voice-frontend';

const moduleLoaders = {
  './functions/getUser.ts': () => import('./functions/getUser'),
  './functions/updateSettings.ts': () => import('./functions/updateSettings')
};

// Direct use with buildNavaiAgent
const { agent, warnings } = await buildNavaiAgent({
  navigate: (path) => router.push(path),
  routes: [...],
  functionModuleLoaders: moduleLoaders  // Automatically loads internally
});

// Or manual loading for inspection
const registry = await loadNavaiFunctions(moduleLoaders);
console.log(`Loaded ${registry.ordered.length} functions`);

Dynamic Function Execution

const registry = await loadNavaiFunctions(moduleLoaders);
const context = { navigate: (path) => router.push(path) };

// Execute function with different payload types

// 1. Simple arguments
const fn1 = registry.byName.get('get_user_profile');
const result1 = await fn1.run(
  { args: ['user-123'] },
  context
);

// 2. Single value
const fn2 = registry.byName.get('process_data');
const result2 = await fn2.run(
  { value: { data: [1, 2, 3] } },
  context
);

// 3. Object payload
const fn3 = registry.byName.get('send_notification');
const result3 = await fn3.run(
  { to: '[email protected]', message: 'Hello' },
  context
);

// 4. Class method with constructor args
const fn4 = registry.byName.get('user_service_update_profile');
const result4 = await fn4.run(
  {
    constructorArgs: [apiClient],
    methodArgs: ['user-123', { name: 'John' }]
  },
  context
);

Error Handling

const registry = await loadNavaiFunctions(moduleLoaders);

// Check warnings for loading issues
for (const warning of registry.warnings) {
  if (warning.includes('Failed to load')) {
    console.error('Critical loading error:', warning);
  } else {
    console.warn('Non-critical warning:', warning);
  }
}

// Handle function execution errors
const fn = registry.byName.get('risky_function');
if (fn) {
  try {
    const result = await fn.run(payload, context);
    console.log('Success:', result);
  } catch (error) {
    console.error('Function execution failed:', error);
  }
}

Advanced Use Cases

Function with Context

// navigateAndFetch.ts
export async function navigateAndFetch(
  page: string,
  context: NavaiFunctionContext
) {
  // Navigate first
  context.navigate(`/${page}`);
  
  // Then fetch data
  const data = await fetchData(page);
  
  return {
    navigated: true,
    data
  };
}

Class with Complex Methods

// DataProcessor.ts
export class DataProcessor {
  constructor(
    private apiClient: ApiClient,
    private config: Config
  ) {}
  
  async processAndSave(
    data: unknown[],
    options: ProcessOptions,
    context: NavaiFunctionContext
  ) {
    const processed = this.process(data, options);
    await this.save(processed);
    
    // Optionally navigate after success
    if (options.navigateOnSuccess) {
      context.navigate('/results');
    }
    
    return { success: true, count: processed.length };
  }
}
Usage:
{
  function_name: 'data_processor_process_and_save',
  payload: {
    constructorArgs: [apiClient, config],
    methodArgs: [dataArray, { navigateOnSuccess: true }]
  }
}

Source Reference

Defined in: packages/voice-frontend/src/functions.ts:280

Build docs developers (and LLMs) love