Skip to content

Commit

Permalink
Adds Allora Network price prediction to the CDP AgentKit (typescript)
Browse files Browse the repository at this point in the history
  • Loading branch information
fernandofcampos committed Jan 16, 2025
1 parent 58a7dd0 commit 0369e70
Show file tree
Hide file tree
Showing 11 changed files with 485 additions and 3 deletions.
1 change: 1 addition & 0 deletions cdp-agentkit-core/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"typescript"
],
"dependencies": {
"@alloralabs/allora-sdk": "^0.0.4",
"@coinbase/coinbase-sdk": "^0.13.0",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from "zod";
import { AlloraAPIClient } from "@alloralabs/allora-sdk";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AlloraActionSchemaAny = z.ZodObject<any, any, any, any>;

/**
* Represents the base structure for Allora Actions.
*/
export interface AlloraAction<TActionSchema extends AlloraActionSchemaAny> {
/**
* The name of the action.
*/
name: string;

/**
* A description of what the action does
*/
description: string;

/**
* Schema for validating action arguments
*/
argsSchema: TActionSchema;

/**
* The function to execute for this action
*/
func:
| ((client: AlloraAPIClient, args: z.infer<TActionSchema>) => Promise<string>)
| ((args: z.infer<TActionSchema>) => Promise<string>);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AlloraAction } from "./allora_action";
import { AlloraAPIClient } from "@alloralabs/allora-sdk";
import { z } from "zod";

const GET_ALL_TOPICS_PROMPT = `
This tool will get all available topics from Allora Network.
A successful response will return a message with a list of available topics from Allora Network in JSON format:
[
{
"topic_id": 1,
"topic_name": ""Bitcoin 8h",
"description": "Bitcoin price prediction for the next 8 hours",
"epoch_length": 100,
"ground_truth_lag": 10,
"loss_method": "method1",
"worker_submission_window": 50,
"worker_count": 5,
"reputer_count": 3,
"total_staked_allo": 1000,
"total_emissions_allo": 500,
"is_active": true,
"updated_at": "2023-01-01T00:00:00Z"
}
]
`;

/**
* Input schema for get all topics action.
*/
export const GetAllTopicsInput = z
.object({})
.strip()
.describe("Instructions for getting all topics");

/**
* Gets all available topics from Allora Network.
*
* @param client - The Allora API client.
* @param args - The input arguments for the action.
* @returns A message containing the topics.
*/
export async function getAllTopics(
client: AlloraAPIClient,
_: z.infer<typeof GetAllTopicsInput>,
): Promise<string> {
try {
const topics = await client.getAllTopics();
const topicsJson = JSON.stringify(topics);
return `The available topics at Allora Network are:\n ${topicsJson}`;
} catch (error) {
return `Error getting all topics: ${error}`;
}
}

/**
* Get price prediction action.
*/
export class GetAllTopicsAction implements AlloraAction<typeof GetAllTopicsInput> {
public name = "get_all_topics";
public description = GET_ALL_TOPICS_PROMPT;
public argsSchema = GetAllTopicsInput;
public func = getAllTopics;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { AlloraAction } from "./allora_action";
import {
AlloraAPIClient,
PricePredictionTimeframe,
PricePredictionToken,
} from "@alloralabs/allora-sdk";
import { z } from "zod";

const GET_PRICE_PREDICTION_PROMPT = `
This tool will get the future price prediction for a given crypto asset from Allora Network.
It takes the crypto asset and timeframe as inputs.
`;

/**
* Input schema for get price prediction action.
*/
export const GetPricePredictionInput = z
.object({
asset: z.string().describe("The crypto asset to get the price prediction for, e.g. 'BTC'"),
timeframe: z
.string()
.describe("The timeframe to get the price prediction for, e.g. '5m' or '8h'"),
})
.strip()
.describe("Instructions for getting the price prediction");

/**
* Gets the future price prediction for a given crypto asset from Allora Network.
*
* @param client - The Allora API client.
* @param args - The input arguments for the action.
* @returns A message containing the price prediction.
*/
export async function getPricePrediction(
client: AlloraAPIClient,
args: z.infer<typeof GetPricePredictionInput>,
): Promise<string> {
const getPricePredictionArgs = {
asset: args.asset,
timeframe: args.timeframe,
};

try {
const asset = getPricePredictionArgs.asset as PricePredictionToken;
const timeframe = getPricePredictionArgs.timeframe as PricePredictionTimeframe;

const pricePrediction = await client.getPricePrediction(asset, timeframe);

return `The future price prediction for ${asset} in ${timeframe} is ${pricePrediction.inference_data.network_inference_normalized}`;
} catch (error) {
return `Error getting price prediction: ${error}`;
}
}

/**
* Get price prediction action.
*/
export class GetPricePredictionAction implements AlloraAction<typeof GetPricePredictionInput> {
public name = "get_price_prediction";
public description = GET_PRICE_PREDICTION_PROMPT;
public argsSchema = GetPricePredictionInput;
public func = getPricePrediction;
}
25 changes: 25 additions & 0 deletions cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* This module exports various Allora action instances and their associated types.
*/

import { AlloraAction, AlloraActionSchemaAny } from "./allora_action";
import { GetPricePredictionAction } from "./get_price_prediction";
import { GetAllTopicsAction } from "./get_all_topics";
/**
* Retrieve an array of Allora action instances.
*
* @returns {AlloraAction<AlloraActionSchemaAny>[]} An array of Allora action instances.
*/
export function getAllAlloraActions(): AlloraAction<AlloraActionSchemaAny>[] {
return [new GetPricePredictionAction(), new GetAllTopicsAction()];
}

/**
* All available Allora actions.
*/
export const ALLORA_ACTIONS = getAllAlloraActions();

/**
* All Allora action types.
*/
export { AlloraAction, AlloraActionSchemaAny, GetPricePredictionAction, GetAllTopicsAction };
44 changes: 44 additions & 0 deletions cdp-agentkit-core/typescript/src/allora_agentkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AlloraAPIClient, ChainSlug } from "@alloralabs/allora-sdk";
import { AlloraAction, AlloraActionSchemaAny } from "./actions/cdp/allora";

interface AlloraAgentkitOptions {
apiKey?: string;
baseAPIUrl?: string;
chainSlug?: string;
}

export class AlloraAgentkit {
private client: AlloraAPIClient;

constructor(config: AlloraAgentkitOptions = {}) {
const apiKey = config.apiKey || process.env.ALLORA_API_KEY || "UP-4151d0cc489a44a7aa5cd7ef";
const baseAPIUrl = config.baseAPIUrl || process.env.ALLORA_BASE_API_URL;
const chainSlug = config.chainSlug || process.env.ALLORA_CHAIN_SLUG || ChainSlug.TESTNET;

if (!Object.values(ChainSlug).includes(chainSlug as ChainSlug)) {
throw new Error(
`Invalid chainSlug: ${chainSlug}. Valid options are: ${Object.values(ChainSlug).join(", ")}`,
);
}

this.client = new AlloraAPIClient({
apiKey,
baseAPIUrl,
chainSlug: chainSlug as ChainSlug,
});
}

/**
* Executes a Allora action.
*
* @param action - The Allora action to execute.
* @param args - The arguments for the action.
* @returns The result of the execution.
*/
async run<TActionSchema extends AlloraActionSchemaAny>(
action: AlloraAction<TActionSchema>,
args: TActionSchema,
): Promise<string> {
return await action.func(this.client!, args);
}
}
3 changes: 3 additions & 0 deletions cdp-agentkit-core/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ export { CdpAgentkit } from "./cdp_agentkit";

// Export Twitter AgentKit
export { TwitterAgentkit } from "./twitter_agentkit";

// Export Allora AgentKit
export { AlloraAgentkit } from "./allora_agentkit";
64 changes: 64 additions & 0 deletions cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AlloraAPIClient, ChainSlug } from "@alloralabs/allora-sdk";
import { AlloraAgentkit } from "../allora_agentkit";

jest.mock("@alloralabs/allora-sdk");

describe("AlloraAgentkit", () => {
describe("initialization", () => {
beforeEach(() => {
process.env.ALLORA_API_KEY = "test-api-key";
process.env.ALLORA_BASE_API_URL = "https://test.api.url";
process.env.ALLORA_CHAIN_SLUG = ChainSlug.TESTNET;
});

afterEach(() => {
jest.resetAllMocks();
process.env.ALLORA_API_KEY = "";
process.env.ALLORA_BASE_API_URL = "";
process.env.ALLORA_CHAIN_SLUG = "";
});

it("should successfully init with env variables", () => {
const agentkit = new AlloraAgentkit();
expect(agentkit).toBeDefined();
expect(AlloraAPIClient).toHaveBeenCalledWith({
apiKey: "test-api-key",
baseAPIUrl: "https://test.api.url",
chainSlug: ChainSlug.TESTNET,
});
});

it("should successfully init with options overriding env", () => {
const options = {
apiKey: "custom-api-key",
baseAPIUrl: "https://custom.api.url",
chainSlug: ChainSlug.MAINNET,
};

const agentkit = new AlloraAgentkit(options);
expect(agentkit).toBeDefined();
expect(AlloraAPIClient).toHaveBeenCalledWith(options);
});

it("should use default values when no options or env provided", () => {
process.env.ALLORA_API_KEY = "";
process.env.ALLORA_BASE_API_URL = "";
process.env.ALLORA_CHAIN_SLUG = "";

const agentkit = new AlloraAgentkit();
expect(agentkit).toBeDefined();
expect(AlloraAPIClient).toHaveBeenCalledWith({
apiKey: "UP-4151d0cc489a44a7aa5cd7ef",
baseAPIUrl: "",
chainSlug: ChainSlug.TESTNET,
});
});

it("should throw error for invalid chain slug", () => {
const invalidChainSlug = "INVALID_CHAIN" as unknown as ChainSlug;
expect(() => {
new AlloraAgentkit({ chainSlug: invalidChainSlug });
}).toThrow(/Invalid chainSlug/);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AlloraAPIClient } from "@alloralabs/allora-sdk";
import { GetAllTopicsAction, getAllTopics } from "../actions/cdp/allora/get_all_topics";

describe("GetAllTopicsAction", () => {
const mockTopics = [
{
topic_id: 1,
topic_name: "Bitcoin 8h",
description: "Bitcoin price prediction for the next 8 hours",
epoch_length: 100,
ground_truth_lag: 10,
loss_method: "method1",
worker_submission_window: 50,
worker_count: 5,
reputer_count: 3,
total_staked_allo: 1000,
total_emissions_allo: 500,
is_active: true,
updated_at: "2023-01-01T00:00:00Z",
},
];

it("should have correct action properties", () => {
const action = new GetAllTopicsAction();
expect(action.name).toBe("get_all_topics");
expect(action.description).toContain(
"This tool will get all available topics from Allora Network",
);
expect(action.argsSchema).toBeDefined();
});

describe("getAllTopics", () => {
it("should return topics successfully", async () => {
const mockClient = {
getAllTopics: jest.fn().mockResolvedValue(mockTopics),
} as unknown as jest.Mocked<AlloraAPIClient>;

const result = await getAllTopics(mockClient, {});

expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1);
expect(result).toContain("The available topics at Allora Network are:");
expect(result).toContain(JSON.stringify(mockTopics));
});

it("should handle errors gracefully", async () => {
const mockError = new Error("API Error");
const mockClient = {
getAllTopics: jest.fn().mockRejectedValue(mockError),
} as unknown as jest.Mocked<AlloraAPIClient>;

const result = await getAllTopics(mockClient, {});

expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1);
expect(result).toContain("Error getting all topics:");
expect(result).toContain(mockError.toString());
});
});
});
Loading

0 comments on commit 0369e70

Please sign in to comment.