Ragged is a 0-dependency, lightweight, universal LLM client for JavaScript and Typescript. It makes it easy to access LLMs via a simple, easy to understand, and uncomplicated API.
The heart of Ragged is a simple abstraction that allows you to interact with LLMs in a consistent way, regardless of the provider or model you are using. This abstraction makes it easy to switch between different LLMs without having to change your code.
Ragged's job is to be a low-level connector library. it is, explicitly, not a framework; but it is meant to be easy to use. You can build your own framework on top of Ragged, or use it as a standalone library in your existing projects.
Installing Ragged is very easy.
# either
npm install --save-exact ragged
# or
pnpm add --save-exact ragged
# or
yarn add --exact ragged
That's it. You're ready to go!
Ragged's core Chat Completion abstraction is an easy-to-use Message
interface.
import type { Message } from "ragged";
const history: Message[] = [
{ type: "user", text: "What is a rickroll?" },
{ type: "bot", text: "A rickroll is a prank..." }
]
Because this interface is standard, a lot of operations become very easy to perform. For example, you can access the last message in the history using the .at
method.
console.log(history.at(-1)?.text); // A rickroll is a prank...
Or, you can simply modify history by pushing new messages to the array.
history.push({ type: "bot", text: "I'm a bot!" });
About 90% of Ragged is built around this simple interface. (The other 10% is for Embeddings, which has its own analog to the Message
type). This standard interface is the same across all LLM providers, making it easy to switch between providers without changing your code.
In the following sections, we will show you how to use Ragged to perform many complex operations with ease, including chat completion, tool calling, multimodal input, agent creation, and more.
- Ragged
- Development Instructions
Ragged is very easy to use. Here is a complete application that shows chat completion.
import { Chat } from "ragged"
// create a new Chat instance with the OpenAI provider
const c = Chat.with({
provider: 'openai',
config: { apiKey: process.env.OPENAI_API_KEY }
});
// chat with the model
const {history} = await c.chat('What is a rickroll?');
// {history}.at(-1) is a native JS array method for the last element
console.log(history.at(-1)?.text); // A rickroll is a prank...
Nothing to it!
Tip
You can see an example of how to use the history functionality in the examples folder. Click here to see the example. This is a fully working example, so if you set up Ragged locally, you can execute the example using pnpm run:example history.ts
.
By default, each instance of the Chat
object records the history of the conversation. You can access the history of the conversation using the .history
property.
console.log(c.history);
This array gets updated with each call to the chat
method.
You can also set the history of the conversation by setting the .history
property to an array of messages. This way, you can control the history of the conversation.
const history = c.history;
You can access the history using the .history
property.
console.log(c.history); // [ { text: 'What is a rickroll?' ... ]
You can also access the last message in the history using the .at
method.
console.log(c.history.at(-1)?.text); // A rickroll is a prank...
Tip
The .at
method is a native JavaScript array method that allows you to access elements in an array using negative indices. This is useful for accessing the last element in an array. The .at()
method has been available in JavaScript since ES2022. See MDN documentation.
You can set the history by setting the .history
property to an array of messages.
c.history = [
{ type: "user", text: "What is a rickroll?" },
{ type: "bot", text: "A rickroll is a prank..." }
];
You can clear the history by setting the .history
property to an empty array.
c.history = [];
Warning
Never modify elements inside the history object directly. Always set the .history
property to a new array. This prevents unexpected behavior and makes the code more predictable.
Tip
You can see an example of how to use the freezing functionality in the examples folder. Click here to see the example. This is a fully working example, so if you set up Ragged locally, you can execute the example using pnpm run:example frozen.ts
.
You can turn recording on and off by passing a boolean to the .record
method. To turn recording off, pass false
. We call this "freezing" the conversation. When the conversation is frozen, the history will not be updated with each call.
// Recording is on by default. Here is how you can turn it off.
c.record(false);
This is useful when you want to create multiple responses to a single prompt. Then, you can prompt the model multiple times. Each time, the model will respond as if it were the first time, and the history will not be updated with each call.
// Turn off history
c.record(false);
// Chat with the model
const response1 = await c.chat('Remember that my name is "John."');
// Response: Okay! I will remember that your name is "John."
const response2 = await c.chat('What is my name?');
// Response: I do not know your name. Please tell me.
Ragged supports multimodal input. This means that you can pass images to the LLM along with text. This is useful for creating more interactive and engaging chat experiences.
Currently we only support base64 encoded images, but will expand this support in the future.
// chat with the model
const { history } = await c.chat([
{
type: "user",
text: "What do these images contain? Describe them.",
attachments: [
{
type: "image",
payload: {
data: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=",
encoding: "base64_data_url",
mimeType: "image/png",
},
},
{
type: "image",
payload: {
encoding: "base64_data_url",
data: "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC",
mimeType: "image/png",
},
},
],
},
], {
model: "gpt-4o"
});
// log the messages
console.log(history.at(-1)?.text);
// Output:
// The first image is an emoji of a face with heart-shaped eyes, typically used to express love, adoration, or strong liking for something or someone.
// The second image is a gradient background that transitions from dark to light colors. The colors include black, brown, orange at the top, and transition through white and blue towards the bottom.
Tip
You can see 3 examples of how to use the tool calling functionality in the examples folder. The Simple Example shows a very simple tool calling example that returns some mock data (although the LLM thinks it's coming from an actual website). The Fetch BBC News RSS Feed Example is a simple, single-tool example that shows how to give the LLM the ability to fetch
the latest news from the BBC RSS feed. The List Files example is a more complex example that shows how to use multiple tools in order to let the LLM read your local file system. These are fully working examples, so if you set up Ragged locally, you can execute the example using pnpm run:example tools.ts
.
Ragged allows you to further extend its functionality using tools. This gives you the power to integrate custom behavior or commands directly into your chat-based application.
Tools are very powerful! You can use tools to fetch data from external APIs, perform calculations, generate dynamic content, and much more. Tools can be used to extend the capabilities of the LLM and create more interactive and dynamic chat experiences.
To define a tool, first we import the Tool type from Ragged and then define a tool object.
import type { Tool } from "ragged";
const getHomepageTool: Tool = {
id: "get-homepage-contents",
description: "Gets the contents of my homepage.",
handler: async () => {
return Promise.resolve("Hello! My name is John! I'm a student at a community college!")
}
}
In this example, we define a tool called getHomepageTool
. This tool has the following properties:
id
: A unique identifier for the tool. This identifier is how the LLM references the tool in the chat. The LLM will be aware of the tool's existence and will be able to use it in the chat.description
: A brief description of what the tool does. This description is used to help the LLM understand the purpose of the tool.handler
is a function that processes the input and returns the output. It handles the logic of the tool and manages errors gracefully.
Once you have defined a tool, you can use it in a chat interaction by passing it to the chat
method.
const { history } = await c.chat("Get the contents of my homepage.", {
// Pass the tool to the chat method.
tools: [getHomepageTool],
model: "gpt-3.5-turbo"
});
console.log(history.at(-1)?.text);
// RESPONSE: I retrieved the contents of your homepage. It says: "Hello! My name is John! I'm a student at a community college!"
Putting it all together, here's what it looks like:
import { Chat } from "ragged"
import type { Tool } from "ragged";
const getHomepageTool: Tool = {
id: "get-homepage-contents",
description: "Gets the contents of my homepage.",
props: {
type: "object",
description: "The properties of the tool.",
props: {}
},
handler: async () => {
return Promise.resolve("Hello! My name is John! I'm a student at a community college!")
}
}
const c = Chat.with({
provider: 'openai',
config: {
apiKey: process.env.OPENAI_API_KEY
// You can also pass the organization ID here.
organizationId: process.env.OPENAI_ORGANIZATION_ID
}
});
const { history } = await c.chat("Get the contents of my homepage.", {
// Pass the tool to the chat method.
tools: [getHomepageTool],
model: "gpt-3.5-turbo"
});
console.log(history.at(-1)?.text);
// RESPONSE: I retrieved the contents of your homepage. It says: "Hello! My name is John! I'm a student at a community college!"
The Tool
object can also take an optional props
object. This object allows the LLM to pass pass additional information to the tool handler. This can be useful if you want the LLM to pass configuration options, data, or other information to the tool handler.
Here is an example of how to use the props
object:
import type { Tool } from "ragged";
const fetchTool: Tool = {
id: "fetch",
description: "Do a simple GET call and retrieve the contents of a URL.",
// The props object describes the expected input for the tool.
props: {
type: "object",
props: {
url: {
type: "string",
description: "The URL to fetch.",
required: true
}
}
},
// The handler function processes the input and returns the output.
handler: async (props: string) => {
// Props are passed to the handler function as a string.
// This string needs to be parsed into an object before it can be used.
// Several examples can be seen in the `/examples` folder.
}
}
Here, we define a tool called fetchTool
. This tool has a props
object that describes the expected input for the tool. The props
object contains a url
property that is required and must be a string. The handler
function processes the input and returns the output.
Warning
It's important to note that the LLM can hallucinate the props object. This means that the LLM can pass props which are not defined in the props object. This is dangerous, as it can lead to unexpected behavior. To prevent this, you should always validate the props object before using it in your tool handler.
You can get the raw request and response objects from the tool handler by using the raw
property. This property contains the raw vanillla Request
and Response
objects that were sent to and received from the tool handler.
const { history, raw } = await c.chat("What is a rickroll?");
console.log(history.at(-1)?.text); // A rickroll is a prank...
console.log(raw?.requests); // All API requests done during the chat (there can be more than 1 if you are doing tool calling)
console.log(raw?.responses); // All API responses done during the chat (there can be more than 1 if you are doing tool calling)
Tip
You can see 2 examples of how to use the agent functionality in the examples folder. The Simple Example shows a very simple agent that increments a number automatically. The Multiple Agents Example uses multiple agents to generate tweets. These are fully working examples, so if you set up Ragged locally, you can execute the example using pnpm run:example agents-simple.ts
or pnpm run:example agents-multiple.ts
.
Many LLM frameworks support autonomous agents. These are tools that can be used to perform tasks without user input. They can be used to automate repetitive tasks, provide real-time information, or interact with external services.
In many LLM frameworks, agents are complex and require a lot of setup. But in Ragged, agents are very simple to implement using normal, easy-to-understand code. Agents are just pieces of code that take input and return output in a recursive or repetive way.
The simplest agent has the following main components:
- A starting state
- A loop which calls an LLM to mutate the state recursively
- A stop condition which determines when the agent should stop
Using these components, you can create agents that perform a wide variety of tasks.
Here is an example of a simple agent that generates a conversation with an LLM. You can also access it here: examples/nodejs/agents-simple.ts. If you run this code, you will see the agent incrementing a number automatically.
/**
* This example demonstrates how to build a simple agent that increments a number automatically.
* This is a very simple example, but the principles can be applied to more complex agents.
*
* EXPECTED OUTPUT:
* The current number is 1.
* The current number is 2.
* The current number is 3.
* The current number is 4.
* The current number is 5.
* The current number is 6.
* The current number is 7.
* The current number is 8.
* The current number is 9.
* The current number is 10.
* The agent has reached the stop condition.
*/
import { config } from 'dotenv';
config();
import { Chat } from "ragged"
// Define the main function
async function main() {
const c = Chat.with({
provider: 'openai',
config: { apiKey: process.env.OPENAI_API_KEY }
});
c.record(false);
// Start with the initial state
let currentState = `The current number is 1.`;
// Define the stop condition
const stopCondition = () => currentState.includes("10");
// Start iterating
console.log(currentState);
while (!stopCondition()) {
currentState = await getNextNumber(c, currentState);
console.log(currentState);
}
// Print the stop condition
console.log("The agent has reached the stop condition.");
}
// Define the agent function
async function getNextNumber(c: Chat, input: string): Promise<string> {
// Call the LLM with the input
const { history } = await c.chat([
{
type: "system",
text: `
The user will state that "The current number is X". Output "The current number is X+1". Examples:
If the user input is empty, malformed, or not a number, return "The current number is 1."
EXAMPLES:
User: The current number is 1.
AI: The current number is 2.
User: The current number is 2.
AI: The current number is 3.
User: The current number is 3.
AI: The current number is 4.
// If the user input is malformed
User: The current number is
AI: The current number is 1.
// If the user input is empty
User:
AI: The current number is 1.
// If the user input is not a number
User: The current number is yellow.
AI: The current number is 1.
`
},
{
type: "user",
text: input
}
]);
// Get the last message from the response
const lastMessage = history.at(-1)?.text;
return lastMessage || "";
}
// run the code
await main();
Agents can get very complex, with multiple agents running at the same time. Here is an example of a simple chat application that uses multiple agents to generate a conversation: examples/nodejs/agents-multiple.ts.
Ragged has a built-in logging system that allows you to log messages to the console. This is useful for debugging and troubleshooting your code. You can use the Logger
class to control the logging level and format of the log messages.
import { Logger } from "ragged";
Logger.setLogLevel('debug'); // make it verbose
Logger.setLogLevel('info'); // default
Logger.setLogLevel('warn'); // only log warnings
Logger.setLogLevel('error'); // only log errors
Logger.setLogLevel('none'); // turn off logging altogether
Ragged provides a powerful and flexible hook system that allows you to customize and extend the behavior of the API client. Hooks are functions that are executed at specific points during the request/response lifecycle, allowing you to modify requests, handle responses, and perform additional actions based on the API interactions.
There are three types of hooks in Ragged:
- BeforeRequestHook
- AfterResponseHook
- AfterResponseParsedHook
Hooks can be asynchronous, allowing you to perform asynchronous operations such as making additional API calls, reading/writing files, or interacting with databases. You can use async
functions as your hooks to handle asynchronous operations.
Each hook type receives a specific context object that contains relevant information about the request or response. The context objects share a base structure and have additional properties specific to the hook type.
The BaseHookContext
contains common properties available to all hooks. Each hook also may have additional properties specific to its type.
type BaseHookContext = {
apiClient: ApiClient;
requestParams: {
method: string;
url: string;
headers?: RequestInit["headers"];
body?: any;
}
}
The BeforeSerializeHook
is executed before the request is serialized. This allows you to modify the request parameters before they are sent to the API. Its context includes the requestParams
object.
Note that after the BeforeSerializeHook is executed, the requestParams are frozen and cannot be modified in subsequent hooks.
Example Usage:
c.chat(`Hello, World!`, {
hooks: {
beforeSerialize: ({ requestParams }) => {
// You could log the request parameters before they are serialized
console.log("Before serialize:", requestParams);
// You could also modify the request parameters
requestParams.url = "http://modified.com";
requestParams.body.some = "modified-field";
if (requestParams.method === "PUT" || requestParams.method === "PATCH") {
requestParams.method = "POST";
}
}
}
})
The BeforeRequestHook
is executed before the request is sent. This allows you to modify the request or perform actions before the request is made. Its context includes the request
object.
Example Usage:
c.chat(`Hello, World!`, {
hooks: {
beforeRequest: ({ request }) => {
// You could log the request before it is sent
console.log("Before request:", request);
// You could also modify the request headers
request.headers.append('X-Custom-Header', 'custom-value');
}
}
})
The AfterResponseHook
is executed after the response is received but before it is parsed. This allows you to handle raw responses or perform actions based on the response status. Its context includes both the request
and response
objects.
Be careful not to use the .json()
or .text()
methods on the response object in this hook! This will cause the response to be consumed and will prevent it from being parsed later, which will cause an error to be thrown by the chat
method.
Example Usage:
c.chat(`Hello, World!`, {
hooks: {
afterResponse: ({ response }) => {
// You could log the response JSON
console.log("Logging the raw response:", response);
}
}
});
The AfterResponseParsedHook
is executed after the response is parsed into JSON. This allows you to handle the parsed response or perform actions based on the response data.
Example Usage:
c.chat(`Hello, World!`, {
hooks: {
afterResponseParsed: async ({ json }) => {
// You could log the response JSON
console.log("JSON response:", json);
// You could also modify it
json.data = "Hello, World!";
}
}
});
All hooks can be asynchronous, allowing you to perform asynchronous operations such as making additional API calls, reading/writing files, or interacting with databases. You can use async
functions as your hooks to handle asynchronous operations.
c.chat(`Hello, World!`, {
hooks: {
// notice the async keyword... hooks can be async!
afterResponseParsed: async ({ json }) => {
await sendToSlack(json);
}
}
});
import { config } from "dotenv";
config();
import { Chat } from "ragged";
const c = Chat.with({
provider: 'openai',
config: {
apiKey: process.env.OPENAI_API_KEY
}
});
c.chat(`say hello world`, {
hooks: {
beforeSerialize: ({ requestParams }) => {
// You could log the request parameters before they are serialized
console.log("Before serialize:", requestParams);
// You could also modify the request parameters
requestParams.url = "http://modified.com";
requestParams.body.some = "modified-field";
if (requestParams.method === "PUT" || requestParams.method === "PATCH") {
requestParams.method = "POST";
}
}
beforeRequest: ({ request }) => {
// Print the Content-Type header value, just to test the hook
console.log("We will be sending the Content-Type header with value: ",
request.headers.get('Content-Type'));
},
afterResponse: ({ response }) => {
// Get the rate limit info from the response headers. This is very useful!
console.log("Received rate limit info from OpenAI: ",
Array.from(response.headers.entries())
.filter(([key]) => key.startsWith('x-ratelimit')));
},
afterResponseParsed: (context) => {
// Finally, print the raw JSON response from OpenAI
console.log("Raw OpenAI response JSON: ",
context.json);
}
}
});
Ragged supports multiple LLM providers out of the box. You can use these providers to interact with the LLMs and generate responses to your prompts.
const c = Chat.with({
provider: 'openai',
config: { apiKey: process.env.OPENAI_API_KEY }
});
await c.chat('What is a rickroll?', { model: 'gpt-4' });
Warning
This is a new provider, and tool calling is not yet functional.
const c = Chat.with({
provider: 'cohere',
config: { apiKey: process.env.COHERE_API_KEY }
});
await c.chat('What is a rickroll?', { model: 'command-nightly' });
Warning
This is a new provider, and tool calling is not yet functional.
The OpenAI Assistants adapter allows you to interact with the OpenAI Assistants API. The adapter creates assistants, threads, messages, and runs under the hood for you. As a result, you get a relatively streamlined experience.
const c = Chat.with({
provider: 'openai-assistants',
config: {
apiKey: '123',
assistant: {
model: 'model',
description: 'description',
instructions: 'instructions',
name: 'name',
}
}
});
await c.chat('What is a rickroll?', { model: 'gpt-4o' });
Warning
This is a new provider, and tool calling is not yet functional.
const c = Chat.with({
provider: 'azure-openai',
config: {
apiKey: '123',
apiVersion: 'v1',
resourceName: 'resource',
deploymentName: 'deployment'
}
});
await c.chat('What is a rickroll?', { model: 'gpt-4o' });
Warning
This is a new provider, and tool calling is not yet functional.
The Azure OpenAI Assistants adapter allows you to interact with the OpenAI Assistants API. The adapter creates assistants, threads, messages, and runs under the hood for you. As a result, you get a relatively streamlined experience.
const c = Chat.with({
provider: 'azure-openai-assistants',
config: {
apiKey: '123',
apiVersion: 'v1',
resourceName: 'resource',
deploymentName: 'deployment',
modelName: 'model'
}
});
await c.chat('What is a rickroll?', { model: 'gpt-4o' });
Warning
This is a new provider, and tool calling is not yet functional.
const c = Chat.with({
provider: 'ollama',
config: {}
})
await c.chat('What is a rickroll?', { model: 'gpt-4o' });
Feature | Is Working | API Frozen* |
---|---|---|
Chat Completion | π’ 100% | β |
Embeddings Generation | π’ 100% | β |
In-built Message History | π’ 100% | β |
Write your own custom LLM adapters | π’ 100% | β |
Multimodal Input | π‘ 50% | β |
Tool Calling | π‘ 30% | β |
Autonomous Agents | π’ 100% | β |
Message History | π’ 100% | β |
Helpful Errors | π’ 100% | β |
Rate Limits | π‘ 30% | β |
Streaming | π΄ 0% | β |
Model Fine-Tuning | π΄ 0% | β |
Multimodal Generation | π΄ 0% | β |
* By "API Frozen," we mean that these features will not change in a breaking way. We will add new features, but we will not change the existing interface in a way that breaks existing code.
The following table lists the providers and models that Ragged Comes with out of the box. If you want to use a different provider or model, you can create a custom adapter.
Provider | Models | Chat | Embeddings | Tool Calls | Multimodal |
---|---|---|---|---|---|
OpenAI | GPT: 4o, 4T, 4, 3.5 | β | β | β | β |
OpenAI Assistants | GPT: 4o, 4T, 4, 3.5 | β | β | β | β |
Azure OpenAI | GPT: 4, 4T, 3.5 | β | β | β | β |
Azure OpenAI Assistants | GPT: 4, 4T, 3.5 | β | β | β | β |
Together | Several OSS Models | β | β | β | β |
Cohere | CommandR, Command | β | β | β | β |
Anthropic | Claude 2, Claude 3 | β | β | β | β |
Mistral | 7B, 8x7B, S, M & L | β | β | β | β |
Groq | Lama2-70B, Mixtral-8x7b | β | β | β | β |
DeepSeek | Chat and Code | β | β | β | β |
Ollama | All models | β | β | β | β |
Google Gemini | Gemini: Flash, Pro | β | β | β | β |
Hugging Face | OSS Model | β | β | β | β |
Usually, we would use Chat.with
to create a new instance of the Chat
class. This is the easiest way to create a new instance of the Chat
class, as it automatically selects the correct adapter based on the provider name.
However, you can also create a new instance of the Chat
class using the constructor directly. This allows you to use your own custom adapters with the Chat
class. This is useful if you want to use a different model or if you want to use a different API that is not supported by the built-in adapters. It is also useful if you want to mock the adapter for testing purposes.
Custom adapters must implement the BaseChatAdapter
interface. This interface defines the chat
method, which is used to interact with the LLM provider.
The chat
method takes a ChatRequest
object as input and returns a ChatResponse
object as output.
You do not need to manage history or state in your custom adapter. The Chat
class will handle that for you. You just need to return the latest messages from the LLM in the return object of the chat
method.
Here are some examples of how to create a new instance of the Chat
class using the constructor and custom adapters.
Tip
You can see an example of how to use an inline adapter in the examples folder. Click here to see the example. This is a fully working example, so if you set up Ragged locally, you can execute the example using pnpm run:example custom-adapter-inline.ts
.
An inline adapter is a simple way to create a custom adapter. You can define the adapter inline when you create a new instance of the Chat
class.
import { Chat } from "ragged"
import type { ChatAdapterRequest ,ChatAdapterResponse } from "ragged"
const c = new Chat({
chat: async (request: ChatAdapterRequest): Promise<ChatAdapterResponse> => {
// Make your API calls here, then return the mapped response.
// request.context.apiClient.post('https://some-llm.com', { text: request.text })
return { history: [] };
}
});
Tip
You can see an example of how to use an object adapter in the examples folder. Click here to see the example. This is a fully working example, so if you set up Ragged locally, you can execute the example using pnpm run:example custom-adapter-object.ts
.
You could also create a custom adapter as an object and pass it to the constructor. This is useful if you want to store the adapter in a separate variable or if you want to reuse the adapter in multiple places.
import { Chat } from "ragged"
import type { BaseChatAdapter } from "ragged"
const adapter: BaseChatAdapter = {
chat: async (request: ChatAdapterRequest): Promise<ChatAdapterResponse> => {
// Make your API calls here, then return the mapped response.
// request.context.apiClient.post('https://some-llm.com', { text: request.text })
return { history: [] };
}
}
const c = new Chat(adapter);
Tip
You can see an example of how to use an object adapter in the examples folder. Click here to see the example. This is a fully working example, so if you set up Ragged locally, you can execute the example using pnpm run:example custom-adapter-class.ts
.
You could also create a custom adapter as a class and pass it to the constructor. This is useful if you want to use inheritance or if you want to use a constructor function, or store state.
import { Chat, BaseChatAdapter, ChatAdapterRequest, ChatAdapterResponse } from "ragged"
class ExampleAdapter implements BaseChatAdapter {
async chat(request: ChatAdapterRequest): Promise<ChatAdapterResponse> {
// Make your API calls here, then return the mapped response.
// request.context.apiClient.post('https://some-llm.com', { text: request.text })
return { history: [] };
}
}
const c = new Chat(new ExampleAdapter());
Resource pooling is a technique used to manage resources efficiently. It allows you to use multiple LLMs with different API keys in the same chat history. This is useful if you want to use different models or different providers in the same chat session.
Since every app will have some level of custom resource pooling, Ragged does not provide a built-in resource pooling mechanism. Instead, you can use the Chat
class and its adapters mechanism to manage resource pooling in your app.
See the Resource Pooling example in the examples folder for a working example of how to use resource pooling in Ragged.
These instructions will get you a copy of Ragged up and running on your local machine for development and testing purposes. If you are not working on Ragged's own source code, you can skip all documentation past this point.
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
You will need to have Node.js installed on your machine. You can download it from the official website, but we recommend using a version manager like nvm
or nodenv
to manage your Node.js installations.
You will also need pnpm. Please see the official website for installation instructions.
Development is easy. First, clone the repository to your local machine.
You will then have to clone the repository to your local machine.
# This will create a new directory called "ragged" in your current directory, and clone the repository into it.
git clone [email protected]:monarchwadia/ragged.git
Then, change into the ragged
directory.
cd ragged
Then, install all dependencies for all projects. In pnpm
this is easy.
# -r is short for --recursive, which means the command is run in all projects.
# Projects are defined in the `pnpm-workspace.yaml` file.
pnpm -r install
Now that you have pnpm
installed, you are mostly ready to go. You can enter TDD mode using the following command (or by setting it up on your IDE).
# make sure youre in the /ragged subfolder
cd ragged
pnpm tdd
This will run the tests in watch mode. You can now make changes to the code and see the tests run automatically.
If you want to run the tests once, you can use the following command.
# make sure youre in the /ragged subfolder
cd ragged
pnpm test
To manually test your local build of Ragged without publishing it to npm
, you can use the following command.
# make sure youre in the /ragged subfolder
cd ragged
pnpm build
pnpm link --global
Important
It's absolutely necessary to rebuild the project after every code change that you want to test. This is because the pnpm link --global
command creates a symlink to the build output, not to the source code. Thankfully it is not necessary to run pnpm link --global
after every build. Usually this is not a problem because using Test-Driven Development, i.e. the pnpm tdd
command, we know the outcome of our changes without building them. Therefore, we can build only when we want to test the changes manually.
This will create a global symlink to your local build of Ragged. You can then use this build in other projects by running the following command in a project that uses Ragged:
pnpm link --global ragged
This will link the global Ragged build to your project. You can now use your local build of Ragged in your project.
PollyJS is a tool that we use to record and replay API interactions. This is useful for testing and development, as it allows us to test our code against real API responses without making actual API calls, and without having to worry about rate limits or other issues. It also removes the need to manually mock API responses, which can be time-consuming and error-prone.
PollyJS recordings are stored in the recordings
folder. Each recording is a JSON file that contains the request and response data for a single API interaction. The recordings are used by the unit tests to simulate API interactions. When the tests run, they check the recordings to see if there is a matching response for the request. If there is, the response is used. If there isn't, then the test makes a real API call and records the response.
First, make sure you have the .env
file set up with the necessary API keys. You can copy the .env.sample
file to .env
and fill in the necessary API keys.
If you want to refresh a specific PollyJS recording, then you'll have to pass { refresh: true }
to startPollyRecording
in the test file. This will cause the test to make a real API call and record the response, overwriting the existing recording.
const polly = startPollyRecording("name of the test", { refresh: true });
If you want to refresh ALL PollyJS recordings, you can set the REFRESH_POLY_RECORDINGS
environment variable to true
before running the tests. This will cause the tests to make real API calls and record the responses, overwriting any existing recordings.
REFRESH_POLY_RECORDINGS=true pnpm test
Ragged has a simple folder structure. Here is a brief overview of the folders and their contents.
ragged
contains the main codebase for Ragged. This is where you will find the source code for the library itself.
Within the ragged
folder, you will find the following files and folders:
buildConfig
contains the configuration for the build process. We use esbuild to build the library. This folder contains the configuration for esbuild.recordings
contain Pollyjs recordings of various API calls. These recordings are used for testing and development. Do not modify these files directly. Instead, work with Pollyjs to record new interactions.src
contains the source code for Ragged. This is where you will find the main codebase for the library.src/chat
contains the code for theChat
class.src/chat/adapter
contains the code for the built-in adapters for theChat
class. These adapters are used to interact with the various LLM providers. You can add new adapters here if you want to support a new provider.src/public
is a folder that dynamically defines the public API for the raggednpm
module (i.e. what you can import usingimport * from "ragged/....."
). The folder structure insrc/public
is mapped topackage.json
'sexports
field, and is also mapped toesbuild
'sentryPoints
field. This is kind of neat and I don't know if this is a common pattern, but it works well for us.src/support
contains support code that is used by the main codebase. This includes the API client, custom errors, JSON parsing, logging, and other shared code.src/tools
contains the code for theTool
class, which enables tool calls in Ragged./test
contains test utilities, used in*.test.ts
files..env.sample
is a sample.env
file that you can use to set environment variables. Right now it only contains placeholders for various LLM API keys, and is mainly used in the unit tests to generate PollyJS recordings.
.github
contains GitHub Actions workflows that are used to automate the testing and deployment process. I.e. this is the CI/CD pipeline for deploying Ragged tonpm
and for deploying the documentation to GitHub Pages..vscode
contains Visual Studio Code settings that are used to configure the editor for Ragged development. You can ignore this folder if you are not using Visual Studio Code.documentation
contains a Docusaurus project that is used to generate the documentation for Ragged. Currently, this is out of date and not deployed. We may delete this folder in the future, or we may update it and deploy it.examples
contains example code that demonstrates how to use Ragged. This is useful for testing and learning how to use Ragged. Please refer to the README in each examples folder for more information on how to run the examples.scratch
contains scratch files that are used for testing and debugging. These files are not part of the main codebase. They are used for quick testing and prototyping. You can ignore this folder if you are not familiar with it.