LLM Function Calling in Strongly-typed language

YoMo is designed to streamline the creation of LLM function calling tools using strongly-typed languages, currently, TypeScript and Golang are supported. By leveraging the power of TypeScript’s type system, we aim to provide a more efficient and engineering-friendly approach to defining and managing function calls for Large Language Models, including benefits like compile-time checks and easier unit testing.

This framework allows TypeScript developers to define functions that can be called by LLMs or MCP Clients. Instead of relying solely on JSONSchema for function definitions, you define your tools using standard TypeScript constructs:

  • a description string
  • an Argument type for input type definitions
  • an asynchronous handler() function that contains your tool’s logic

This approach offers several advantages:

  • Type Safety: Define your function arguments with TypeScript types, catching potential errors at compile time.

  • Improved Developer Experience: Work within the familiar TypeScript ecosystem, utilizing existing tools for linting, formatting, and testing.

  • Enhanced Maintainability: Function definitions and implementations are co-located and strongly typed, making code easier to understand and maintain.

  • Facilitates Testing: Unit testing your tool’s handler function becomes straightforward.

The framework is designed to make it easy to deploy your LLM tools as serverless while team collaboration through its structured and strongly-typed approach, and can be adapted to various environments.

Install the Package

npm install @yomo/sfn

Enssential Components

There are 3 essential components to define a tool:

Description

The description is a descriptive string to provide a clear, concise explanation of the tool’s functionality to the LLM.

example:

const description = "Get the current weather for `city_name`";

Argument

The Argument type defines the structure of the input data that your handler function expects. This TypeScript type is used by the framework to generate a the JSONSchema that the LLM can understand to format the arguments correctly.

Internally use typescript-json-schema to generate the JSONSchema from the TypeScript type.

example:

export type Argument = {
  /**
   * The name of the city to be queried
   */
  city_name: string;
}

Handler Function

The handler() function is the core logic of your tool. This is the function that the LLM will call when it determines that your tool is needed.

Signature: Must be an async function that accepts a single argument of the Argument type and returns a Promise that resolves to an object.

Return Value: The object returned by the handler() will typically be formatted and presented back to the callee, which could be the LLM or MCP Client.

example:

export async function handler(args: Argument) {
  const result = await getWeather(args.city_name)
  return result
}

Testing Your Tools

Create the test file in test/ folder, for example test/app.test.ts, and add the following code:

import { expect, test } from "bun:test";
import { getGeocode, getWeather } from "../src/app";

test('getGeocode', async () => {
        const address = '1600 Amphitheatre Parkway, Mountain View, CA';
        const result = await getGeocode(address);
        expect(result.lat).toBeCloseTo(37.4193295);
        expect(result.lng).toBeCloseTo(-122.0816532);
})

test('getWeatherByGoogleAPI', async () => {
        const lat = 37.4193295;
        const lng = -122.0816532;
        const result = await getWeather(lat, lng);
        expect(result).toBeDefined();
        expect(result.timeZone.id).toBe('America/Los_Angeles');
        expect(result.temperature).toBeDefined();
});

Then, run bun test to execute the tests. The framework will automatically validate the types and ensure that your tools are functioning as expected.

You can use any testing framework you prefer, such as Jest or Mocha, to run your tests. The key is to ensure that your tests cover the functionality of your tools and validate the expected behavior.

How YoMo Works

The framework acts as an intermediary between the LLM or MCP Client and your TypeScript tool code. The general flow is as follows:

  1. Tool Registration: You register your tools with the framework. The framework reads the description and the Argument type for each tool.

  2. Schema Generation: Using the Argument type, the framework generates a machine-readable schema (e.g., JSONSchema) for each tool’s input.

  3. LLM Interaction: When you interact with an LLM (using the framework’s integration layer), the framework provides the LLM with the description and the generated schema for your registered tools.

  4. LLM Function Call: Based on the user’s prompt and the provided tool information, the LLM decides if it needs to call a tool. If it does, it responds with the name of the tool to call and an object containing the arguments, formatted according to the schema.

  5. Argument Validation and Handling: The framework receives the LLM’s response, validates the provided arguments against the Argument type (or the generated schema), and then invokes the corresponding handler function with the validated arguments.

  6. Result to LLM/MCP Client: The object returned by your handler function is sent back to the LLM for it to formulate a response to the user, or is processed further by your application.

Advanced Topics

  • Error Handling: if error occurs in the handler function, the best practice is to generate a descriptive string and return it, in that, the LLM can understand the error and take action accordingly.
  • Logging: You can use any logging library you prefer, to log the events in your handler function. This will help you to debug the issues and understand the flow of the application.