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();