From 2904ba81e90ce88323e2416b53d7da4c0be18eee Mon Sep 17 00:00:00 2001 From: Fernando Campos Date: Wed, 15 Jan 2025 13:33:31 -0300 Subject: [PATCH 1/2] Adds Allora Network price inference to the CDP AgentKit (typescript) --- .../src/actions/cdp/allora/README.md | 11 +++ .../src/actions/cdp/allora/allora_action.ts | 32 ++++++++ .../src/actions/cdp/allora/get_all_topics.ts | 64 +++++++++++++++ .../actions/cdp/allora/get_price_inference.ts | 63 +++++++++++++++ .../src/actions/cdp/allora/index.ts | 25 ++++++ .../typescript/src/allora_agentkit.ts | 44 +++++++++++ .../src/tests/allora_agentkit_test.ts | 64 +++++++++++++++ .../src/tests/allora_get_all_topics_test.ts | 58 ++++++++++++++ .../tests/allora_get_price_inference_test.ts | 79 +++++++++++++++++++ package-lock.json | 55 ++++++++++++- typescript/agentkit/package.json | 1 + 11 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts create mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts create mode 100644 cdp-agentkit-core/typescript/src/allora_agentkit.ts create mode 100644 cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts create mode 100644 cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts create mode 100644 cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md b/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md new file mode 100644 index 000000000..7a0cdbb2e --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md @@ -0,0 +1,11 @@ +## Allora + +[Allora Network](https://allora.network/) is an AI-powered inference platform that delivers real-time, self-improving forecasts and insights for various use cases. By aggregating and analyzing data from diverse sources—such as blockchain networks and off-chain APIs—Allora seamlessly provides low-latency, high-performance analytics without requiring complex infrastructure. The platform’s intuitive approach allows developers to focus on building intelligence-driven solutions, while Allora takes care of the heavy lifting behind the scenes. + +### Actions + +- get_all_topics: Lists all available topics from Allora Network. +- get_price_inference: Returns the future price inference for a given crypto asset from Allora Network. It takes the crypto asset and timeframe as inputs. e.g.: "Get the inference for BTC in 5 min". + + + diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts new file mode 100644 index 000000000..2251e26f8 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts @@ -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; + +/** + * Represents the base structure for Allora Actions. + */ +export interface AlloraAction { + /** + * 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) => Promise) + | ((args: z.infer) => Promise); +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts new file mode 100644 index 000000000..a9d3bfe96 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts @@ -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, +): Promise { + 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 all topics action. + */ +export class GetAllTopicsAction implements AlloraAction { + public name = "get_all_topics"; + public description = GET_ALL_TOPICS_PROMPT; + public argsSchema = GetAllTopicsInput; + public func = getAllTopics; +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts new file mode 100644 index 000000000..7c60a3da0 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts @@ -0,0 +1,63 @@ +import { AlloraAction } from "./allora_action"; +import { + AlloraAPIClient, + PriceInferenceTimeframe, + PriceInferenceToken, +} from "@alloralabs/allora-sdk"; +import { z } from "zod"; + +const GET_PRICE_INFERENCE_PROMPT = ` +This tool will get the future price inference for a given crypto asset from Allora Network. +It takes the crypto asset and timeframe as inputs. +`; + +/** + * Input schema for get price inference action. + */ +export const GetPriceInferenceInput = z + .object({ + asset: z.string().describe("The crypto asset to get the price inference for, e.g. 'BTC'"), + timeframe: z + .string() + .describe("The timeframe to get the price inference for, e.g. '5m' or '8h'"), + }) + .strip() + .describe("Instructions for getting the price inference"); + +/** + * Gets the future price inference for a given crypto asset from Allora Network. + * + * @param client - The Allora API client. + * @param args - A zod object containing the asset and timeframe. + * @returns A message containing the price inference. + */ +export async function getPriceInference( + client: AlloraAPIClient, + args: z.infer, +): Promise { + const getPriceInferenceArgs = { + asset: args.asset, + timeframe: args.timeframe, + }; + + try { + const asset = getPriceInferenceArgs.asset as PriceInferenceToken; + const timeframe = getPriceInferenceArgs.timeframe as PriceInferenceTimeframe; + + const priceInference = await client.getPriceInference(asset, timeframe); + + return `The future price inference for ${asset} in ${timeframe} is ${priceInference.inference_data.network_inference_normalized}`; + } catch (error) { + return `Error getting price inference: ${error}`; + } +} + +/** + * Get price inference action. + */ +export class GetPriceInferenceAction implements AlloraAction { + public name = "get_price_inference"; + public description = GET_PRICE_INFERENCE_PROMPT; + public argsSchema = GetPriceInferenceInput; + public func = getPriceInference; +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts new file mode 100644 index 000000000..c343115dc --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts @@ -0,0 +1,25 @@ +/** + * This module exports various Allora action instances and their associated types. + */ + +import { AlloraAction, AlloraActionSchemaAny } from "./allora_action"; +import { GetPriceInferenceAction } from "./get_price_inference"; +import { GetAllTopicsAction } from "./get_all_topics"; +/** + * Retrieve an array of Allora action instances. + * + * @returns {AlloraAction[]} An array of Allora action instances. + */ +export function getAllAlloraActions(): AlloraAction[] { + return [new GetPriceInferenceAction(), new GetAllTopicsAction()]; +} + +/** + * All available Allora actions. + */ +export const ALLORA_ACTIONS = getAllAlloraActions(); + +/** + * All Allora action types. + */ +export { AlloraAction, AlloraActionSchemaAny, GetPriceInferenceAction, GetAllTopicsAction }; diff --git a/cdp-agentkit-core/typescript/src/allora_agentkit.ts b/cdp-agentkit-core/typescript/src/allora_agentkit.ts new file mode 100644 index 000000000..799fbfffe --- /dev/null +++ b/cdp-agentkit-core/typescript/src/allora_agentkit.ts @@ -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( + action: AlloraAction, + args: TActionSchema, + ): Promise { + return await action.func(this.client!, args); + } +} diff --git a/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts new file mode 100644 index 000000000..f700771aa --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts @@ -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/); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts new file mode 100644 index 000000000..b6b102af8 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts @@ -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; + + 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; + + const result = await getAllTopics(mockClient, {}); + + expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1); + expect(result).toContain("Error getting all topics:"); + expect(result).toContain(mockError.toString()); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts new file mode 100644 index 000000000..6eefdd07d --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts @@ -0,0 +1,79 @@ +import { + AlloraAPIClient, + PriceInferenceTimeframe, + PriceInferenceToken, + AlloraInference, +} from "@alloralabs/allora-sdk"; +import { + getPriceInference, + GetPriceInferenceInput, +} from "../actions/cdp/allora/get_price_inference"; + +describe("Get Price Inference Input", () => { + it("should successfully parse valid input", () => { + const validInput = { + asset: "BTC", + timeframe: "5m", + }; + + const result = GetPriceInferenceInput.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = GetPriceInferenceInput.safeParse(emptyInput); + + expect(result.success).toBe(false); + }); +}); + +describe("Get Price Inference Action", () => { + let mockAlloraClient: jest.Mocked; + + beforeEach(() => { + mockAlloraClient = { + getPriceInference: jest.fn(), + } as unknown as jest.Mocked; + }); + + it("should successfully get price inference", async () => { + const args = { + asset: "BTC", + timeframe: "5m", + }; + const mockInference = { + signature: "mockSignature", + inference_data: { network_inference_normalized: "45000.00" }, + }; + + mockAlloraClient.getPriceInference.mockResolvedValue(mockInference as AlloraInference); + + const response = await getPriceInference(mockAlloraClient, args); + + expect(mockAlloraClient.getPriceInference).toHaveBeenCalledWith( + args.asset as PriceInferenceToken, + args.timeframe as PriceInferenceTimeframe, + ); + expect(response).toBe( + `The future price inference for BTC in 5m is ${mockInference.inference_data.network_inference_normalized}`, + ); + }); + + it("should handle errors gracefully", async () => { + const args = { + asset: "BTC", + timeframe: "5m", + }; + + const error = new Error("Failed to fetch price inference"); + mockAlloraClient.getPriceInference.mockRejectedValue(error); + + const response = await getPriceInference(mockAlloraClient, args); + + expect(mockAlloraClient.getPriceInference).toHaveBeenCalled(); + expect(response).toBe(`Error getting price inference: ${error}`); + }); +}); diff --git a/package-lock.json b/package-lock.json index c04b67e93..25b57523c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,31 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, + "node_modules/@alloralabs/allora-sdk": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@alloralabs/allora-sdk/-/allora-sdk-0.1.0.tgz", + "integrity": "sha512-jVCIx+PXOrklDf4TU27DCuf0Nri2+s+hhDGMP/s8CHUY6eSaL8G3S0E1L1vP+sF6gIjzCdV7P68QtRB0ym5vNQ==", + "dependencies": { + "@types/node": "^22.10.5", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alloralabs/allora-sdk/node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@alloralabs/allora-sdk/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -9355,7 +9380,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9866,6 +9890,7 @@ "version": "0.1.2", "license": "Apache-2.0", "dependencies": { + "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.17.0", "@solana/web3.js": "^1.98.0", "md5": "^2.3.0", @@ -10048,6 +10073,30 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, + "@alloralabs/allora-sdk": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@alloralabs/allora-sdk/-/allora-sdk-0.1.0.tgz", + "integrity": "sha512-jVCIx+PXOrklDf4TU27DCuf0Nri2+s+hhDGMP/s8CHUY6eSaL8G3S0E1L1vP+sF6gIjzCdV7P68QtRB0ym5vNQ==", + "requires": { + "@types/node": "^22.10.5", + "typescript": "^5.7.2" + }, + "dependencies": { + "@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "requires": { + "undici-types": "~6.20.0" + } + }, + "undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + } + } + }, "@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -10423,6 +10472,7 @@ "@coinbase/agentkit": { "version": "file:typescript/agentkit", "requires": { + "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.17.0", "@solana/web3.js": "^1.98.0", "@types/jest": "^29.5.14", @@ -16949,8 +16999,7 @@ "typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, "uc.micro": { "version": "2.1.0", diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json index e773b2622..5c5557118 100644 --- a/typescript/agentkit/package.json +++ b/typescript/agentkit/package.json @@ -43,6 +43,7 @@ "@solana/web3.js": "^1.98.0", "md5": "^2.3.0", "reflect-metadata": "^0.2.2", + "@alloralabs/allora-sdk": "^0.1.0", "twitter-api-v2": "^1.18.2", "viem": "^2.22.16", "zod": "^3.23.8" From a09b31a50b917715a1ddde9a4bab51bdaf3a76c5 Mon Sep 17 00:00:00 2001 From: Fernando Campos Date: Tue, 11 Feb 2025 15:47:05 -0300 Subject: [PATCH 2/2] Change implementation to support the ActionProvider paradigm. --- README.md | 1 + assets/protocols/allora.svg | 17 ++ .../src/actions/cdp/allora/README.md | 11 -- .../src/actions/cdp/allora/allora_action.ts | 32 ---- .../src/actions/cdp/allora/get_all_topics.ts | 64 ------- .../actions/cdp/allora/get_price_inference.ts | 63 ------ .../src/actions/cdp/allora/index.ts | 25 --- .../typescript/src/allora_agentkit.ts | 44 ----- .../src/tests/allora_agentkit_test.ts | 64 ------- .../src/tests/allora_get_all_topics_test.ts | 58 ------ .../tests/allora_get_price_inference_test.ts | 79 -------- typescript/agentkit/CHANGELOG.md | 1 + .../src/action-providers/allora/README.md | 91 +++++++++ .../allora/alloraActionProvider.test.ts | 134 +++++++++++++ .../allora/alloraActionProvider.ts | 179 ++++++++++++++++++ .../src/action-providers/allora/index.ts | 2 + .../src/action-providers/allora/schemas.ts | 30 +++ .../agentkit/src/action-providers/index.ts | 1 + 18 files changed, 456 insertions(+), 440 deletions(-) create mode 100644 assets/protocols/allora.svg delete mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md delete mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts delete mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts delete mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts delete mode 100644 cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts delete mode 100644 cdp-agentkit-core/typescript/src/allora_agentkit.ts delete mode 100644 cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts delete mode 100644 cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts delete mode 100644 cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts create mode 100644 typescript/agentkit/src/action-providers/allora/README.md create mode 100644 typescript/agentkit/src/action-providers/allora/alloraActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/allora/alloraActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/allora/index.ts create mode 100644 typescript/agentkit/src/action-providers/allora/schemas.ts diff --git a/README.md b/README.md index e68637ace..e2a10c2de 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ AgentKit is proud to have support for the following protocols, frameworks, and w Moonwell Morpho Pyth +Allora ## 📝 License diff --git a/assets/protocols/allora.svg b/assets/protocols/allora.svg new file mode 100644 index 000000000..854e9c00d --- /dev/null +++ b/assets/protocols/allora.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md b/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md deleted file mode 100644 index 7a0cdbb2e..000000000 --- a/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## Allora - -[Allora Network](https://allora.network/) is an AI-powered inference platform that delivers real-time, self-improving forecasts and insights for various use cases. By aggregating and analyzing data from diverse sources—such as blockchain networks and off-chain APIs—Allora seamlessly provides low-latency, high-performance analytics without requiring complex infrastructure. The platform’s intuitive approach allows developers to focus on building intelligence-driven solutions, while Allora takes care of the heavy lifting behind the scenes. - -### Actions - -- get_all_topics: Lists all available topics from Allora Network. -- get_price_inference: Returns the future price inference for a given crypto asset from Allora Network. It takes the crypto asset and timeframe as inputs. e.g.: "Get the inference for BTC in 5 min". - - - diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts deleted file mode 100644 index 2251e26f8..000000000 --- a/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from "zod"; -import { AlloraAPIClient } from "@alloralabs/allora-sdk"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AlloraActionSchemaAny = z.ZodObject; - -/** - * Represents the base structure for Allora Actions. - */ -export interface AlloraAction { - /** - * 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) => Promise) - | ((args: z.infer) => Promise); -} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts deleted file mode 100644 index a9d3bfe96..000000000 --- a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts +++ /dev/null @@ -1,64 +0,0 @@ -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, -): Promise { - 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 all topics action. - */ -export class GetAllTopicsAction implements AlloraAction { - public name = "get_all_topics"; - public description = GET_ALL_TOPICS_PROMPT; - public argsSchema = GetAllTopicsInput; - public func = getAllTopics; -} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts deleted file mode 100644 index 7c60a3da0..000000000 --- a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AlloraAction } from "./allora_action"; -import { - AlloraAPIClient, - PriceInferenceTimeframe, - PriceInferenceToken, -} from "@alloralabs/allora-sdk"; -import { z } from "zod"; - -const GET_PRICE_INFERENCE_PROMPT = ` -This tool will get the future price inference for a given crypto asset from Allora Network. -It takes the crypto asset and timeframe as inputs. -`; - -/** - * Input schema for get price inference action. - */ -export const GetPriceInferenceInput = z - .object({ - asset: z.string().describe("The crypto asset to get the price inference for, e.g. 'BTC'"), - timeframe: z - .string() - .describe("The timeframe to get the price inference for, e.g. '5m' or '8h'"), - }) - .strip() - .describe("Instructions for getting the price inference"); - -/** - * Gets the future price inference for a given crypto asset from Allora Network. - * - * @param client - The Allora API client. - * @param args - A zod object containing the asset and timeframe. - * @returns A message containing the price inference. - */ -export async function getPriceInference( - client: AlloraAPIClient, - args: z.infer, -): Promise { - const getPriceInferenceArgs = { - asset: args.asset, - timeframe: args.timeframe, - }; - - try { - const asset = getPriceInferenceArgs.asset as PriceInferenceToken; - const timeframe = getPriceInferenceArgs.timeframe as PriceInferenceTimeframe; - - const priceInference = await client.getPriceInference(asset, timeframe); - - return `The future price inference for ${asset} in ${timeframe} is ${priceInference.inference_data.network_inference_normalized}`; - } catch (error) { - return `Error getting price inference: ${error}`; - } -} - -/** - * Get price inference action. - */ -export class GetPriceInferenceAction implements AlloraAction { - public name = "get_price_inference"; - public description = GET_PRICE_INFERENCE_PROMPT; - public argsSchema = GetPriceInferenceInput; - public func = getPriceInference; -} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts deleted file mode 100644 index c343115dc..000000000 --- a/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * This module exports various Allora action instances and their associated types. - */ - -import { AlloraAction, AlloraActionSchemaAny } from "./allora_action"; -import { GetPriceInferenceAction } from "./get_price_inference"; -import { GetAllTopicsAction } from "./get_all_topics"; -/** - * Retrieve an array of Allora action instances. - * - * @returns {AlloraAction[]} An array of Allora action instances. - */ -export function getAllAlloraActions(): AlloraAction[] { - return [new GetPriceInferenceAction(), new GetAllTopicsAction()]; -} - -/** - * All available Allora actions. - */ -export const ALLORA_ACTIONS = getAllAlloraActions(); - -/** - * All Allora action types. - */ -export { AlloraAction, AlloraActionSchemaAny, GetPriceInferenceAction, GetAllTopicsAction }; diff --git a/cdp-agentkit-core/typescript/src/allora_agentkit.ts b/cdp-agentkit-core/typescript/src/allora_agentkit.ts deleted file mode 100644 index 799fbfffe..000000000 --- a/cdp-agentkit-core/typescript/src/allora_agentkit.ts +++ /dev/null @@ -1,44 +0,0 @@ -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( - action: AlloraAction, - args: TActionSchema, - ): Promise { - return await action.func(this.client!, args); - } -} diff --git a/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts deleted file mode 100644 index f700771aa..000000000 --- a/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts +++ /dev/null @@ -1,64 +0,0 @@ -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/); - }); - }); -}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts deleted file mode 100644 index b6b102af8..000000000 --- a/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts +++ /dev/null @@ -1,58 +0,0 @@ -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; - - 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; - - const result = await getAllTopics(mockClient, {}); - - expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1); - expect(result).toContain("Error getting all topics:"); - expect(result).toContain(mockError.toString()); - }); - }); -}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts deleted file mode 100644 index 6eefdd07d..000000000 --- a/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - AlloraAPIClient, - PriceInferenceTimeframe, - PriceInferenceToken, - AlloraInference, -} from "@alloralabs/allora-sdk"; -import { - getPriceInference, - GetPriceInferenceInput, -} from "../actions/cdp/allora/get_price_inference"; - -describe("Get Price Inference Input", () => { - it("should successfully parse valid input", () => { - const validInput = { - asset: "BTC", - timeframe: "5m", - }; - - const result = GetPriceInferenceInput.safeParse(validInput); - - expect(result.success).toBe(true); - expect(result.data).toEqual(validInput); - }); - - it("should fail parsing empty input", () => { - const emptyInput = {}; - const result = GetPriceInferenceInput.safeParse(emptyInput); - - expect(result.success).toBe(false); - }); -}); - -describe("Get Price Inference Action", () => { - let mockAlloraClient: jest.Mocked; - - beforeEach(() => { - mockAlloraClient = { - getPriceInference: jest.fn(), - } as unknown as jest.Mocked; - }); - - it("should successfully get price inference", async () => { - const args = { - asset: "BTC", - timeframe: "5m", - }; - const mockInference = { - signature: "mockSignature", - inference_data: { network_inference_normalized: "45000.00" }, - }; - - mockAlloraClient.getPriceInference.mockResolvedValue(mockInference as AlloraInference); - - const response = await getPriceInference(mockAlloraClient, args); - - expect(mockAlloraClient.getPriceInference).toHaveBeenCalledWith( - args.asset as PriceInferenceToken, - args.timeframe as PriceInferenceTimeframe, - ); - expect(response).toBe( - `The future price inference for BTC in 5m is ${mockInference.inference_data.network_inference_normalized}`, - ); - }); - - it("should handle errors gracefully", async () => { - const args = { - asset: "BTC", - timeframe: "5m", - }; - - const error = new Error("Failed to fetch price inference"); - mockAlloraClient.getPriceInference.mockRejectedValue(error); - - const response = await getPriceInference(mockAlloraClient, args); - - expect(mockAlloraClient.getPriceInference).toHaveBeenCalled(); - expect(response).toBe(`Error getting price inference: ${error}`); - }); -}); diff --git a/typescript/agentkit/CHANGELOG.md b/typescript/agentkit/CHANGELOG.md index 57cb497a9..3b832befd 100644 --- a/typescript/agentkit/CHANGELOG.md +++ b/typescript/agentkit/CHANGELOG.md @@ -7,6 +7,7 @@ - Added `svmWalletProvider` abstract class for interacting with Solana. - Added `solanaKeypairWalletProvider` to concretely implement `svmWalletProvider` with a local keypair. - Added gas configuration parameters (`gasLimitMultiplier`, `feePerGasMultiplier`) to `CdpWalletProvider` and `ViemWalletProvider`. +- Added `alloraActionProvider` to fetch inferences from Allora Network. ## [0.1.2] - 2025-02-07 diff --git a/typescript/agentkit/src/action-providers/allora/README.md b/typescript/agentkit/src/action-providers/allora/README.md new file mode 100644 index 000000000..8836884a0 --- /dev/null +++ b/typescript/agentkit/src/action-providers/allora/README.md @@ -0,0 +1,91 @@ +# Allora Action Provider + +This action provider enables interaction with the Allora Network, allowing AI agents to fetch topics and inferences. + +## Setup + +To use the Allora action provider, you'll need an API key from Allora Network. Initialize the provider like this: + +```typescript +import { createAlloraActionProvider } from "@coinbase/agentkit"; + +const provider = createAlloraActionProvider({ + apiKey: "your-api-key", + chainSlug: "testnet" // optional, defaults to testnet +}); +``` + +## Available Actions + +### Get All Topics + +Fetches all available topics from Allora Network. Each topic represents a prediction market. + +Example response: +```json +[ + { + "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" + } +] +``` + +### Get Inference By Topic ID + +Fetches inference data for a specific topic. Requires a topic ID which can be obtained from the get_all_topics action. + +Example usage: +```typescript +const result = await provider.getInferenceByTopicId({ topicId: 1 }); +``` + +Example response: +```json +{ + "network_inference": "0.5", + "network_inference_normalized": "0.5", + "confidence_interval_percentiles": ["0.1", "0.5", "0.9"], + "confidence_interval_percentiles_normalized": ["0.1", "0.5", "0.9"], + "confidence_interval_values": ["0.1", "0.5", "0.9"], + "confidence_interval_values_normalized": ["0.1", "0.5", "0.9"], + "topic_id": "1", + "timestamp": 1718198400, + "extra_data": "extra_data" +} +``` + +### Get Price Inference + +Fetches price inference for a specific token and timeframe. Requires a token symbol from the supported list and a timeframe. + +Example usage: +```typescript +import { PriceInferenceToken } from "@alloralabs/allora-sdk"; + +const result = await provider.getPriceInference({ + asset: PriceInferenceToken.BTC, + timeframe: "8h" +}); +``` + +Example response: +```json +{ + "price": "50000.00", + "timestamp": 1718198400, + "asset": "BTC", + "timeframe": "8h" +} +``` \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/allora/alloraActionProvider.test.ts b/typescript/agentkit/src/action-providers/allora/alloraActionProvider.test.ts new file mode 100644 index 000000000..6b600848f --- /dev/null +++ b/typescript/agentkit/src/action-providers/allora/alloraActionProvider.test.ts @@ -0,0 +1,134 @@ +import { AlloraActionProvider } from "./alloraActionProvider"; +import { AlloraAPIClient, ChainSlug, PriceInferenceToken } from "@alloralabs/allora-sdk"; + +jest.mock("@alloralabs/allora-sdk"); + +describe("AlloraActionProvider", () => { + let provider: AlloraActionProvider; + const mockConfig = { apiKey: "test-api-key", chainSlug: ChainSlug.TESTNET }; + + beforeEach(() => { + jest.clearAllMocks(); + provider = new AlloraActionProvider(mockConfig); + }); + + describe("getAllTopics", () => { + it("should return topics when successful", async () => { + const mockTopics = [ + { + topic_id: 1, + topic_name: "Bitcoin 8h", + description: "Bitcoin price prediction", + 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", + }, + ]; + + (AlloraAPIClient.prototype.getAllTopics as jest.Mock).mockResolvedValue(mockTopics); + + const result = await provider.getAllTopics({}); + expect(result).toContain("The available topics at Allora Network are:"); + expect(result).toContain(JSON.stringify(mockTopics)); + }); + + it("should handle errors", async () => { + const error = new Error("API Error"); + (AlloraAPIClient.prototype.getAllTopics as jest.Mock).mockRejectedValue(error); + + const result = await provider.getAllTopics({}); + expect(result).toContain("Error getting all topics:"); + expect(result).toContain(error.toString()); + }); + }); + + describe("getInferenceByTopicId", () => { + const mockTopicId = 1; + + it("should return inference when successful", async () => { + const mockInferenceResponse = { + inference_data: { + network_inference: "0.5", + network_inference_normalized: "0.5", + confidence_interval_percentiles: ["0.1", "0.5", "0.9"], + confidence_interval_percentiles_normalized: ["0.1", "0.5", "0.9"], + confidence_interval_values: ["0.1", "0.5", "0.9"], + confidence_interval_values_normalized: ["0.1", "0.5", "0.9"], + topic_id: "1", + timestamp: 1718198400, + extra_data: "extra_data", + }, + }; + + (AlloraAPIClient.prototype.getInferenceByTopicID as jest.Mock).mockResolvedValue( + mockInferenceResponse, + ); + + const result = await provider.getInferenceByTopicId({ topicId: mockTopicId }); + expect(result).toContain(`The inference for topic ${mockTopicId} is:`); + expect(result).toContain(JSON.stringify(mockInferenceResponse.inference_data)); + }); + + it("should handle errors", async () => { + const error = new Error("API Error"); + (AlloraAPIClient.prototype.getInferenceByTopicID as jest.Mock).mockRejectedValue(error); + + const result = await provider.getInferenceByTopicId({ topicId: mockTopicId }); + expect(result).toContain(`Error getting inference for topic ${mockTopicId}:`); + expect(result).toContain(error.toString()); + }); + }); + + describe("getPriceInference", () => { + const mockAsset = PriceInferenceToken.BTC; + const mockTimeframe = "8h"; + + it("should return price inference when successful", async () => { + const mockPriceInference = { + inference_data: { + network_inference_normalized: "50000.00", + timestamp: 1718198400, + }, + }; + + (AlloraAPIClient.prototype.getPriceInference as jest.Mock).mockResolvedValue( + mockPriceInference, + ); + + const expectedResponse = { + price: "50000.00", + timestamp: 1718198400, + asset: mockAsset, + timeframe: mockTimeframe, + }; + + const result = await provider.getPriceInference({ + asset: mockAsset, + timeframe: mockTimeframe, + }); + expect(result).toContain(`The price inference for ${mockAsset} (${mockTimeframe}) is:`); + expect(result).toContain(JSON.stringify(expectedResponse)); + }); + + it("should handle errors", async () => { + const error = new Error("API Error"); + (AlloraAPIClient.prototype.getPriceInference as jest.Mock).mockRejectedValue(error); + + const result = await provider.getPriceInference({ + asset: mockAsset, + timeframe: mockTimeframe, + }); + expect(result).toContain( + `Error getting price inference for ${mockAsset} (${mockTimeframe}):`, + ); + expect(result).toContain(error.toString()); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/allora/alloraActionProvider.ts b/typescript/agentkit/src/action-providers/allora/alloraActionProvider.ts new file mode 100644 index 000000000..71c0641b6 --- /dev/null +++ b/typescript/agentkit/src/action-providers/allora/alloraActionProvider.ts @@ -0,0 +1,179 @@ +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { + AlloraAPIClient, + AlloraAPIClientConfig, + ChainSlug, + PriceInferenceTimeframe, + PriceInferenceToken, +} from "@alloralabs/allora-sdk"; +import { z } from "zod"; +import { + GetAllTopicsSchema, + GetInferenceByTopicIdSchema, + GetPriceInferenceSchema, +} from "./schemas"; + +/** + * Action provider for interacting with Allora Network + */ +export class AlloraActionProvider extends ActionProvider { + private client: AlloraAPIClient; + + /** + * Creates an instance of AlloraActionProvider + * + * @param config - Configuration for the Allora API client including API key and optional chain slug + */ + constructor(config: AlloraAPIClientConfig) { + super("allora", []); + config.apiKey = config.apiKey || "UP-4151d0cc489a44a7aa5cd7ef"; + config.chainSlug = config.chainSlug || ChainSlug.TESTNET; + this.client = new AlloraAPIClient(config); + } + + /** + * Gets all available topics from Allora Network + * + * @param _ - Empty object as no parameters are required + * @returns A string containing the list of topics in JSON format + */ + @CreateAction({ + name: "get_all_topics", + description: ` +This tool will get all available inference topics from Allora Network. + +A successful response will return a message with a list of available topics from Allora Network in JSON format. Example: + [ + { + "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" + } + ] +The description field is a short description of the topic, and the topic_name is the name of the topic. These fields can be used to understand the topic and its purpose. +The topic_id field is the unique identifier for the topic, and can be used to get the inference data for the topic using the get_inference_by_topic_id action. +The is_active field indicates if the topic is currently active and accepting submissions. +The updated_at field is the timestamp of the last update for the topic. + +A failure response will return an error message with details. +`, + schema: GetAllTopicsSchema, + }) + async getAllTopics(_: z.infer): Promise { + try { + const topics = await this.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}`; + } + } + + /** + * Gets inference data for a specific topic from Allora Network + * + * @param args - Object containing the topic ID to get inference for + * @returns A string containing the inference data in JSON format + */ + @CreateAction({ + name: "get_inference_by_topic_id", + description: ` +This tool will get inference for a specific topic from Allora Network. +It requires a topic ID as input, which can be obtained from the get_all_topics action. + +A successful response will return a message with the inference data in JSON format. Example: + { + "network_inference": "0.5", + "network_inference_normalized": "0.5", + "confidence_interval_percentiles": ["0.1", "0.5", "0.9"], + "confidence_interval_percentiles_normalized": ["0.1", "0.5", "0.9"], + "confidence_interval_values": ["0.1", "0.5", "0.9"], + "confidence_interval_values_normalized": ["0.1", "0.5", "0.9"], + "topic_id": "1", + "timestamp": 1718198400, + "extra_data": "extra_data" + } +The network_inference field is the inference for the topic. +The network_inference_normalized field is the normalized inference for the topic. + +A failure response will return an error message with details. +`, + schema: GetInferenceByTopicIdSchema, + }) + async getInferenceByTopicId(args: z.infer): Promise { + try { + const inference = await this.client.getInferenceByTopicID(args.topicId); + const inferenceJson = JSON.stringify(inference.inference_data); + return `The inference for topic ${args.topicId} is:\n ${inferenceJson}`; + } catch (error) { + return `Error getting inference for topic ${args.topicId}: ${error}`; + } + } + + /** + * Gets price inference for a token/timeframe pair from Allora Network + * + * @param args - Object containing the asset symbol and timeframe + * @returns A string containing the price inference data in JSON format + */ + @CreateAction({ + name: "get_price_inference", + description: ` +This tool will get price inference for a specific token and timeframe from Allora Network. +It requires an asset symbol (e.g., 'BTC', 'ETH') and a timeframe (e.g., '8h', '24h') as input. + +A successful response will return a message with the price inference. Example: + The price inference for BTC (8h) is: + { + "price": "100000", + "timestamp": 1718198400, + "asset": "BTC", + "timeframe": "8h" + } + +A failure response will return an error message with details. +`, + schema: GetPriceInferenceSchema, + }) + async getPriceInference(args: z.infer): Promise { + try { + const inference = await this.client.getPriceInference( + args.asset as PriceInferenceToken, + args.timeframe as PriceInferenceTimeframe, + ); + const response = { + price: inference.inference_data.network_inference_normalized, + timestamp: inference.inference_data.timestamp, + asset: args.asset, + timeframe: args.timeframe, + }; + const inferenceJson = JSON.stringify(response); + return `The price inference for ${args.asset} (${args.timeframe}) is:\n${inferenceJson}`; + } catch (error) { + return `Error getting price inference for ${args.asset} (${args.timeframe}): ${error}`; + } + } + + /** + * Checks if the provider supports a given network + * + * @returns Always returns true as Allora service is network-agnostic + */ + supportsNetwork(): boolean { + return true; // Allora service is network-agnostic + } +} + +export const createAlloraActionProvider = (config: AlloraAPIClientConfig) => + new AlloraActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/allora/index.ts b/typescript/agentkit/src/action-providers/allora/index.ts new file mode 100644 index 000000000..8ee65d19f --- /dev/null +++ b/typescript/agentkit/src/action-providers/allora/index.ts @@ -0,0 +1,2 @@ +export * from "./alloraActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/allora/schemas.ts b/typescript/agentkit/src/action-providers/allora/schemas.ts new file mode 100644 index 000000000..ddf1c6c5d --- /dev/null +++ b/typescript/agentkit/src/action-providers/allora/schemas.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +/** + * Input schema for getting all topics from Allora Network + */ +export const GetAllTopicsSchema = z + .object({}) + .strip() + .describe("Instructions for getting all topics from Allora Network"); + +/** + * Input schema for getting inference data by topic ID from Allora Network + */ +export const GetInferenceByTopicIdSchema = z + .object({ + topicId: z.number().describe("The ID of the topic to get inference data for"), + }) + .strip() + .describe("Instructions for getting inference data from Allora Network by topic ID"); + +/** + * Input schema for getting price inference for a token/timeframe pair + */ +export const GetPriceInferenceSchema = z + .object({ + asset: z.string().describe("The token to get price inference for (e.g., 'BTC', 'ETH')"), + timeframe: z.string().describe("The timeframe for the prediction (e.g., '8h', '24h')"), + }) + .strip() + .describe("Instructions for getting price inference for a token/timeframe pair"); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index fc6e1b600..07767a4c4 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -14,3 +14,4 @@ export * from "./wallet"; export * from "./customActionProvider"; export * from "./alchemy"; export * from "./moonwell"; +export * from "./allora";