Skip to content

Commit cd52003

Browse files
authored
refactor: extract shared core logic into src/tools/core/ modules (#466)
Extract mode-agnostic business logic from tool handlers into dedicated core modules, establishing the foundation for UI/normal mode separation. Changes: - Create src/tools/core/actor-execution.ts (callActorGetDataset + type) - Create src/tools/core/actor-tools-factory.ts (getActorsAsTools, getNormalActorsAsTools, getMCPServersAsTools, enrichActorToolOutputSchemas) - Move src/utils/actor-response.ts to src/tools/core/actor-response.ts - Move filterRentalActors from tools/store_collection.ts to utils/actor-search.ts (fixes circular dependency) - Update all imports; re-export from tools/actor.ts for backward compat Pure refactor — no behavioral changes. All 248 unit tests pass.
1 parent 47f8828 commit cd52003

File tree

8 files changed

+442
-415
lines changed

8 files changed

+442
-415
lines changed

src/mcp/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { createResourceService } from '../resources/resource_service.js';
5454
import type { AvailableWidget } from '../resources/widgets.js';
5555
import { resolveAvailableWidgets } from '../resources/widgets.js';
5656
import { getTelemetryEnv, trackToolCall } from '../telemetry.js';
57+
import { buildActorResponseContent } from '../tools/core/actor-response.js';
5758
import { callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
5859
import { decodeDotPropertyNames } from '../tools/utils.js';
5960
import type {
@@ -68,7 +69,6 @@ import type {
6869
ToolEntry,
6970
ToolStatus,
7071
} from '../types.js';
71-
import { buildActorResponseContent } from '../utils/actor-response.js';
7272
import { logHttpError, redactSkyfirePayId } from '../utils/logging.js';
7373
import { buildMCPResponse, buildUsageMeta } from '../utils/mcp.js';
7474
import { createProgressTracker } from '../utils/progress.js';

src/tools/actor.ts

Lines changed: 13 additions & 385 deletions
Large diffs are not rendered by default.

src/tools/core/actor-execution.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { ActorCallOptions, ActorRun } from 'apify-client';
2+
3+
import log from '@apify/log';
4+
5+
import type { ApifyClient } from '../../apify-client.js';
6+
import { TOOL_MAX_OUTPUT_CHARS } from '../../const.js';
7+
import type { ActorDefinitionStorage, DatasetItem } from '../../types.js';
8+
import { ensureOutputWithinCharLimit, getActorDefinitionStorageFieldNames } from '../../utils/actor.js';
9+
import { logHttpError, redactSkyfirePayId } from '../../utils/logging.js';
10+
import type { ProgressTracker } from '../../utils/progress.js';
11+
import type { JsonSchemaProperty } from '../../utils/schema-generation.js';
12+
import { generateSchemaFromItems } from '../../utils/schema-generation.js';
13+
14+
// Define a named return type for callActorGetDataset
15+
export type CallActorGetDatasetResult = {
16+
runId: string;
17+
datasetId: string;
18+
itemCount: number;
19+
schema: JsonSchemaProperty;
20+
previewItems: DatasetItem[];
21+
usageTotalUsd?: number;
22+
usageUsd?: Record<string, number>;
23+
};
24+
25+
/**
26+
* Calls an Apify Actor and retrieves metadata about the dataset results.
27+
*
28+
* This function executes an Actor and returns summary information instead with a result items preview of the full dataset
29+
* to prevent overwhelming responses. The actual data can be retrieved using the get-actor-output tool.
30+
*
31+
* It requires the `APIFY_TOKEN` environment variable to be set.
32+
* If the `APIFY_IS_AT_HOME` the dataset items are pushed to the Apify dataset.
33+
*
34+
* @param {string} actorName - The name of the Actor to call.
35+
* @param {unknown} input - The input to pass to the actor.
36+
* @param {ApifyClient} apifyClient - The Apify client to use for authentication.
37+
* @returns {Promise<CallActorGetDatasetResult | null>} - A promise that resolves to an object containing the actor run and dataset items.
38+
* @throws {Error} - Throws an error if the `APIFY_TOKEN` is not set
39+
*/
40+
export async function callActorGetDataset(options: {
41+
actorName: string;
42+
input: unknown;
43+
apifyClient: ApifyClient;
44+
callOptions?: ActorCallOptions;
45+
progressTracker?: ProgressTracker | null;
46+
abortSignal?: AbortSignal;
47+
previewOutput?: boolean;
48+
mcpSessionId?: string;
49+
}): Promise<CallActorGetDatasetResult | null> {
50+
const {
51+
actorName,
52+
input,
53+
apifyClient,
54+
callOptions,
55+
progressTracker,
56+
abortSignal,
57+
previewOutput = true,
58+
mcpSessionId,
59+
} = options;
60+
const CLIENT_ABORT = Symbol('CLIENT_ABORT'); // Just internal symbol to identify client abort
61+
const actorClient = apifyClient.actor(actorName);
62+
63+
// Start the actor run
64+
const actorRun: ActorRun = await actorClient.start(input, callOptions);
65+
66+
// Start progress tracking if tracker is provided
67+
if (progressTracker) {
68+
progressTracker.startActorRunUpdates(actorRun.id, apifyClient, actorName);
69+
}
70+
71+
// Create abort promise that handles both API abort and race rejection
72+
const abortPromise = async () => new Promise<typeof CLIENT_ABORT>((resolve) => {
73+
abortSignal?.addEventListener('abort', async () => {
74+
// Abort the actor run via API
75+
try {
76+
await apifyClient.run(actorRun.id).abort({ gracefully: false });
77+
} catch (e) {
78+
logHttpError(e, 'Error aborting Actor run', { runId: actorRun.id });
79+
}
80+
// Reject to stop waiting
81+
resolve(CLIENT_ABORT);
82+
}, { once: true });
83+
});
84+
85+
// Wait for completion or cancellation
86+
const potentialAbortedRun = await Promise.race([
87+
apifyClient.run(actorRun.id).waitForFinish(),
88+
...(abortSignal ? [abortPromise()] : []),
89+
]);
90+
91+
if (potentialAbortedRun === CLIENT_ABORT) {
92+
log.info('Actor run aborted by client', { actorName, mcpSessionId, input: redactSkyfirePayId(input) });
93+
return null;
94+
}
95+
const completedRun = potentialAbortedRun as ActorRun;
96+
97+
// Process the completed run
98+
const dataset = apifyClient.dataset(completedRun.defaultDatasetId);
99+
const [datasetItems, defaultBuild] = await Promise.all([
100+
dataset.listItems(),
101+
(await actorClient.defaultBuild()).get(),
102+
]);
103+
104+
// Generate schema using the shared utility
105+
const generatedSchema = generateSchemaFromItems(datasetItems.items, {
106+
clean: true,
107+
arrayMode: 'all',
108+
});
109+
const schema = generatedSchema || { type: 'object', properties: {} };
110+
111+
/**
112+
* Get important fields that are using in any dataset view as they MAY be used in filtering to ensure the output fits
113+
* the tool output limits. Client has to use the get-actor-output tool to retrieve the full dataset or filtered out fields.
114+
*/
115+
const storageDefinition = defaultBuild?.actorDefinition?.storages?.dataset as ActorDefinitionStorage | undefined;
116+
const importantProperties = getActorDefinitionStorageFieldNames(storageDefinition || {});
117+
const previewItems = previewOutput
118+
? ensureOutputWithinCharLimit(datasetItems.items, importantProperties, TOOL_MAX_OUTPUT_CHARS)
119+
: [];
120+
121+
return {
122+
runId: actorRun.id,
123+
datasetId: completedRun.defaultDatasetId,
124+
itemCount: datasetItems.count,
125+
schema,
126+
previewItems,
127+
usageTotalUsd: completedRun.usageTotalUsd,
128+
usageUsd: completedRun.usageUsd as Record<string, number> | undefined,
129+
};
130+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { CallActorGetDatasetResult } from '../tools/actor.js';
2-
import type { DatasetItem } from '../types.js';
1+
import type { DatasetItem } from '../../types.js';
2+
import type { CallActorGetDatasetResult } from './actor-execution.js';
33

44
/**
55
* Result from buildActorResponseContent function.

0 commit comments

Comments
 (0)