Skip to content

Commit df22556

Browse files
authoredFeb 5, 2025
Merge pull request #97 from restackio/agentTool
Agent Tool example
2 parents a192ab7 + 8a03edc commit df22556

19 files changed

+543
-0
lines changed
 

‎agent-tool/.env.example

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
# For inference
3+
RESTACK_API_KEY=
4+
5+
# For Restack Cloud deployment
6+
RESTACK_ENGINE_ID=
7+
RESTACK_ENGINE_ADDRESS=
8+
RESTACK_ENGINE_API_KEY=
9+
RESTACK_ENGINE_API_ADDRESS=
10+

‎agent-tool/chat_post.png

40.6 KB
Loading

‎agent-tool/chat_put.png

57.7 KB
Loading

‎agent-tool/chat_run.png

128 KB
Loading

‎agent-tool/eventAgent.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { client } from "./src/client";
2+
3+
export type EventInput = {
4+
agentId: string;
5+
runId: string;
6+
};
7+
8+
async function eventAgent(input: EventInput) {
9+
try {
10+
await client.sendAgentEvent({
11+
event: {
12+
name: "message",
13+
input: { content: "Sales on boots?" },
14+
},
15+
agent: {
16+
agentId: input.agentId,
17+
runId: input.runId,
18+
},
19+
});
20+
21+
await client.sendAgentEvent({
22+
event: {
23+
name: "end",
24+
},
25+
agent: {
26+
agentId: input.agentId,
27+
runId: input.runId,
28+
},
29+
});
30+
31+
process.exit(0); // Exit the process successfully
32+
} catch (error) {
33+
console.error("Error sending event to agent:", error);
34+
process.exit(1); // Exit the process with an error code
35+
}
36+
}
37+
38+
eventAgent({
39+
agentId: "your-agent-id",
40+
runId: "your-run-id",
41+
});

‎agent-tool/package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "agent-tool",
3+
"version": "0.0.1",
4+
"description": "Restack Agent with Tool Calling example",
5+
"scripts": {
6+
"dev": "open-cli http://localhost:5233 && tsx watch --include src src/services.ts",
7+
"build": "tsc --build",
8+
"clean": "rm -rf node_modules",
9+
"schedule": "tsx scheduleAgent.ts",
10+
"event": "tsx eventAgent.ts"
11+
},
12+
"dependencies": {
13+
"@restackio/ai": "^0.0.104",
14+
"@temporalio/workflow": "1.11.6",
15+
"openai": "^4.80.1",
16+
"zod": "^3.24.1"
17+
},
18+
"devDependencies": {
19+
"@types/node": "20.16.9",
20+
"dotenv-cli": "^7.4.2",
21+
"open-cli": "^8.0.0",
22+
"prettier": "3.3.3",
23+
"tsx": "4.19.2",
24+
"typescript": "^5.7.2"
25+
}
26+
}

‎agent-tool/readme.md

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Restack Agent with Tool Calling
2+
3+
A sample repository with an agent using tool calling.
4+
5+
## Requirements
6+
7+
- **Node 20+**
8+
9+
## Start Restack
10+
11+
To start Restack, use the following Docker command:
12+
13+
```bash
14+
docker run -d --pull always --name restack -p 5233:5233 -p 6233:6233 -p 7233:7233 ghcr.io/restackio/restack:main
15+
```
16+
17+
## Install dependencies and start services
18+
19+
```bash
20+
npm install
21+
npm run dev
22+
```
23+
24+
This will start a Node.js app with Restack Services.
25+
Your code will be running and syncing with Restack to execute agents.
26+
27+
## Run agents
28+
29+
### from UI
30+
31+
You can run agents from the UI by clicking the "Run" button.
32+
33+
![Run agents from UI](./chat_post.png)
34+
35+
### from API
36+
37+
You can run agents from the API by using the generated endpoint:
38+
39+
`POST http://localhost:6233/api/agents/agentChatTool`
40+
41+
### from any client
42+
43+
You can run agents with any client connected to Restack, for example:
44+
45+
```bash
46+
npm run schedule
47+
```
48+
49+
executes `scheduleAgent.ts` which will connect to Restack and execute the `agentChatTool` agent.
50+
51+
## Send events to the Agent
52+
53+
### from UI
54+
55+
You can send events like message or end from the UI.
56+
57+
![Send events from UI](./chat_put.png)
58+
59+
And see the events in the run:
60+
61+
![See events in UI](./chat_run.png)
62+
63+
### from API
64+
65+
You can send events to the agent by using the following endpoint:
66+
67+
`PUT http://localhost:6233/api/agents/agentChatTool/:agentId/:runId`
68+
69+
with the payload:
70+
71+
```json
72+
{
73+
"eventName": "message",
74+
"eventInput": { "content": "Sales on boots?" }
75+
}
76+
```
77+
78+
to send messages to the agent.
79+
80+
or
81+
82+
```json
83+
{
84+
"eventName": "end"
85+
}
86+
```
87+
88+
to end the conversation with the agent.
89+
90+
### from any client
91+
92+
You can send event to the agent with any client connected to Restack, for example:
93+
94+
Modify agentId and runId in eventAgent.ts and then run:
95+
96+
```bash
97+
npm run event
98+
```
99+
100+
It will connect to Restack and send 2 events to the agent, one to generate another agent and another one to end the conversation.
101+
102+
## Deploy on Restack Cloud
103+
104+
To deploy the application on Restack, you can create an account at [https://console.restack.io](https://console.restack.io)

‎agent-tool/scheduleAgent.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { client } from "./src/client";
2+
import { agentChatTool } from "./src/agents/agent";
3+
export type InputSchedule = {
4+
name: string;
5+
};
6+
7+
async function scheduleAgent(input: InputSchedule) {
8+
try {
9+
const agentId = `${Date.now()}-${agentChatTool.name}`;
10+
const runId = await client.scheduleAgent({
11+
agentName: agentChatTool.name,
12+
agentId,
13+
input,
14+
});
15+
16+
const result = await client.getAgentResult({ agentId, runId });
17+
18+
console.log("Agent result:", result);
19+
20+
process.exit(0); // Exit the process successfully
21+
} catch (error) {
22+
console.error("Error scheduling agent:", error);
23+
process.exit(1); // Exit the process with an error code
24+
}
25+
}
26+
27+
scheduleAgent({
28+
name: "test",
29+
});

‎agent-tool/src/agents/agent.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
defineEvent,
3+
onEvent,
4+
condition,
5+
log,
6+
step,
7+
} from "@restackio/ai/agent";
8+
import * as functions from "../functions";
9+
10+
export type EndEvent = {
11+
end: boolean;
12+
};
13+
14+
export const messageEvent = defineEvent<functions.Message[]>("message");
15+
export const endEvent = defineEvent("end");
16+
17+
type AgentChatOutput = {
18+
messages: functions.Message[];
19+
};
20+
21+
export async function agentChatTool(): Promise<AgentChatOutput> {
22+
let endReceived = false;
23+
let messages: functions.Message[] = [];
24+
25+
const tools = await step<typeof functions.getTools>({}).getTools();
26+
27+
onEvent(messageEvent, async ({ content }: functions.Message) => {
28+
messages.push({ role: "user", content });
29+
const result = await step<typeof functions.llmChat>({}).llmChat({
30+
messages,
31+
tools,
32+
});
33+
34+
messages.push(result);
35+
36+
if (result.tool_calls) {
37+
for (const toolCall of result.tool_calls) {
38+
switch (toolCall.function.name) {
39+
case "lookupSales":
40+
const toolResult = await step<typeof functions.lookupSales>(
41+
{}
42+
).lookupSales(JSON.parse(toolCall.function.arguments));
43+
44+
messages.push({
45+
role: "tool",
46+
tool_call_id: toolCall.id,
47+
content: JSON.stringify(toolResult),
48+
});
49+
50+
const toolChatResult = await step<typeof functions.llmChat>(
51+
{}
52+
).llmChat({
53+
messages,
54+
tools,
55+
});
56+
57+
messages.push(toolChatResult);
58+
59+
break;
60+
default:
61+
break;
62+
}
63+
}
64+
}
65+
return messages;
66+
});
67+
68+
onEvent(endEvent, async () => {
69+
endReceived = true;
70+
});
71+
72+
await condition(() => endReceived);
73+
74+
log.info("end condition met");
75+
return { messages };
76+
}

‎agent-tool/src/agents/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./agent";

‎agent-tool/src/client.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Restack from "@restackio/ai";
2+
3+
import "dotenv/config";
4+
5+
export const connectionOptions = {
6+
engineId: process.env.RESTACK_ENGINE_ID!,
7+
address: process.env.RESTACK_ENGINE_ADDRESS!,
8+
apiKey: process.env.RESTACK_ENGINE_API_KEY!,
9+
apiAddress: process.env.RESTACK_ENGINE_API_ADDRESS!,
10+
};
11+
12+
export const client = new Restack(
13+
process.env.RESTACK_ENGINE_API_KEY ? connectionOptions : undefined
14+
);

‎agent-tool/src/functions/getTools.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { zodFunction } from "openai/helpers/zod";
2+
import { lookupSales } from "./lookupSales";
3+
import { LookupSalesInput } from "./toolTypes";
4+
5+
export const getTools = async () => {
6+
const tools = [
7+
zodFunction({
8+
name: lookupSales.name,
9+
parameters: LookupSalesInput,
10+
}),
11+
];
12+
return tools;
13+
};

‎agent-tool/src/functions/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./llmChat";
2+
export * from "./lookupSales";
3+
export * from "./getTools";

‎agent-tool/src/functions/llmChat.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { FunctionFailure, log } from "@restackio/ai/function";
2+
import {
3+
ChatCompletionCreateParamsNonStreaming,
4+
ChatCompletionMessage,
5+
ChatCompletionMessageParam,
6+
ChatCompletionSystemMessageParam,
7+
ChatCompletionTool,
8+
ChatCompletionToolMessageParam,
9+
ChatCompletionUserMessageParam,
10+
} from "openai/resources/chat/completions";
11+
12+
import { openaiClient } from "../utils/client";
13+
14+
export type Message =
15+
| ChatCompletionSystemMessageParam
16+
| ChatCompletionUserMessageParam
17+
| ChatCompletionToolMessageParam;
18+
19+
export type OpenAIChatInput = {
20+
systemContent?: string;
21+
model?: string;
22+
messages: Message[];
23+
tools?: ChatCompletionTool[];
24+
};
25+
26+
export const llmChat = async ({
27+
systemContent = "",
28+
model = "gpt-4o-mini",
29+
messages,
30+
tools,
31+
}: OpenAIChatInput): Promise<ChatCompletionMessage> => {
32+
try {
33+
const openai = openaiClient({});
34+
35+
const chatParams: ChatCompletionCreateParamsNonStreaming = {
36+
messages: [
37+
...(systemContent
38+
? [{ role: "system" as const, content: systemContent }]
39+
: []),
40+
...(messages ?? []),
41+
],
42+
model,
43+
tools,
44+
};
45+
46+
log.debug("OpenAI chat completion params", {
47+
chatParams,
48+
});
49+
50+
const completion = await openai.chat.completions.create(chatParams);
51+
52+
const message = completion.choices[0].message;
53+
54+
return message;
55+
} catch (error) {
56+
throw FunctionFailure.nonRetryable(`Error OpenAI chat: ${error}`);
57+
}
58+
};
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { log } from "@restackio/ai/function";
2+
import { LookupSalesInputType } from "./toolTypes";
3+
4+
type SalesItem = {
5+
item_id: number;
6+
type: string;
7+
name: string;
8+
retail_price_usd: number;
9+
sale_price_usd: number;
10+
sale_discount_pct: number;
11+
};
12+
13+
type LookupSalesOutput = {
14+
sales: SalesItem[];
15+
};
16+
17+
export const lookupSales = async ({
18+
category,
19+
}: LookupSalesInputType): Promise<LookupSalesOutput> => {
20+
try {
21+
log.info("lookupSales function started", { category });
22+
23+
const items: SalesItem[] = [
24+
{
25+
item_id: 101,
26+
type: "snowboard",
27+
name: "Alpine Blade",
28+
retail_price_usd: 450,
29+
sale_price_usd: 360,
30+
sale_discount_pct: 20,
31+
},
32+
{
33+
item_id: 102,
34+
type: "snowboard",
35+
name: "Peak Bomber",
36+
retail_price_usd: 499,
37+
sale_price_usd: 374,
38+
sale_discount_pct: 25,
39+
},
40+
{
41+
item_id: 201,
42+
type: "apparel",
43+
name: "Thermal Jacket",
44+
retail_price_usd: 120,
45+
sale_price_usd: 84,
46+
sale_discount_pct: 30,
47+
},
48+
{
49+
item_id: 202,
50+
type: "apparel",
51+
name: "Insulated Pants",
52+
retail_price_usd: 150,
53+
sale_price_usd: 112,
54+
sale_discount_pct: 25,
55+
},
56+
{
57+
item_id: 301,
58+
type: "boots",
59+
name: "Glacier Grip",
60+
retail_price_usd: 250,
61+
sale_price_usd: 200,
62+
sale_discount_pct: 20,
63+
},
64+
{
65+
item_id: 302,
66+
type: "boots",
67+
name: "Summit Steps",
68+
retail_price_usd: 300,
69+
sale_price_usd: 210,
70+
sale_discount_pct: 30,
71+
},
72+
{
73+
item_id: 401,
74+
type: "accessories",
75+
name: "Goggles",
76+
retail_price_usd: 80,
77+
sale_price_usd: 60,
78+
sale_discount_pct: 25,
79+
},
80+
{
81+
item_id: 402,
82+
type: "accessories",
83+
name: "Warm Gloves",
84+
retail_price_usd: 60,
85+
sale_price_usd: 48,
86+
sale_discount_pct: 20,
87+
},
88+
];
89+
90+
const filtered_items: SalesItem[] =
91+
category === "any"
92+
? items
93+
: items.filter((item) => item.type === category);
94+
95+
// Sort by largest discount first
96+
filtered_items.sort((a, b) => b.sale_discount_pct - a.sale_discount_pct);
97+
98+
return { sales: filtered_items };
99+
} catch (e) {
100+
log.error("lookupSales function failed", { error: e });
101+
throw e;
102+
}
103+
};

‎agent-tool/src/functions/toolTypes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from "zod";
2+
3+
export const LookupSalesInput = z.object({
4+
category: z.enum(["snowboard", "apparel", "boots", "accessories", "any"]),
5+
});
6+
7+
export type LookupSalesInputType = z.infer<typeof LookupSalesInput>;

‎agent-tool/src/services.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { llmChat, lookupSales, getTools } from "./functions";
2+
import { client } from "./client";
3+
4+
async function services() {
5+
const agentsPath = require.resolve("./agents");
6+
try {
7+
await Promise.all([
8+
client.startService({
9+
agentsPath: agentsPath,
10+
functions: { llmChat, lookupSales, getTools },
11+
}),
12+
]);
13+
14+
console.log("Services running successfully.");
15+
} catch (e) {
16+
console.error("Failed to run services", e);
17+
}
18+
}
19+
20+
services().catch((err) => {
21+
console.error("Error running services:", err);
22+
});

‎agent-tool/src/utils/client.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import OpenAI from "openai/index";
2+
import "dotenv/config";
3+
4+
let openaiInstance: OpenAI | null = null;
5+
6+
export const openaiClient = ({
7+
apiKey = process.env.RESTACK_API_KEY,
8+
}: {
9+
apiKey?: string;
10+
}): OpenAI => {
11+
if (!apiKey) {
12+
throw new Error("API key is required to create OpenAI client.");
13+
}
14+
15+
if (!openaiInstance) {
16+
openaiInstance = new OpenAI({
17+
baseURL: "https://ai.restack.io",
18+
apiKey,
19+
});
20+
}
21+
return openaiInstance;
22+
};

‎agent-tool/tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6",
4+
"module": "Node16",
5+
"strict": true,
6+
"esModuleInterop": true,
7+
"skipLibCheck": true,
8+
"outDir": "./dist",
9+
"rootDir": "./src",
10+
"resolveJsonModule": true
11+
},
12+
"include": ["src/**/*.ts"],
13+
"exclude": ["node_modules"]
14+
}

0 commit comments

Comments
 (0)
Please sign in to comment.