Skip to content
75 changes: 72 additions & 3 deletions src/tools/store_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zodToJsonSchema from 'zod-to-json-schema';

import { ApifyClient } from '../apify-client.js';
import { HelperTools } from '../const.js';
import type { ActorStorePruned, HelperTool, PricingInfo, ToolEntry } from '../types.js';
import type { ActorStorePruned, ApifyStorePricingModel, HelperTool, PricingInfo, ToolEntry } from '../types.js';

function pruneActorStoreInfo(response: ActorStoreList): ActorStorePruned {
const stats = response.stats || {};
Expand Down Expand Up @@ -38,9 +38,10 @@ export async function searchActorsByKeywords(
apifyToken: string,
limit: number | undefined = undefined,
offset: number | undefined = undefined,
pricingModel: ApifyStorePricingModel | undefined = undefined,
): Promise<ActorStorePruned[] | null> {
const client = new ApifyClient({ token: apifyToken });
const results = await client.store().list({ search, limit, offset });
const results = await client.store().list({ search, limit, offset, pricingModel });
return results.items.map((x) => pruneActorStoreInfo(x));
}

Expand Down Expand Up @@ -68,6 +69,61 @@ export const searchActorsArgsSchema = z.object({
.describe('Filters the results by the specified category.'),
});

/**
* Filters out actors with the 'FLAT_PRICE_PER_MONTH' pricing model (rental actors).
*
* @param actors - Array of ActorStorePruned objects to filter.
* @returns Array of actors excluding those with 'FLAT_PRICE_PER_MONTH' pricing model.
*/
function filterRentalActors(
actors: ActorStorePruned[],
): ActorStorePruned[] {
// Store list API does not support filtering by two pricing models at once,
// so we filter the results manually after fetching them.
return actors.filter((actor) => (actor.currentPricingInfo.pricingModel as ApifyStorePricingModel) !== 'FLAT_PRICE_PER_MONTH');
}

/**
* Fallback function to fetch actors if no rental actors are found.
* Fetches both free and pay-per-result actors and merges them in a zig-zag order.
*
* @param search - Search keywords for actors.
* @param apifyToken - Apify API token.
* @param limit - Maximum number of actors to return.
* @param offset - Number of actors to skip from the start.
* @returns Array of ActorStorePruned objects, alternating between free and pay-per-result actors.
*/
async function getFallbackActors(
search: string,
apifyToken: string,
limit: number | undefined,
offset: number | undefined,
): Promise<ActorStorePruned[]> {
const freeActors = await searchActorsByKeywords(
search,
apifyToken,
limit,
offset,
'FREE',
);
const payPerResultActors = await searchActorsByKeywords(
search,
apifyToken,
limit,
offset,
'PRICE_PER_DATASET_ITEM',
);
const allActors: ActorStorePruned[] = [];
// Push Actors in zig-zag order to ensure that we return all Actors
// in relevant order.
const maxLength = Math.max(freeActors?.length || 0, payPerResultActors?.length || 0);
for (let i = 0; i < maxLength; i++) {
if (freeActors && freeActors[i]) allActors.push(freeActors[i]);
if (payPerResultActors && payPerResultActors[i]) allActors.push(payPerResultActors[i]);
}
return allActors;
}

/**
* https://docs.apify.com/api/v2/store-get
*/
Expand All @@ -88,12 +144,25 @@ export const searchActors: ToolEntry = {
call: async (toolArgs) => {
const { args, apifyToken } = toolArgs;
const parsed = searchActorsArgsSchema.parse(args);
const actors = await searchActorsByKeywords(
let actors = await searchActorsByKeywords(
parsed.search,
apifyToken,
parsed.limit,
parsed.offset,
);
actors = filterRentalActors(actors || []);
if (actors.length === 0) {
// If no non-rental actors found, search for free and pay-per-result actors directly
// and sort them by total stars.
// This is a fallback to ensure we return some results.
actors = await getFallbackActors(
parsed.search,
apifyToken,
parsed.limit,
parsed.offset,
);
}

return { content: actors?.map((item) => ({ type: 'text', text: JSON.stringify(item) })) };
},
} as HelperTool,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,5 @@ export interface ToolCacheEntry {
expiresAt: number;
tool: ToolEntry;
}

export type ApifyStorePricingModel = 'FREE' | 'FLAT_PRICE_PER_MONTH' | 'PRICE_PER_DATASET_ITEM';