Skip to content

Commit a76bec9

Browse files
committed
feat: add function calling tools
- add custom intructions for each prompt that are silently passed to the agent. - add minor code fixes - add .env.template and change AZURE_AI_PROJECTS_CONNECTION_STRING env name to AI_FOUNDRY_PROJECT_CONNECTION_STRING as in .env
1 parent 453f5ef commit a76bec9

File tree

8 files changed

+179
-29
lines changed

8 files changed

+179
-29
lines changed

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
AZURE_AI_PROJECTS_CONNECTION_STRING=
1+
AI_FOUNDRY_PROJECT_CONNECTION_STRING=
22
AI_SEARCH_CONNECTION_STRING=
33
AI_MODEL=gpt-4o

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ downloads/*.png
1414
# Ignore any Webpack generated files
1515
dist
1616

17-
tsconfig.tsbuildinfo
17+
tsconfig.tsbuildinfo
18+
19+
.DS_Store

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config/promptConfig.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ export const promptConfig: Record<string, PromptConfig> = {
55
prompt: 'I need to solve the equation `3x + 11 = 14`. Can you help me?',
66
emoji: '🧮'
77
},
8+
localCpusUsage: {
9+
prompt: 'What is the average CPUs usage on my local machine?',
10+
instructions: 'Always use the function tool to get the CPUs usage.',
11+
emoji: '💾',
12+
tool: 'function-tool',
13+
executor: null, // set at runtime (see toolService.ts)
14+
},
815
codeGenerator: {
916
prompt: 'Write a function that finds prime numbers',
17+
instructions: 'Always use the code interpreter to generate code.',
1018
tool: 'code-interpreter',
1119
emoji: '💻'
1220
},
@@ -16,19 +24,22 @@ export const promptConfig: Record<string, PromptConfig> = {
1624
- Relationships between Price, Mileage, and Year.
1725
- Sales by SalesPerson.
1826
- Sales by Make, Model, and Year for 2023.`,
27+
instructions: 'Always use the code interpreter to generate visualizations.',
1928
tool: 'code-interpreter',
2029
filePath: './files/car_sales_data.csv',
2130
emoji: '📊'
2231
},
2332
hotelReviews: {
24-
prompt: 'Tell me about the hotel reviews in the HotelReviews_data.csv.',
33+
prompt: 'Tell me about the hotel reviews in the hotel_reviews_data.csv.',
34+
instructions: 'Always use the code interpreter to analyze the data.',
2535
tool: 'code-interpreter',
2636
fileId: '',
2737
filePath: './files/hotel_reviews_data.csv',
2838
emoji: '🏨'
2939
},
3040
insuranceCoverage: {
3141
prompt: 'What are my health insurance plan coverage types?',
42+
instructions: 'Always use the AI Search tool to analyze the data.',
3243
tool: 'ai-search',
3344
emoji: '🏥'
3445
}

src/services/agentService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function processSelectedPrompt(client: AIProjectsClient, selectedKe
1818

1919
const agent = await client.agents.createAgent(model, {
2020
name: 'my-agent',
21-
instructions: 'You are a helpful agent',
21+
instructions: `You are a helpful agent. ${selectedPromptConfig.instructions}`,
2222
temperature: 0.5,
2323
tools: selectedPromptConfig.tools,
2424
toolResources: selectedPromptConfig.toolResources
@@ -27,7 +27,7 @@ export async function processSelectedPrompt(client: AIProjectsClient, selectedKe
2727
const thread = await client.agents.createThread();
2828
await addMessageToThread(client, thread.id, selectedPromptConfig.prompt);
2929

30-
const runId = await runAgent(client, thread, agent);
30+
const runId = await runAgent(client, thread, agent, selectedPromptConfig);
3131
await printThreadMessages(selectedPromptConfig, client, thread.id);
3232
await getRunStats(runId, client, thread);
3333

src/services/threadService.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import { AIProjectsClient, AgentThreadOutput, DoneEvent, ErrorEvent, MessageStreamEvent, RunStreamEvent, isOutputOfType } from '@azure/ai-projects';
3-
import type { AgentOutput, MessageDeltaChunk, MessageDeltaTextContent, MessageImageFileContentOutput, MessageTextContentOutput, OpenAIPageableListOfThreadMessageOutput, ThreadRunOutput } from '@azure/ai-projects';
3+
import type { AgentOutput, FunctionToolDefinitionOutput, MessageDeltaChunk, MessageDeltaTextContent, MessageImageFileContentOutput, MessageTextContentOutput, OpenAIPageableListOfThreadMessageOutput, SubmitToolOutputsActionOutput, ThreadRunOutput, ToolOutput } from '@azure/ai-projects';
44
import { PromptConfig } from '../types.js';
55

66
export async function addMessageToThread(client: AIProjectsClient, threadId: string, message: string) {
@@ -18,10 +18,18 @@ export async function printThreadMessages(selectedPromptConfig: PromptConfig, cl
1818
const messagesArray = messages.data;
1919
for (let i = messagesArray.length - 1; i >= 0; i--) {
2020
const m = messagesArray[i];
21+
const content = m.content[0];
22+
23+
if (!content) {
24+
// Skip if no content
25+
continue;
26+
}
27+
2128
console.log(`Type: ${m.content[0].type}`);
2229
if (isOutputOfType<MessageTextContentOutput>(m.content[0], 'text')) {
2330
const textContent = m.content[0] as MessageTextContentOutput;
24-
console.log(`Text: ${textContent.text.value}`);
31+
const role = m.role === 'user' ? 'User' : 'Agent';
32+
console.log(`${role}: ${textContent.text.value}`);
2533
}
2634
}
2735

@@ -77,7 +85,7 @@ export async function getRunStats(runId: string, client: AIProjectsClient, threa
7785
}
7886
}
7987

80-
export async function runAgent(client: AIProjectsClient, thread: AgentThreadOutput, agent: AgentOutput): Promise<string> {
88+
export async function runAgent(client: AIProjectsClient, thread: AgentThreadOutput, agent: AgentOutput, promptConfig: PromptConfig): Promise<string> {
8189
const run = client.agents.createRun(thread.id, agent.id);
8290
const streamEventMessages = await run.stream();
8391
let runId = '';
@@ -104,8 +112,16 @@ export async function runAgent(client: AIProjectsClient, thread: AgentThreadOutp
104112
}
105113
break;
106114

115+
case RunStreamEvent.ThreadRunRequiresAction:
116+
console.log("\nThread run requires action.");
117+
const run = eventMessage.data as ThreadRunOutput;
118+
if (run.requiredAction) {
119+
await processRequiredAction(client, thread, run, promptConfig);
120+
}
121+
break;
122+
107123
case RunStreamEvent.ThreadRunCompleted:
108-
console.log('\nThread run completed.');
124+
console.log("\nThread run completed.");
109125
break;
110126

111127
case ErrorEvent.Error:
@@ -119,4 +135,42 @@ export async function runAgent(client: AIProjectsClient, thread: AgentThreadOutp
119135
}
120136

121137
return runId;
122-
}
138+
}
139+
140+
async function processRequiredAction(
141+
client: AIProjectsClient,
142+
thread: AgentThreadOutput,
143+
run: ThreadRunOutput,
144+
promptConfig: PromptConfig
145+
) {
146+
if (
147+
run.requiredAction &&
148+
isOutputOfType<SubmitToolOutputsActionOutput>(
149+
run.requiredAction,
150+
"submit_tool_outputs"
151+
)
152+
) {
153+
const submitToolOutputsActionOutput =
154+
run.requiredAction as SubmitToolOutputsActionOutput;
155+
const toolCalls = submitToolOutputsActionOutput.submitToolOutputs.toolCalls;
156+
const toolResponses: ToolOutput[] = [];
157+
for (const toolCall of toolCalls) {
158+
if (isOutputOfType<FunctionToolDefinitionOutput>(toolCall, "function")) {
159+
const toolResponse = promptConfig.executor?.invokeTool(toolCall) as ToolOutput;
160+
console.log(
161+
`💡 Function tool ${toolCall.id} - ${toolResponse.output}`
162+
);
163+
if (toolResponse) {
164+
toolResponses.push(toolResponse);
165+
}
166+
}
167+
}
168+
if (toolResponses.length > 0) {
169+
run = await client.agents.submitToolOutputsToRun(
170+
thread.id,
171+
run.id,
172+
toolResponses
173+
);
174+
}
175+
}
176+
}

src/services/toolService.ts

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import fs from 'fs';
2-
import { AIProjectsClient, ToolUtility } from '@azure/ai-projects';
3-
import { aiSearchConnectionString } from '../config/env.js';
4-
import { PromptConfig } from '../types.js';
1+
import fs from "fs";
2+
import { cpus } from "os";
3+
import {
4+
AIProjectsClient,
5+
FunctionToolDefinition,
6+
FunctionToolDefinitionOutput,
7+
RequiredToolCallOutput,
8+
ToolOutput,
9+
ToolUtility,
10+
} from "@azure/ai-projects";
11+
import { aiSearchConnectionString } from "../config/env.js";
12+
import { PromptConfig } from "../types.js";
513

614
export async function createTools(selectedPromptConfig: PromptConfig, client: AIProjectsClient) {
715
if (selectedPromptConfig.tool === 'code-interpreter') {
@@ -13,11 +21,18 @@ export async function createTools(selectedPromptConfig: PromptConfig, client: AI
1321
selectedPromptConfig.toolResources = codeInterpreterTool.resources;
1422
}
1523

16-
if (selectedPromptConfig.aiSearch) {
17-
const azureAISearchTool = await createAISearchTool(client);
18-
selectedPromptConfig.tools = [azureAISearchTool.definition];
19-
selectedPromptConfig.toolResources = azureAISearchTool.resources;
20-
}
24+
if (selectedPromptConfig.aiSearch) {
25+
const azureAISearchTool = await createAISearchTool(client);
26+
selectedPromptConfig.tools = [azureAISearchTool.definition];
27+
selectedPromptConfig.toolResources = azureAISearchTool.resources;
28+
}
29+
30+
if (selectedPromptConfig.tool === "function-tool") {
31+
selectedPromptConfig.executor = FunctionToolExecutor;
32+
selectedPromptConfig.tools = [
33+
...FunctionToolExecutor.getFunctionDefinitions(),
34+
];
35+
}
2136
}
2237

2338
export async function getCodeInterpreter(selectedPromptConfig: PromptConfig, client: AIProjectsClient) {
@@ -41,4 +56,70 @@ export async function createAISearchTool(client: AIProjectsClient) {
4156
aiSearchConnection.id,
4257
aiSearchConnection.name
4358
);
44-
}
59+
}
60+
61+
class FunctionToolFactory {
62+
static getCpuUsage() {
63+
return `CPU Usage: ${cpus()[0].model} ${Math.floor( (cpus().reduce((acc, core) => acc + core.speed, 0)) / 1000)}%`;
64+
}
65+
}
66+
67+
export class FunctionToolExecutor {
68+
static functionTools: {
69+
func: Function;
70+
definition: FunctionToolDefinition;
71+
}[] = [
72+
{
73+
func: FunctionToolFactory.getCpuUsage,
74+
...ToolUtility.createFunctionTool({
75+
name: "getCpuUsage",
76+
description: "Gets the current CPU usage of the system.",
77+
parameters: {},
78+
}),
79+
},
80+
];
81+
82+
public static invokeTool(
83+
toolCall: RequiredToolCallOutput & FunctionToolDefinitionOutput
84+
): ToolOutput | undefined {
85+
console.log(`💡 Function tool ${toolCall.id} - ${toolCall.function.name}`);
86+
const args: string[] = [];
87+
if (toolCall.function.parameters) {
88+
try {
89+
const params = JSON.parse(toolCall.function.parameters) as Record<
90+
string,
91+
string
92+
>;
93+
for (const key in params) {
94+
if (Object.prototype.hasOwnProperty.call(params, key)) {
95+
args.push(params[key] as string);
96+
}
97+
}
98+
} catch (error) {
99+
console.error(
100+
`Failed to parse parameters: ${toolCall.function.parameters}`,
101+
error
102+
);
103+
return undefined;
104+
}
105+
}
106+
const result = this.functionTools
107+
.find((tool) => tool.definition.function.name === toolCall.function.name)
108+
?.func(...args);
109+
110+
console.log(`💡 Function tool ${toolCall.id} - completed`);
111+
112+
return result
113+
? {
114+
toolCallId: toolCall.id,
115+
output: JSON.stringify(result),
116+
}
117+
: undefined;
118+
}
119+
120+
public static getFunctionDefinitions(): FunctionToolDefinition[] {
121+
return FunctionToolExecutor.functionTools.map((tool) => {
122+
return tool.definition;
123+
});
124+
}
125+
}

src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { ToolDefinition } from '@azure/ai-projects';
22

33
export interface PromptConfig {
44
prompt: string;
5+
instructions?: string;
56
emoji?: string;
6-
tool?: string;
7+
tool?: "code-interpreter" | "function-tool" | "ai-search";
78
filePath?: string;
89
fileId?: string;
910
aiSearch?: boolean;
11+
executor?: any;
1012
tools?: ToolDefinition[];
1113
toolResources?: any;
1214
}

0 commit comments

Comments
 (0)