Skip to content

Commit 6cee1b3

Browse files
authored
Messari tool (#70)
* feat(messari): Introduce MessariTool for contextual crypto responses - Added MessariTool to generate contextual responses using Messari's knowledge graph, including detailed message structure and response formats. - Implemented error handling for API interactions and logging for execution errors. - Created comprehensive unit tests to validate tool functionality, including successful API responses and error scenarios. * feat: Add Messari tool to available tools and update tool names - Registered MessariTool in availableTools for enhanced functionality. - Updated ToolName enum to include MESSARI for better type safety. - Incorporated MESSARI in specialtyDomains to ensure comprehensive tool coverage. * chore: Add prerelease script for formatting and coverage checks - Introduced a new script in package.json to run formatting and coverage checks before release, ensuring code quality and consistency. * feat(tests): Add MESSARI tool to AskSpecialtyTool tests - Included MESSARI in the test suite for AskSpecialtyTool to ensure comprehensive coverage of available tools. - Updated expectations to validate the integration of MESSARI in the toolset.
1 parent c9bc5ea commit 6cee1b3

File tree

7 files changed

+372
-1
lines changed

7 files changed

+372
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"test": "vitest run",
1010
"test:watch": "vitest",
1111
"coverage": "vitest --coverage",
12-
"format": "bun prettier . --write"
12+
"format": "bun prettier . --write",
13+
"prerelease": "bun run format && bun run coverage"
1314
},
1415
"keywords": [],
1516
"author": "",

src/registry/toolClasses.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TimestampConverterTool } from "../tools/time";
1818
import { CalculatorTool } from "../tools/calculator";
1919
import { AirQualityTool } from "../tools/airquality";
2020
import { DePINNinjaTool } from "../tools/depinninja";
21+
import MessariTool from "../tools/messari";
2122

2223
export const availableTools = [
2324
{
@@ -92,4 +93,8 @@ export const availableTools = [
9293
name: ToolName.DEPIN_NINJA,
9394
toolClass: DePINNinjaTool,
9495
},
96+
{
97+
name: ToolName.MESSARI,
98+
toolClass: MessariTool,
99+
},
95100
];

src/registry/toolNames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export enum ToolName {
1717
CALCULATOR = "calculator",
1818
AIR_QUALITY = "air_quality",
1919
DEPIN_NINJA = "depin_ninja",
20+
MESSARI = "messari",
2021
}

src/tools/__tests__/askSpecialty.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ describe("AskSpecialtyTool", () => {
8888
ToolName.DEPIN_PROJECTS,
8989
ToolName.L1DATA,
9090
ToolName.THIRDWEB,
91+
ToolName.MESSARI,
9192
]);
9293
expect(ToolRegistry.buildToolSet).toHaveBeenCalled();
9394
expect(mockOrchestrator.process).toHaveBeenCalledWith(
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import { MessariTool } from "../messari";
3+
import { logger } from "../../logger/winston";
4+
5+
vi.mock("global", () => ({
6+
fetch: vi.fn(),
7+
}));
8+
9+
vi.mock("../../logger/winston", () => ({
10+
logger: {
11+
error: vi.fn(),
12+
},
13+
}));
14+
15+
describe("MessariTool", () => {
16+
let tool: MessariTool;
17+
const mockApiKey = "test-api-key";
18+
19+
beforeEach(() => {
20+
process.env.MESSARI_API_KEY = mockApiKey;
21+
tool = new MessariTool();
22+
vi.clearAllMocks();
23+
});
24+
25+
it("should initialize with correct properties", () => {
26+
expect(tool.name).toBe("MessariCopilot");
27+
expect(tool.description).toContain(
28+
"Tool for generating contextual crypto responses"
29+
);
30+
expect(tool.schema).toHaveLength(1);
31+
expect(tool.schema[0].name).toBe("messari_copilot");
32+
});
33+
34+
it("should throw error when API key is not set", () => {
35+
delete process.env.MESSARI_API_KEY;
36+
expect(() => new MessariTool()).toThrow(
37+
"Missing MESSARI_API_KEY environment variable"
38+
);
39+
});
40+
41+
describe("getCopilotResponse", () => {
42+
const mockMessages = [
43+
{
44+
role: "user" as const,
45+
content: "Tell me about Bitcoin's market cap",
46+
},
47+
];
48+
49+
const mockResponse = {
50+
choices: [
51+
{
52+
message: {
53+
content: "Bitcoin's market cap is...",
54+
},
55+
},
56+
],
57+
};
58+
59+
it("should handle successful API response", async () => {
60+
const mockFetch = vi.fn().mockResolvedValueOnce({
61+
ok: true,
62+
json: () => Promise.resolve(mockResponse),
63+
});
64+
65+
global.fetch = mockFetch;
66+
67+
const result = await tool.getCopilotResponse({
68+
messages: mockMessages,
69+
verbosity: "balanced",
70+
response_format: "markdown",
71+
});
72+
73+
expect(result).toEqual(mockResponse);
74+
expect(mockFetch).toHaveBeenCalledWith(
75+
"https://api.messari.io/ai/v1/chat/completions",
76+
{
77+
method: "POST",
78+
headers: {
79+
"x-messari-api-key": mockApiKey,
80+
"Content-Type": "application/json",
81+
},
82+
body: JSON.stringify({
83+
messages: mockMessages,
84+
verbosity: "balanced",
85+
response_format: "markdown",
86+
}),
87+
}
88+
);
89+
});
90+
91+
it("should handle API error response", async () => {
92+
const errorMessage = "API Error";
93+
const mockFetch = vi.fn().mockResolvedValueOnce({
94+
ok: false,
95+
text: () => Promise.resolve(errorMessage),
96+
});
97+
98+
global.fetch = mockFetch;
99+
100+
await expect(
101+
tool.getCopilotResponse({
102+
messages: mockMessages,
103+
})
104+
).rejects.toThrow(`Messari API error: ${errorMessage}`);
105+
});
106+
107+
it("should use default values for optional parameters", async () => {
108+
const mockFetch = vi.fn().mockResolvedValueOnce({
109+
ok: true,
110+
json: () => Promise.resolve(mockResponse),
111+
});
112+
113+
global.fetch = mockFetch;
114+
115+
await tool.getCopilotResponse({
116+
messages: mockMessages,
117+
});
118+
119+
expect(mockFetch).toHaveBeenCalledWith(
120+
"https://api.messari.io/ai/v1/chat/completions",
121+
expect.objectContaining({
122+
body: expect.stringContaining('"verbosity":"balanced"'),
123+
})
124+
);
125+
});
126+
});
127+
128+
describe("getRawData", () => {
129+
it("should delegate to getCopilotResponse", async () => {
130+
const mockMessages = [
131+
{
132+
role: "user" as const,
133+
content: "Test message",
134+
},
135+
];
136+
137+
const mockResponse = {
138+
choices: [
139+
{
140+
message: {
141+
content: "Test response",
142+
},
143+
},
144+
],
145+
};
146+
147+
const mockFetch = vi.fn().mockResolvedValueOnce({
148+
ok: true,
149+
json: () => Promise.resolve(mockResponse),
150+
});
151+
152+
global.fetch = mockFetch;
153+
154+
const result = await tool.getRawData({
155+
messages: mockMessages,
156+
});
157+
158+
expect(result).toEqual(mockResponse);
159+
expect(mockFetch).toHaveBeenCalled();
160+
});
161+
});
162+
163+
describe("execute", () => {
164+
const mockInput = {
165+
messages: [
166+
{
167+
role: "user" as const,
168+
content: "Test message",
169+
},
170+
],
171+
verbosity: "balanced" as const,
172+
response_format: "markdown" as const,
173+
};
174+
const executionOptions = {
175+
toolCallId: "test-tool-call-id",
176+
messages: mockInput.messages,
177+
};
178+
179+
const mockResponse = {
180+
choices: [
181+
{
182+
message: {
183+
content: "Test response",
184+
},
185+
},
186+
],
187+
};
188+
189+
it("should successfully execute the tool", async () => {
190+
const mockFetch = vi.fn().mockResolvedValueOnce({
191+
ok: true,
192+
json: () => Promise.resolve(mockResponse),
193+
});
194+
195+
global.fetch = mockFetch;
196+
197+
const result = await tool.schema[0].tool.execute(
198+
mockInput,
199+
executionOptions
200+
);
201+
202+
expect(result).toEqual(mockResponse);
203+
expect(mockFetch).toHaveBeenCalledWith(
204+
"https://api.messari.io/ai/v1/chat/completions",
205+
expect.any(Object)
206+
);
207+
expect(logger.error).not.toHaveBeenCalled();
208+
});
209+
210+
it("should handle errors and log them", async () => {
211+
const mockError = new Error("API Error");
212+
const mockFetch = vi.fn().mockRejectedValueOnce(mockError);
213+
214+
global.fetch = mockFetch;
215+
216+
const result = await tool.schema[0].tool.execute(
217+
mockInput,
218+
executionOptions
219+
);
220+
221+
expect(result).toBe("Error executing messari_copilot tool");
222+
expect(logger.error).toHaveBeenCalledWith(
223+
"Error executing messari_copilot tool",
224+
mockError
225+
);
226+
expect(mockFetch).toHaveBeenCalledWith(
227+
"https://api.messari.io/ai/v1/chat/completions",
228+
expect.any(Object)
229+
);
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)