1import dotenv from 'dotenv';
2import { Actor, ApifyClient, log } from 'apify';
3import { StateGraph, StateGraphArgs, START, END } from '@langchain/langgraph';
4import LocationAgent from './agents/location.js';
5import AirbnbDeciderAgent from './agents/airbnb_decider.js';
6import AirbnbResearcherAgent from './agents/airbnb_researcher.js';
7import SuccessAgent from './agents/success.js';
8import TripadvisorDeciderAgent from './agents/tripadvisor_decider.js';
9import TripadvisorResearcherAgent from './agents/tripadvisor_researcher.js';
10import { chargeForActorStart } from './utils/ppe_handler.js';
11
12
13dotenv.config();
14
15
16
17
18type Input = {
19 instructions: string;
20 modelName?: string;
21 openaiApiKey?: string;
22 debug?: boolean;
23}
24
25
26
27
28await Actor.init();
29await chargeForActorStart();
30
31
32const {
33 instructions,
34 modelName = 'gpt-4o-mini',
35 openaiApiKey,
36 debug,
37} = await Actor.getInput() as Input;
38if (debug) {
39 log.setLevel(log.LEVELS.DEBUG);
40} else {
41 log.setLevel(log.LEVELS.INFO);
42}
43
44const tokenCostActive = (openaiApiKey ?? '').length === 0;
45if (tokenCostActive) {
46 log.info("No openaiApiKey was detected. You'll be charged for token usage.");
47} else {
48 log.info("Env openaiApiKey detected. You won't be charged for token usage.");
49}
50if (!instructions) {
51 throw new Error('Instructions are required. Create an INPUT.json file in the `storage/key_value_stores/default` folder and add the respective keys.');
52}
53
54
55const userToken = process.env.USER_APIFY_TOKEN;
56if (!userToken) {
57 throw new Error('User token is required. Export your Apify secret as USER_APIFY_TOKEN.');
58}
59const apifyClient = new ApifyClient({
60 token: userToken,
61});
62
63
64
65
66type StateSchema = {
67 instructions: string;
68 bestLocations: string;
69 airbnbDatasetId: string;
70 airbnbTotalItems: number;
71 airbnbAssumptions: string;
72 airbnbItemsChecked: number;
73 airbnbRecommendations: string[];
74 tripadvisorDatasetId: string;
75 tripadvisorTotalItems: number;
76 tripadvisorAssumptions: string;
77 tripadvisorItemsChecked: number;
78 tripadvisorRecommendations: string[];
79 output: string;
80}
81
82const graphState: StateGraphArgs<StateSchema>['channels'] = {
83 instructions: {
84 value: (x?: string, y?: string) => y ?? x ?? '',
85 default: () => instructions,
86 },
87 bestLocations: {
88 value: (x?: string, y?: string) => y ?? x ?? '',
89 default: () => '',
90 },
91 airbnbDatasetId: {
92 value: (x?: string, y?: string) => y ?? x ?? '',
93 default: () => '',
94 },
95 airbnbTotalItems: {
96 value: (x?: number, y?: number) => y ?? x ?? 0,
97 default: () => 0,
98 },
99 airbnbAssumptions: {
100 value: (x?: string, y?: string) => y ?? x ?? '',
101 default: () => '',
102 },
103 airbnbItemsChecked: {
104 value: (x?: number, y?: number) => y ?? x ?? 0,
105 default: () => 0,
106 },
107 airbnbRecommendations: {
108 value: (x?: string[], y?: string[]) => y ?? x ?? [],
109 default: () => [],
110 },
111 tripadvisorDatasetId: {
112 value: (x?: string, y?: string) => y ?? x ?? '',
113 default: () => '',
114 },
115 tripadvisorTotalItems: {
116 value: (x?: number, y?: number) => y ?? x ?? 0,
117 default: () => 0,
118 },
119 tripadvisorAssumptions: {
120 value: (x?: string, y?: string) => y ?? x ?? '',
121 default: () => '',
122 },
123 tripadvisorItemsChecked: {
124 value: (x?: number, y?: number) => y ?? x ?? 0,
125 default: () => 0,
126 },
127 tripadvisorRecommendations: {
128 value: (x?: string[], y?: string[]) => y ?? x ?? [],
129 default: () => [],
130 },
131 output: {
132 value: (x?: string, y?: string) => y ?? x ?? '',
133 default: () => '',
134 },
135};
136
137async function locationNode(state: StateSchema) {
138 const locationAgent = new LocationAgent({
139 apifyClient,
140 modelName,
141 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
142 log,
143 });
144 const { agentExecutor, costHandler } = locationAgent;
145 const response = await agentExecutor.invoke({ input: state.instructions });
146 log.debug(`locationAgent 🤖 : ${response.output}`);
147 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
148 return { bestLocations: response.output };
149}
150
151async function airbnbResearcherNode(state: StateSchema) {
152 const airbnbResearcherAgent = new AirbnbResearcherAgent({
153 apifyClient,
154 modelName,
155 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
156 log,
157 });
158 const { agentExecutor, costHandler } = airbnbResearcherAgent;
159 const input = 'The user asked sent this exact query:\n\n'
160 + `'${state.instructions}'\n\n`
161 + 'You asked someone for help to get the best locations in that city and country. '
162 + 'The answer you received was this:\n\n'
163 + `'${state.bestLocations}'\n\n`
164 + 'Please make some research to gather information to be able to answer the user accordingly.';
165 const response = await agentExecutor.invoke({ input });
166 log.debug(`airbnbResearcherAgent 🤖 : ${response.output}`);
167 const { datasetId, totalItems, assumptions } = JSON.parse(response.output);
168 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
169 return {
170 airbnbDatasetId: datasetId,
171 airbnbTotalItems: totalItems,
172 airbnbAssumptions: assumptions,
173 };
174}
175
176async function airbnbDeciderNode(state: StateSchema) {
177 const airbnbDeciderAgent = new AirbnbDeciderAgent({
178 apifyClient,
179 modelName,
180 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
181 log,
182 });
183 const { agentExecutor, costHandler } = airbnbDeciderAgent;
184 const input = 'The user asked sent this exact query:\n\n'
185 + `'${state.instructions}'\n\n`
186 + 'You asked someone for help to get the best locations in that city and country. '
187 + 'The answer you received was this:\n\n'
188 + `'${state.bestLocations}'\n\n`
189 + 'You asked someone else for help to get the best places to stay in in those locations using Airbnb. '
190 + 'The answer you received was this:\n\n'
191 + `Airbnb Apify dataset ID: '${state.airbnbDatasetId}'\n`
192 + `Total items in Airbnb dataset: '${state.airbnbTotalItems}'\n`
193 + `Assumptions made when cretating the Airbnb dataset:: '${state.airbnbAssumptions}'\n`
194 + `Total results already checked in Airbnb dataset: airbnbItemsChecked='${state.airbnbItemsChecked}'\n\n`
195 + 'Please explore theavailable datasets for the best results.';
196 const response = await agentExecutor.invoke({ input });
197 log.debug(`airbnbDeciderAgent 🤖 : ${response.output}`);
198 const { itemsChecked, recommendations } = JSON.parse(response.output);
199 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
200 return {
201 airbnbItemsChecked: state.airbnbItemsChecked + itemsChecked,
202 airbnbRecommendations: [
203 ...state.airbnbRecommendations,
204 ...recommendations
205 ]
206 };
207}
208
209const airbnbResultsRouter = (state: StateSchema) => {
210 const allResultsChecked = state.airbnbItemsChecked >= state.airbnbTotalItems;
211 log.debug(`allResultsChecked: ${allResultsChecked}`);
212 const hundredResultsChecked = state.airbnbItemsChecked > 100;
213 log.debug(`hundredResultsChecked: ${hundredResultsChecked}`);
214 const fiveRecommendationsFound = state.airbnbRecommendations.length > 5;
215 log.debug(`fiveRecommendationsFound: ${fiveRecommendationsFound}`);
216 if (
217 allResultsChecked
218 || hundredResultsChecked
219 || fiveRecommendationsFound
220 ) {
221
222 return 'success';
223 }
224
225 return 'airbnbDecider';
226};
227
228async function tripadvisorResearcherNode(state: StateSchema) {
229 const tripadvisorResearcherAgent = new TripadvisorResearcherAgent({
230 apifyClient,
231 modelName,
232 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
233 log,
234 });
235 const { agentExecutor, costHandler } = tripadvisorResearcherAgent;
236 const input = 'The user asked sent this exact query:\n\n'
237 + `'${state.instructions}'\n\n`
238 + 'You asked someone for help to get the best locations in that city and country. '
239 + 'The answer you received was this:\n\n'
240 + `'${state.bestLocations}'\n\n`
241 + 'Please make some research to gather information to be able to answer the user accordingly.';
242 const response = await agentExecutor.invoke({ input });
243 log.debug(`tripadvisorResearcherAgent 🤖 : ${response.output}`);
244 const { datasetId, totalItems, assumptions } = JSON.parse(response.output);
245 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
246 return {
247 tripadvisorDatasetId: datasetId,
248 tripadvisorTotalItems: totalItems,
249 tripadvisorAssumptions: assumptions,
250 };
251}
252
253async function tripadvisorDeciderNode(state: StateSchema) {
254 const tripadvisorDeciderAgent = new TripadvisorDeciderAgent({
255 apifyClient,
256 modelName,
257 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
258 log,
259 });
260 const { agentExecutor, costHandler } = tripadvisorDeciderAgent;
261 const input = 'The user asked sent this exact query:\n\n'
262 + `'${state.instructions}'\n\n`
263 + 'You asked someone for help to get the best locations in that city and country. '
264 + 'The answer you received was this:\n\n'
265 + `'${state.bestLocations}'\n\n`
266 + 'You asked someone else for help to get the attractions in in those locations using Tripadvisor. '
267 + 'The answer you received was this:\n\n'
268 + `Tripadvisor Apify dataset ID: '${state.tripadvisorDatasetId}'\n`
269 + `Total items in Tripadvisor dataset: '${state.tripadvisorTotalItems}'\n`
270 + `Assumptions made when cretating the Tripadvisor dataset:: '${state.tripadvisorAssumptions}'\n`
271 + `Total results already checked in Tripadvisor dataset: tripadvisorItemsChecked='${state.tripadvisorItemsChecked}'\n\n`
272 + 'Please explore theavailable datasets for the best results.';
273 const response = await agentExecutor.invoke({ input });
274 log.debug(`tripadvisorDeciderAgent 🤖 : ${response.output}`);
275 const { itemsChecked, recommendations } = JSON.parse(response.output);
276 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
277 return {
278 tripadvisorItemsChecked: state.tripadvisorItemsChecked + itemsChecked,
279 tripadvisorRecommendations: [
280 ...state.tripadvisorRecommendations, ...recommendations
281 ]
282 };
283}
284
285const tripadvisorResultsRouter = (state: StateSchema) => {
286 const allResultsChecked = state.tripadvisorItemsChecked
287 >= state.tripadvisorTotalItems;
288 log.debug(`allResultsChecked: ${allResultsChecked}`);
289 const hundredResultsChecked = state.tripadvisorItemsChecked > 100;
290 log.debug(`hundredResultsChecked: ${hundredResultsChecked}`);
291 const fiveRecommendationsFound = state.tripadvisorRecommendations.length > 5;
292 log.debug(`fiveRecommendationsFound: ${fiveRecommendationsFound}`);
293 if (
294 allResultsChecked
295 || hundredResultsChecked
296 || fiveRecommendationsFound
297 ) {
298
299 return 'success';
300 }
301
302 return 'tripadvisorDecider';
303};
304
305async function successNode(state: StateSchema) {
306 const successAgent = new SuccessAgent({
307 apifyClient,
308 modelName,
309 openaiApiKey: openaiApiKey ?? process.env.OPENAI_API_KEY,
310 log,
311 });
312 const { agentExecutor, costHandler } = successAgent;
313 const input = 'The user asked sent this exact query:\n\n'
314 + `'${state.instructions}'\n\n`
315 + 'You asked someone for help to get the best locations in that city and state. '
316 + 'The answer you received was this:\n\n'
317 + `'${state.bestLocations}'\n\n`
318 + 'You asked someone else for help to get the best places to stay in those locations using Airbnb and their expert jugdgement. '
319 + 'The answer you received was this:\n\n'
320 + `Assumptions made when searching Airbnb: '${state.tripadvisorAssumptions}'\n\n`
321 + `Total places to stay checked: '${state.tripadvisorItemsChecked}'\n`
322 + `Total places to stay recommended: '${state.tripadvisorRecommendations.length}'\n`
323 + `Best places to stay in stringified JSON format: '${JSON.stringify(state.tripadvisorRecommendations)}'\n`
324 + 'You asked someone else for help to get the attractions in those locations using Tripadvisor and their expert jugdgement. '
325 + 'The answer you received was this:\n\n'
326 + `Assumptions made when searching Tripadvisor: '${state.tripadvisorAssumptions}'\n\n`
327 + `Total attractions checked: '${state.tripadvisorItemsChecked}'\n`
328 + `Total attractions recommended: '${state.tripadvisorRecommendations.length}'\n`
329 + `Best attractions in stringified JSON format: '${JSON.stringify(state.tripadvisorRecommendations)}'\n`
330 + 'Please mention the explored neighborhoods and their descriptions to the user.'
331 + 'Please select the top 3 places to stay and recommend the top 3 attractions (hopefully in the same neighborhoods).'
332 + 'Please answer the user explaining the whole process in markdown format.';
333 const response = await agentExecutor.invoke({ input });
334 log.debug(`successNode 🤖 : ${response.output}`);
335 await costHandler.logOrChargeForTokens(modelName, tokenCostActive);
336 return { output: response.output };
337}
338
339const graph = new StateGraph({ channels: graphState })
340 .addNode('location', locationNode)
341 .addNode('airbnbResearcher', airbnbResearcherNode)
342 .addNode('airbnbDecider', airbnbDeciderNode)
343 .addNode('tripadvisorResearcher', tripadvisorResearcherNode)
344 .addNode('tripadvisorDecider', tripadvisorDeciderNode)
345 .addNode('success', successNode)
346 .addEdge(START, 'location')
347 .addEdge('location', 'airbnbResearcher')
348 .addEdge('airbnbResearcher', 'airbnbDecider')
349 .addConditionalEdges('airbnbDecider', airbnbResultsRouter)
350 .addEdge('location', 'tripadvisorResearcher')
351 .addEdge('tripadvisorResearcher', 'tripadvisorDecider')
352 .addConditionalEdges('tripadvisorDecider', tripadvisorResultsRouter)
353 .addEdge('success', END);
354
355const runnable = graph.compile();
356
357const response = await runnable.invoke(
358 { input: instructions },
359 { configurable: { thread_id: 42 } },
360);
361
362log.debug(`Agent 🤖 : ${response.output}`);
363
364await Actor.pushData({
365 actorName: 'AI Travel Agent',
366 response: response.output,
367});
368
369await Actor.exit();