diff --git a/README.md b/README.md index df5e59e..c3d9cf5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g * [Function Calling](./docs/function-calling/TOOLS.md#Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools * [Creating Custom Tools](./docs/function-calling/CUSTOM-TOOLS.md): You can create your own custom tools to enhance Loki's capabilities. * [Create Custom Python Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-python-based-tools) + * [Create Custom TypeScript Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools) * [Create Custom Bash Tools](./docs/function-calling/CUSTOM-BASH-TOOLS.md) * [Bash Prompt Utilities](./docs/function-calling/BASH-PROMPT-HELPERS.md) * [First-Class MCP Server Support](./docs/function-calling/MCP-SERVERS.md): Easily connect and interact with MCP servers for advanced functionality. diff --git a/config.example.yaml b/config.example.yaml index b626163..b7c1961 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -46,6 +46,7 @@ enabled_tools: null # Which tools to enable by default. (e.g. 'fs,w visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools') # - demo_py.py # - demo_sh.sh +# - demo_ts.ts - execute_command.sh # - execute_py_code.py # - execute_sql_code.sh @@ -61,6 +62,7 @@ visible_tools: # Which tools are visible to be compiled (and a # - fs_write.sh - get_current_time.sh # - get_current_weather.py +# - get_current_weather.ts - get_current_weather.sh - query_jira_issues.sh # - search_arxiv.sh diff --git a/docs/AGENTS.md b/docs/AGENTS.md index d9d0aea..e9782d8 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -33,6 +33,7 @@ If you're looking for more example agents, refer to the [built-in agents](../ass - [.env File Support](#env-file-support) - [Python-Based Agent Tools](#python-based-agent-tools) - [Bash-Based Agent Tools](#bash-based-agent-tools) + - [TypeScript-Based Agent Tools](#typescript-based-agent-tools) - [5. Conversation Starters](#5-conversation-starters) - [6. Todo System & Auto-Continuation](#6-todo-system--auto-continuation) - [7. Sub-Agent Spawning System](#7-sub-agent-spawning-system) @@ -62,10 +63,12 @@ Agent configurations often have the following directory structure: ├── tools.sh or ├── tools.py + or + ├── tools.ts ``` This means that agent configurations often are only two files: the agent configuration file (`config.yaml`), and the -tool definitions (`agents/my-agent/tools.sh` or `tools.py`). +tool definitions (`agents/my-agent/tools.sh`, `tools.py`, or `tools.ts`). To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml). @@ -114,10 +117,10 @@ isolated environment, so in order for an agent to use a tool or MCP server that explicitly state which tools and/or MCP servers the agent uses. Otherwise, it is assumed that the agent doesn't use any tools outside its own custom defined tools. -And if you don't define a `agents/my-agent/tools.sh` or `agents/my-agent/tools.py`, then the agent is really just a +And if you don't define a `agents/my-agent/tools.sh`, `agents/my-agent/tools.py`, or `agents/my-agent/tools.ts`, then the agent is really just a `role`. -You'll notice there's no settings for agent-specific tooling. This is because they are handled separately and +You'll notice there are no settings for agent-specific tooling. This is because they are handled separately and automatically. See the [Building Tools for Agents](#4-building-tools-for-agents) section below for more information. To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml). @@ -205,7 +208,7 @@ variables: ### Dynamic Instructions Sometimes you may find it useful to dynamically generate instructions on startup. Whether that be via a call to Loki itself to generate them, or by some other means. Loki supports this type of behavior using a special function defined -in your `agents/my-agent/tools.py` or `agents/my-agent/tools.sh`. +in your `agents/my-agent/tools.py`, `agents/my-agent/tools.sh`, or `agents/my-agent/tools.ts`. **Example: Instructions for a JSON-reader agent that specializes on each JSON input it receives** `agents/json-reader/tools.py`: @@ -306,8 +309,8 @@ EOF } ``` -For more information on how to create custom tools for your agent and the structure of the `agent/my-agent/tools.sh` or -`agent/my-agent/tools.py` files, refer to the [Building Tools for Agents](#4-building-tools-for-agents) section below. +For more information on how to create custom tools for your agent and the structure of the `agent/my-agent/tools.sh`, +`agent/my-agent/tools.py`, or `agent/my-agent/tools.ts` files, refer to the [Building Tools for Agents](#4-building-tools-for-agents) section below. #### Variables All the same variable interpolations supported by static instructions is also supported by dynamic instructions. For @@ -337,10 +340,11 @@ defining a single function that gets executed at runtime (e.g. `main` for bash t tools define a number of *subcommands*. ### Limitations -You can only utilize either a bash-based `/agents/my-agent/tools.sh` or a Python-based -`/agents/my-agent/tools.py`. However, if it's easier to achieve a task in one language vs the other, +You can only utilize one of: a bash-based `/agents/my-agent/tools.sh`, a Python-based +`/agents/my-agent/tools.py`, or a TypeScript-based `/agents/my-agent/tools.ts`. +However, if it's easier to achieve a task in one language vs the other, you're free to define other scripts in your agent's configuration directory and reference them from the main -`tools.py/sh` file. **Any scripts *not* named `tools.{py,sh}` will not be picked up by Loki's compiler**, meaning they +tools file. **Any scripts *not* named `tools.{py,sh,ts}` will not be picked up by Loki's compiler**, meaning they can be used like any other set of scripts. It's important to keep in mind the following: @@ -428,6 +432,55 @@ the same syntax ad formatting as is used to create custom bash tools globally. For more information on how to write, [build and test](function-calling/CUSTOM-BASH-TOOLS.md#execute-and-test-your-bash-tools) tools in bash, refer to the [custom bash tools documentation](function-calling/CUSTOM-BASH-TOOLS.md). +### TypeScript-Based Agent Tools +TypeScript-based agent tools work exactly the same as TypeScript global tools. Instead of a single `run` function, +you define as many exported functions as you like. Non-exported functions are private helpers and are invisible to the +LLM. + +**Example:** +`agents/my-agent/tools.ts` +```typescript +/** + * Get your IP information + */ +export async function get_ip_info(): Promise { + const resp = await fetch("https://httpbin.org/ip"); + return await resp.text(); +} + +/** + * Find your public IP address using AWS + */ +export async function get_ip_address_from_aws(): Promise { + const resp = await fetch("https://checkip.amazonaws.com"); + return await resp.text(); +} + +// Non-exported helper — invisible to the LLM +function formatResponse(data: string): string { + return data.trim(); +} +``` + +Loki automatically compiles each exported function as a separate tool for the LLM to call. Just make sure you +follow the same JSDoc and parameter conventions as you would when creating custom TypeScript tools. + +TypeScript agent tools also support dynamic instructions via an exported `_instructions()` function: + +```typescript +import { readFileSync } from "fs"; + +/** + * Generates instructions for the agent dynamically + */ +export function _instructions(): string { + const schema = readFileSync("schema.json", "utf-8"); + return `You are an AI agent that works with the following schema:\n${schema}`; +} +``` + +For more information on how to build tools in TypeScript, refer to the [custom TypeScript tools documentation](function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools). + ## 5. Conversation Starters It's often helpful to also have some conversation starters so users know what kinds of things the agent is capable of doing. These are available in the REPL via the `.starter` command and are selectable. diff --git a/docs/function-calling/CUSTOM-TOOLS.md b/docs/function-calling/CUSTOM-TOOLS.md index 1cabcad..1de68cc 100644 --- a/docs/function-calling/CUSTOM-TOOLS.md +++ b/docs/function-calling/CUSTOM-TOOLS.md @@ -10,6 +10,8 @@ into your Loki setup. This document provides a guide on how to create and use cu - [Environment Variables](#environment-variables) - [Custom Bash-Based Tools](#custom-bash-based-tools) - [Custom Python-Based Tools](#custom-python-based-tools) + - [Custom TypeScript-Based Tools](#custom-typescript-based-tools) +- [Custom Runtime](#custom-runtime) --- @@ -19,9 +21,10 @@ Loki supports custom tools written in the following programming languages: * Python * Bash +* TypeScript ## Creating a Custom Tool -All tools are created as scripts in either Python or Bash. They should be placed in the `functions/tools` directory. +All tools are created as scripts in either Python, Bash, or TypeScript. They should be placed in the `functions/tools` directory. The location of the `functions` directory varies between systems, so you can use the following command to locate your `functions` directory: @@ -81,6 +84,7 @@ Loki and demonstrates how to create a Python-based tool: import os from typing import List, Literal, Optional + def run( string: str, string_enum: Literal["foo", "bar"], @@ -89,26 +93,38 @@ def run( number: float, array: List[str], string_optional: Optional[str] = None, + integer_with_default: int = 42, + boolean_with_default: bool = True, + number_with_default: float = 3.14, + string_with_default: str = "hello", array_optional: Optional[List[str]] = None, ): - """Demonstrates how to create a tool using Python and how to use comments. + """Demonstrates all supported Python parameter types and variations. Args: - string: Define a required string property - string_enum: Define a required string property with enum - boolean: Define a required boolean property - integer: Define a required integer property - number: Define a required number property - array: Define a required string array property - string_optional: Define an optional string property - array_optional: Define an optional string array property + string: A required string property + string_enum: A required string property constrained to specific values + boolean: A required boolean property + integer: A required integer property + number: A required number (float) property + array: A required string array property + string_optional: An optional string property (Optional[str] with None default) + integer_with_default: An optional integer with a non-None default value + boolean_with_default: An optional boolean with a default value + number_with_default: An optional number with a default value + string_with_default: An optional string with a default value + array_optional: An optional string array property """ output = f"""string: {string} string_enum: {string_enum} -string_optional: {string_optional} boolean: {boolean} integer: {integer} number: {number} array: {array} +string_optional: {string_optional} +integer_with_default: {integer_with_default} +boolean_with_default: {boolean_with_default} +number_with_default: {number_with_default} +string_with_default: {string_with_default} array_optional: {array_optional}""" for key, value in os.environ.items(): @@ -117,3 +133,150 @@ array_optional: {array_optional}""" return output ``` + +### Custom TypeScript-Based Tools +Loki supports tools written in TypeScript. TypeScript tools require [Node.js](https://nodejs.org/) and +[tsx](https://tsx.is/) (`npx tsx` is used as the default runtime). + +Each TypeScript-based tool must follow a specific structure in order for Loki to properly compile and execute it: + +* The tool must be a TypeScript file with a `.ts` file extension. +* The tool must have an `export function run(...)` that serves as the entry point for the tool. + * Non-exported functions are ignored by the compiler and can be used as private helpers. +* The `run` function must accept flat parameters that define the inputs for the tool. + * Always use type annotations to specify the data type of each parameter. + * Use `param?: type` or `type | null` to indicate optional parameters. + * Use `param: type = value` for parameters with default values. +* The `run` function must return a `string` (or `Promise` for async functions). + * For TypeScript, the return value is automatically written to the `LLM_OUTPUT` environment variable, so there's + no need to explicitly write to the environment variable within the function. +* The function must have a JSDoc comment that describes the tool and its parameters. + * Each parameter should be documented using `@param name - description` tags. + * These descriptions are passed to the LLM as the tool description, letting the LLM know what the tool does and + how to use it. +* Async functions (`export async function run(...)`) are fully supported and handled transparently. + +**Supported Parameter Types:** + +| TypeScript Type | JSON Schema | Notes | +|-------------------|--------------------------------------------------|-----------------------------| +| `string` | `{"type": "string"}` | Required string | +| `number` | `{"type": "number"}` | Required number | +| `boolean` | `{"type": "boolean"}` | Required boolean | +| `string[]` | `{"type": "array", "items": {"type": "string"}}` | Array (bracket syntax) | +| `Array` | `{"type": "array", "items": {"type": "string"}}` | Array (generic syntax) | +| `"foo" \| "bar"` | `{"type": "string", "enum": ["foo", "bar"]}` | String enum (literal union) | +| `param?: string` | `{"type": "string"}` (not required) | Optional via question mark | +| `string \| null` | `{"type": "string"}` (not required) | Optional via null union | +| `param = "value"` | `{"type": "string"}` (not required) | Optional via default value | + +**Unsupported Patterns (will produce a compile error):** + +* Rest parameters (`...args: string[]`) +* Destructured object parameters (`{ a, b }: { a: string, b: string }`) +* Arrow functions (`const run = (x: string) => ...`) +* Function expressions (`const run = function(x: string) { ... }`) + +Only `export function` declarations are recognized. Non-exported functions are invisible to the compiler. + +Below is the [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) tool definition that comes pre-packaged with +Loki and demonstrates how to create a TypeScript-based tool: + +```typescript +/** + * Demonstrates all supported TypeScript parameter types and variations. + * + * @param string - A required string property + * @param string_enum - A required string property constrained to specific values + * @param boolean - A required boolean property + * @param number - A required number property + * @param array_bracket - A required string array using bracket syntax + * @param array_generic - A required string array using generic syntax + * @param string_optional - An optional string using the question mark syntax + * @param string_nullable - An optional string using the union-with-null syntax + * @param number_with_default - An optional number with a default value + * @param boolean_with_default - An optional boolean with a default value + * @param string_with_default - An optional string with a default value + * @param array_optional - An optional string array using the question mark syntax + */ +export function run( + string: string, + string_enum: "foo" | "bar", + boolean: boolean, + number: number, + array_bracket: string[], + array_generic: Array, + string_optional?: string, + string_nullable: string | null = null, + number_with_default: number = 42, + boolean_with_default: boolean = true, + string_with_default: string = "hello", + array_optional?: string[], +): string { + const parts = [ + `string: ${string}`, + `string_enum: ${string_enum}`, + `boolean: ${boolean}`, + `number: ${number}`, + `array_bracket: ${JSON.stringify(array_bracket)}`, + `array_generic: ${JSON.stringify(array_generic)}`, + `string_optional: ${string_optional}`, + `string_nullable: ${string_nullable}`, + `number_with_default: ${number_with_default}`, + `boolean_with_default: ${boolean_with_default}`, + `string_with_default: ${string_with_default}`, + `array_optional: ${JSON.stringify(array_optional)}`, + ]; + + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith("LLM_")) { + parts.push(`${key}: ${value}`); + } + } + + return parts.join("\n"); +} +``` + +## Custom Runtime +By default, Loki uses the following runtimes to execute tools: + +| Language | Default Runtime | Requirement | +|------------|-----------------|--------------------------------| +| Python | `python` | Python 3 on `$PATH` | +| TypeScript | `npx tsx` | Node.js + tsx (`npm i -g tsx`) | +| Bash | `bash` | Bash on `$PATH` | + +You can override the runtime for Python and TypeScript tools using a **shebang line** (`#!`) at the top of your +script. Loki reads the first line of each tool file; if it starts with `#!`, the specified interpreter is used instead +of the default. + +**Examples:** + +```python +#!/usr/bin/env python3.11 +# This Python tool will be executed with python3.11 instead of the default `python` + +def run(name: str): + """Greet someone. + Args: + name: The name to greet + """ + return f"Hello, {name}!" +``` + +```typescript +#!/usr/bin/env bun +// This TypeScript tool will be executed with Bun instead of the default `npx tsx` + +/** + * Greet someone. + * @param name - The name to greet + */ +export function run(name: string): string { + return `Hello, ${name}!`; +} +``` + +This is useful for pinning a specific Python version, using an alternative TypeScript runtime like +[Bun](https://bun.sh/) or [Deno](https://deno.com/), or working with virtual environments. diff --git a/docs/function-calling/TOOLS.md b/docs/function-calling/TOOLS.md index 7aec07b..80bf392 100644 --- a/docs/function-calling/TOOLS.md +++ b/docs/function-calling/TOOLS.md @@ -32,6 +32,7 @@ be enabled/disabled can be found in the [Configuration](#configuration) section |-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| | [`demo_py.py`](../../assets/functions/tools/demo_py.py) | Demonstrates how to create a tool using Python and how to use comments. | 🔴 | | [`demo_sh.sh`](../../assets/functions/tools/demo_sh.sh) | Demonstrate how to create a tool using Bash and how to use comment tags. | 🔴 | +| [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) | Demonstrates how to create a tool using TypeScript and how to use JSDoc comments. | 🔴 | | [`execute_command.sh`](../../assets/functions/tools/execute_command.sh) | Execute the shell command. | 🟢 | | [`execute_py_code.py`](../../assets/functions/tools/execute_py_code.py) | Execute the given Python code. | 🔴 | | [`execute_sql_code.sh`](../../assets/functions/tools/execute_sql_code.sh) | Execute SQL code. | 🔴 | @@ -49,6 +50,7 @@ be enabled/disabled can be found in the [Configuration](#configuration) section | [`get_current_time.sh`](../../assets/functions/tools/get_current_time.sh) | Get the current time. | 🟢 | | [`get_current_weather.py`](../../assets/functions/tools/get_current_weather.py) | Get the current weather in a given location (Python implementation) | 🔴 | | [`get_current_weather.sh`](../../assets/functions/tools/get_current_weather.sh) | Get the current weather in a given location. | 🟢 | +| [`get_current_weather.ts`](../../assets/functions/tools/get_current_weather.ts) | Get the current weather in a given location (TypeScript implementation) | 🔴 | | [`query_jira_issues.sh`](../../assets/functions/tools/query_jira_issues.sh) | Query for jira issues using a Jira Query Language (JQL) query. | 🟢 | | [`search_arxiv.sh`](../../assets/functions/tools/search_arxiv.sh) | Search arXiv using the given search query and return the top papers. | 🔴 | | [`search_wikipedia.sh`](../../assets/functions/tools/search_wikipedia.sh) | Search Wikipedia using the given search query.
Use it to get detailed information about a public figure, interpretation of a
complex scientific concept or in-depth connectivity of a significant historical
event, etc. | 🔴 |