Skip to content

Commit a871460

Browse files
Merge pull request #91 from mlakatkou/GS-3371
[add] Integrate an AI Assistant Using Tool Calls tutorial
2 parents 334102b + d44b051 commit a871460

3 files changed

Lines changed: 392 additions & 1 deletion

File tree

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
---
2+
title: "Integrate an AI Assistant Using Tool Calls"
3+
sidebar_label: "AI Assistant"
4+
description: "How to integrate an AI assistant with DHTMLX Gantt using backend tool calls and frontend command execution"
5+
---
6+
7+
# Integrate an AI Assistant Using Tool Calls
8+
9+
This guide shows how to connect a chat assistant to a DHTMLX Gantt application using tool calls.
10+
11+
- Backend: handles model calls, stores conversation state, and decides which actions can be executed.
12+
- Frontend: executes approved commands and updates the Gantt chart in the browser.
13+
14+
A full example is available here: [Gantt Maker AI Demo](https://github.com/DHTMLX/gantt-maker-ai-demo)
15+
16+
See the "Features" section in the demo for a complete list of supported capabilities.
17+
18+
The following sections focus on the minimal integration pattern.
19+
20+
## How the integration works
21+
22+
The assistant flow:
23+
24+
```text
25+
user message
26+
-> frontend sends the message to the backend
27+
-> backend calls the model with tools
28+
-> model returns a tool call
29+
-> backend forwards the tool call to the frontend
30+
-> frontend executes the Gantt command
31+
-> frontend returns the result
32+
-> backend saves the result
33+
-> backend calls the model again
34+
-> frontend receives the assistant response
35+
```
36+
37+
## Sending user messages
38+
39+
The frontend sends user messages to the backend. The message contains only the user input. Additional data, such as the current Gantt state, is requested separately when needed.
40+
41+
```ts
42+
function sendUserMessage(message: string): void {
43+
if (!message) {
44+
return;
45+
}
46+
47+
socket.emit('user_msg', JSON.stringify({ message }));
48+
}
49+
```
50+
51+
## Call the model
52+
53+
The backend receives user messages, stores them in the conversation history, and calls the model with the available tools.
54+
55+
```ts
56+
function getHistory(socketId: string) {
57+
if (!history.has(socketId)) {
58+
history.set(socketId, [
59+
{
60+
role: 'system',
61+
content: `
62+
You control a Gantt chart using tools.
63+
64+
Rules:
65+
- Use tools to perform actions.
66+
- Do not describe actions in text if a tool can be used.
67+
- Prefer calling tools over explaining.
68+
`
69+
}
70+
]);
71+
}
72+
73+
return history.get(socketId);
74+
}
75+
76+
socket.on('user_msg', async (payload: UserMsgPayload | string) => {
77+
const { message } = typeof payload === 'string' ? JSON.parse(payload) : payload;
78+
79+
const history = getHistory(socket.id);
80+
81+
saveMessage(socket.id, {
82+
role: 'user',
83+
content: message,
84+
});
85+
86+
const response = await openai.chat.completions.create({
87+
model: MODEL,
88+
messages: history,
89+
tools,
90+
tool_choice: 'auto',
91+
});
92+
93+
const assistantMessage = response.choices[0].message;
94+
});
95+
```
96+
97+
The system message is stored in the conversation history and added only once per session.
98+
99+
## Tool schema
100+
101+
The backend defines tools that the model can call. Each tool describes an allowed action and its parameters.
102+
103+
For example, a `zoom` tool lets the model request a change of the Gantt scale:
104+
105+
```ts
106+
export const tools = [
107+
{
108+
type: 'function',
109+
function: {
110+
name: 'zoom',
111+
description: 'Change the Gantt zoom level or fit the chart into view.',
112+
parameters: {
113+
type: 'object',
114+
additionalProperties: false,
115+
properties: {
116+
level: {
117+
type: 'string',
118+
enum: ["hour", "day", "week", "month", "quarter", "year", "fit"],
119+
},
120+
},
121+
required: ['level'],
122+
},
123+
},
124+
},
125+
];
126+
```
127+
128+
## Forward tool calls
129+
130+
When the model returns a tool call, the backend parses its arguments and forwards it to the frontend. The frontend executes the command and returns the result.
131+
132+
```ts
133+
function parseToolArguments(rawArgs: string): Record<string, unknown> {
134+
const parsed = JSON.parse(rawArgs);
135+
136+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
137+
throw new Error('Tool arguments must be a JSON object');
138+
}
139+
140+
return parsed as Record<string, unknown>;
141+
}
142+
143+
function requestClientToolExecution(
144+
socket: Socket,
145+
payload: ClientToolRequest
146+
): Promise<ClientToolResult> {
147+
return new Promise((resolve, reject) => {
148+
const timeout = setTimeout(() => {
149+
reject(new Error(`Timed out waiting for tool result: ${payload.cmd}`));
150+
}, 15000);
151+
152+
socket.emit('tool_call', payload, (result: ClientToolResult) => {
153+
clearTimeout(timeout);
154+
resolve(result);
155+
});
156+
});
157+
}
158+
159+
async function executeToolCall({
160+
socket,
161+
call,
162+
}: {
163+
socket: Socket;
164+
call: ChatCompletionMessageToolCall;
165+
}): Promise<ClientToolResult> {
166+
return requestClientToolExecution(socket, {
167+
toolCallId: call.id,
168+
cmd: call.function.name,
169+
params: parseToolArguments(call.function.arguments),
170+
});
171+
}
172+
```
173+
174+
## Command runner
175+
176+
The command runner defines how tool calls map to DHTMLX Gantt API calls. It acts as a boundary between model output and the Gantt API.
177+
178+
Only predefined commands should be executed. Every backend tool name must have a matching frontend command, and unknown commands must fail.
179+
180+
```ts
181+
function runCommand(cmd: string, params: Record<string, unknown>): void {
182+
switch (cmd) {
183+
case 'zoom':
184+
if (params.level === 'fit') {
185+
gantt.ext.zoomToFit();
186+
} else {
187+
gantt.ext.zoom.setLevel(params.level as string);
188+
}
189+
break;
190+
191+
default:
192+
throw new Error(`Unsupported command: ${cmd}`);
193+
}
194+
}
195+
```
196+
197+
## Execute commands
198+
199+
The frontend receives tool calls, executes the requested command, and returns the result. A successful command returns the current chart state with `gantt.serialize()`, while a failed command returns an error.
200+
201+
```ts
202+
socket.on('tool_call', (payload: ClientToolRequest, ack?: (result: ClientToolResult) => void) => {
203+
try {
204+
runCommand(payload.cmd, payload.params);
205+
206+
ack?.({
207+
ok: true,
208+
cmd: payload.cmd,
209+
data: gantt.serialize(),
210+
});
211+
} catch (error) {
212+
ack?.({
213+
ok: false,
214+
cmd: payload.cmd,
215+
error: error instanceof Error ? error.message : String(error),
216+
});
217+
}
218+
});
219+
```
220+
221+
## Conversation loop
222+
223+
Tool calls are part of the conversation, not the final assistant response. After a command is executed, the result is stored in the conversation history and sent back to the model.
224+
225+
The model may request multiple tool calls in sequence. In this case, the backend repeats the execution cycle until the model returns a message without tool calls.
226+
227+
```ts
228+
saveMessage(socket.id, {
229+
role: 'assistant',
230+
content: null,
231+
tool_calls: assistantMessage.tool_calls,
232+
});
233+
234+
let currentMessage = assistantMessage;
235+
236+
while (currentMessage.tool_calls) {
237+
for (const call of currentMessage.tool_calls) {
238+
const result = await executeToolCall({ socket, call });
239+
240+
saveMessage(socket.id, {
241+
role: 'tool',
242+
tool_call_id: call.id,
243+
content: JSON.stringify(result),
244+
});
245+
}
246+
247+
const followUp = await openai.chat.completions.create({
248+
model: MODEL,
249+
messages: getHistory(socket.id),
250+
tools,
251+
tool_choice: 'auto',
252+
});
253+
254+
currentMessage = followUp.choices[0].message;
255+
256+
saveMessage(socket.id, currentMessage);
257+
}
258+
259+
socket.emit('assistant_msg', currentMessage.content ?? '');
260+
```
261+
262+
## State-aware commands
263+
264+
Some commands do not depend on the current chart state (for example, `zoom`). Commands that modify existing tasks require access to the current Gantt state so the model can reference task ids and prepare updates.
265+
266+
The `get_gantt_state` tool returns the current result of `gantt.serialize()` without modifying the chart. The model can then call `update_tasks` to apply changes based on this state.
267+
268+
```text
269+
User: Move the QA task two days later
270+
-> get_gantt_state
271+
-> tool result (gantt.serialize())
272+
-> update_tasks
273+
-> tool result with updated gantt.serialize()
274+
-> final assistant reply
275+
```
276+
277+
Tool schema for reading the current state:
278+
279+
```ts
280+
{
281+
type: 'function',
282+
function: {
283+
name: 'get_gantt_state',
284+
description: 'Return the current Gantt tasks and links.',
285+
parameters: {
286+
type: 'object',
287+
additionalProperties: false,
288+
properties: {},
289+
},
290+
},
291+
}
292+
```
293+
294+
Tool schema for updating existing tasks:
295+
296+
```ts
297+
{
298+
type: 'function',
299+
function: {
300+
name: 'update_tasks',
301+
description: 'Update existing Gantt tasks by id.',
302+
parameters: {
303+
type: 'object',
304+
additionalProperties: false,
305+
properties: {
306+
tasks: {
307+
type: 'array',
308+
items: {
309+
type: 'object',
310+
additionalProperties: false,
311+
properties: {
312+
id: { type: ['string', 'number'] },
313+
text: { type: 'string' },
314+
start_date: { type: 'string', format: 'date' },
315+
duration: { type: 'number' },
316+
progress: {
317+
type: 'number',
318+
minimum: 0,
319+
maximum: 1,
320+
},
321+
},
322+
required: ['id'],
323+
},
324+
},
325+
},
326+
required: ['tasks'],
327+
},
328+
},
329+
}
330+
```
331+
332+
The command runner cases on the frontend:
333+
334+
```ts
335+
case 'get_gantt_state':
336+
break;
337+
338+
case 'update_tasks':
339+
gantt.batchUpdate(() => {
340+
for (const task of params.tasks as Array<Record<string, unknown>>) {
341+
const taskId = task.id as string | number;
342+
343+
if (!gantt.isTaskExists(taskId)) {
344+
throw new Error(`Task does not exist: ${taskId}`);
345+
}
346+
347+
const existingTask = gantt.getTask(taskId);
348+
Object.assign(existingTask, task);
349+
gantt.updateTask(taskId);
350+
}
351+
});
352+
break;
353+
```
354+
355+
:::note
356+
Gantt accepts both Date objects and ISO 8601 date strings, which are parsed automatically.
357+
358+
For other string formats, Gantt uses `gantt.config.date_format` and `gantt.templates.parse_date`.
359+
:::
360+
361+
## Troubleshooting
362+
363+
If the model returns text instead of a tool call, the backend may not be passing `tools` to `chat.completions.create()` or `tool_choice` may not be set to `'auto'`.
364+
365+
If the backend waits for a tool result until timeout, the frontend may not be calling the acknowledgement callback in the tool call handler.
366+
367+
If a command appears successful but the Gantt chart does not change, the command runner may be reporting success for unsupported commands. Only executed commands should return `{ ok: true, cmd, data }`.
368+
369+
If `JSON.parse` fails while reading tool arguments, return a deterministic error or store `{ ok: false, error }` as the tool result so the model can produce a useful response.
370+
371+
If the model updates the wrong task, the integration may be missing a state-reading step (`get_gantt_state`) or validation of task ids on the frontend.
372+
373+
## Summary
374+
375+
The integration connects an AI assistant to DHTMLX Gantt through backend tool calls and frontend command execution.
376+
377+
The backend handles model calls, tool schemas, and conversation history. The frontend executes approved commands on the Gantt instance and returns the current chart state.
378+
379+
The command runner defines the boundary between model output and the Gantt API: only explicitly supported commands can modify the chart.
380+
381+
After each tool call, the backend stores the result and calls the model again so the final assistant response is based on the actual execution result.
382+
383+
## Related Materials
384+
385+
- [Gantt Maker AI Demo](https://github.com/DHTMLX/gantt-maker-ai-demo)
386+
- [Live Gantt Maker AI Demo](https://dhtmlx.com/docs/demo/ai-gantt-maker/)
387+
- [DHTMLX Gantt documentation](https://docs.dhtmlx.com/gantt/)
388+
- [OpenAI API documentation](https://developers.openai.com/api/docs)
389+
- [Socket.IO documentation](https://socket.io/docs/v4/)

docs/integrations/ai-tools/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ If you're adding AI capabilities to your application (OpenAI-compatible APIs, as
2121
Guides:
2222

2323
- [Semantic Search](./semantic-search/) - add meaning-based task discovery to your Gantt chart using embeddings and cosine similarity.
24+
- [AI Assistant](./ai-assistant/) - connect a chat assistant to DHTMLX Gantt using tool calls and client-side command execution.
2425

2526
Demo apps:
2627

0 commit comments

Comments
 (0)