Skip to content

Commit 6ee4267

Browse files
authored
Merge pull request #106 from khawkins/refactoring
Miscellaneous refactors
2 parents 7c380d2 + 0d95793 commit 6ee4267

File tree

25 files changed

+788
-354
lines changed

25 files changed

+788
-354
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-workflow/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@salesforce/magen-mcp-workflow",
3-
"version": "0.1.0",
3+
"version": "0.0.1",
44
"type": "module",
55
"files": [
66
"dist",

packages/mcp-workflow/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export {
8888
type UserInputExtractionNodeOptions,
8989
} from './nodes/index.js';
9090

91+
// Routers
92+
export { CheckPropertiesFulfilledRouter } from './routers/index.js';
93+
9194
// Base Service Classes
9295
export { AbstractService } from './services/index.js';
9396

packages/mcp-workflow/src/nodes/abstractBaseNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { StateType, StateDefinition } from '@langchain/langgraph';
1414
*
1515
* Example:
1616
* ```
17-
* const MyWorkflowState = Annotation.Root({ count: Annotation<number>() });
17+
* const MyWorkflowState = Annotation.Root({ count: Annotation<number> });
1818
* type State = typeof MyWorkflowState.State; // This is StateType<typeof MyWorkflowState.spec>
1919
*
2020
* class IncrementNode extends BaseNode<State> {

packages/mcp-workflow/src/nodes/getUserInput/factory.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { PropertyFulfilledResult } from '../../common/types.js';
2727
* @example
2828
* ```typescript
2929
* const MyState = Annotation.Root({
30-
* userInput: Annotation<unknown>(),
31-
* platform: Annotation<string>(),
32-
* projectName: Annotation<string>(),
30+
* userInput: Annotation<unknown>,
31+
* platform: Annotation<string>,
32+
* projectName: Annotation<string>,
3333
* });
3434
*
3535
* const properties = {

packages/mcp-workflow/src/nodes/getUserInput/node.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ export interface GetUserInputNodeOptions<TState extends StateType<StateDefinitio
5353
/**
5454
* Property name in state that contains user input to extract from.
5555
* Must be a valid property of TState.
56-
* Defaults to 'userInput' if not specified.
5756
*
5857
* @example
5958
*
60-
* // State has a 'userInput' property - use default
61-
* createUserInputExtractionNode({ ... });
59+
* // State has a 'userInput' property
60+
* createGetUserInputNode({
61+
* userInputProperty: 'userInput',
62+
* ...
63+
* });
6264
*
63-
* // State has a 'currentUtterance' property - specify it
64-
* createUserInputExtractionNode({
65+
* // State has a 'currentUtterance' property
66+
* createGetUserInputNode({
6567
* userInputProperty: 'currentUtterance',
6668
* ...
6769
* });

packages/mcp-workflow/src/nodes/userInputExtraction/factory.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import { UserInputExtractionNodeOptions, UserInputExtractionNode } from './node.
2929
* @example
3030
* ```typescript
3131
* const MyState = Annotation.Root({
32-
* userInput: Annotation<unknown>(),
33-
* platform: Annotation<string>(),
34-
* projectName: Annotation<string>(),
32+
* userInput: Annotation<unknown>,
33+
* platform: Annotation<string>,
34+
* projectName: Annotation<string>,
3535
* });
3636
*
3737
* const properties = {
@@ -62,14 +62,12 @@ export function createUserInputExtractionNode<TState extends StateType<StateDefi
6262
extractionService,
6363
toolExecutor = new LangGraphToolExecutor(),
6464
logger = createComponentLogger('UserInputExtractionNode'),
65-
getUserInput = (state: TState) => {
66-
return state.userInput;
67-
},
65+
userInputProperty,
6866
} = options;
6967

7068
// Create default service implementation if not provided
7169
const service: InputExtractionServiceProvider =
7270
extractionService ?? new InputExtractionService(toolId, toolExecutor, logger);
7371

74-
return new UserInputExtractionNode(service, requiredProperties, getUserInput);
72+
return new UserInputExtractionNode(service, requiredProperties, userInputProperty);
7573
}

packages/mcp-workflow/src/nodes/userInputExtraction/node.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,25 @@ export interface UserInputExtractionNodeOptions<TState extends StateType<StateDe
4444
logger?: Logger;
4545

4646
/**
47-
* Function to get the userInput field from state
48-
* Default: expects state.userInput
47+
* Property name in state that contains user input to extract from.
48+
* Must be a valid property of TState.
49+
*
50+
* @example
51+
*
52+
* // State has a 'userInput' property
53+
* createUserInputExtractionNode({
54+
* userInputProperty: 'userInput',
55+
* ...
56+
* });
57+
*
58+
* // State has a 'currentUtterance' property
59+
* createUserInputExtractionNode({
60+
* userInputProperty: 'currentUtterance',
61+
* ...
62+
* });
63+
*
4964
*/
50-
getUserInput?: (state: TState) => unknown;
65+
userInputProperty: keyof TState;
5166
}
5267

5368
export class UserInputExtractionNode<
@@ -56,13 +71,13 @@ export class UserInputExtractionNode<
5671
constructor(
5772
private readonly extractionService: InputExtractionServiceProvider,
5873
private readonly requiredProperties: PropertyMetadataCollection,
59-
private readonly getUserInput: (state: TState) => unknown
74+
private readonly userInputProperty: keyof TState
6075
) {
6176
super('userInputExtraction');
6277
}
6378

6479
execute = (state: TState): Partial<TState> => {
65-
const userInput = this.getUserInput(state);
80+
const userInput = state[this.userInputProperty];
6681
const result = this.extractionService.extractProperties(userInput, this.requiredProperties);
6782
return result.extractedProperties as unknown as Partial<TState>;
6883
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { StateType, StateDefinition } from '@langchain/langgraph';
9+
import { PropertyMetadataCollection } from '../common/propertyMetadata.js';
10+
import { Logger, createComponentLogger } from '../logging/logger.js';
11+
12+
/**
13+
* Conditional router edge to check whether all required properties have been collected.
14+
*
15+
* This router evaluates the workflow state to determine if all properties specified
16+
* in the required properties collection have been fulfilled (are truthy). Based on
17+
* this evaluation, it routes to either a "fulfilled" or "unfulfilled" node.
18+
*
19+
* @template TState - The state type for the workflow
20+
*
21+
* @example
22+
* ```typescript
23+
* const requiredProperties: PropertyMetadataCollection = {
24+
* platform: {
25+
* zodType: z.enum(['iOS', 'Android']),
26+
* description: 'Target platform',
27+
* friendlyName: 'platform',
28+
* },
29+
* projectName: {
30+
* zodType: z.string(),
31+
* description: 'Project name',
32+
* friendlyName: 'project name',
33+
* },
34+
* };
35+
*
36+
* const router = new CheckPropertiesFulfilledRouter<State>(
37+
* 'continueWorkflow', // Node to route to when all properties are fulfilled
38+
* 'getUserInput', // Node to route to when properties are missing
39+
* requiredProperties
40+
* );
41+
*
42+
* // Use in LangGraph workflow
43+
* workflow.addConditionalEdges('checkProperties', router.execute);
44+
* ```
45+
*/
46+
export class CheckPropertiesFulfilledRouter<TState extends StateType<StateDefinition>> {
47+
private readonly propertiesFulfilledNodeName: string;
48+
private readonly propertiesUnfulfilledNodeName: string;
49+
private readonly requiredProperties: PropertyMetadataCollection;
50+
private readonly logger: Logger;
51+
52+
/**
53+
* Creates a new CheckPropertiesFulfilledRouter.
54+
*
55+
* @param propertiesFulfilledNodeName - The name of the node to route to if all properties are fulfilled
56+
* @param propertiesUnfulfilledNodeName - The name of the node to route to if any property is unfulfilled
57+
* @param requiredProperties - Collection of properties that must be collected from user
58+
* @param logger - Optional logger instance for debugging and monitoring routing decisions.
59+
* If not provided, a default component logger will be created.
60+
*/
61+
constructor(
62+
propertiesFulfilledNodeName: string,
63+
propertiesUnfulfilledNodeName: string,
64+
requiredProperties: PropertyMetadataCollection,
65+
logger?: Logger
66+
) {
67+
this.propertiesFulfilledNodeName = propertiesFulfilledNodeName;
68+
this.propertiesUnfulfilledNodeName = propertiesUnfulfilledNodeName;
69+
this.requiredProperties = requiredProperties;
70+
this.logger = logger || createComponentLogger('CheckPropertiesFulfilledRouter');
71+
}
72+
73+
/**
74+
* Evaluates the state to determine the next node based on property fulfillment.
75+
*
76+
* This method checks each property in the required properties collection.
77+
* If any property is missing or falsy, it routes to the unfulfilled node.
78+
* Only if all properties are present and truthy does it route to the fulfilled node.
79+
*
80+
* @param state - The current workflow state
81+
* @returns The name of the next node to route to
82+
*/
83+
execute = (state: TState): string => {
84+
return this.getPropertyFulfillmentStatus(state);
85+
};
86+
87+
/**
88+
* Internal method to check property fulfillment status.
89+
*
90+
* Iterates through all required properties and checks if they exist
91+
* and are truthy in the state object.
92+
*
93+
* @param state - The current workflow state
94+
* @returns The name of the node to route to based on fulfillment status
95+
*/
96+
private getPropertyFulfillmentStatus(state: TState): string {
97+
const unfulfilledProperties: string[] = [];
98+
99+
for (const propertyName of Object.keys(this.requiredProperties)) {
100+
if (!state[propertyName as keyof TState]) {
101+
unfulfilledProperties.push(propertyName);
102+
}
103+
}
104+
105+
if (unfulfilledProperties.length > 0) {
106+
this.logger.debug('Properties not fulfilled, routing to unfulfilled node', {
107+
unfulfilledProperties,
108+
targetNode: this.propertiesUnfulfilledNodeName,
109+
totalRequired: Object.keys(this.requiredProperties).length,
110+
});
111+
return this.propertiesUnfulfilledNodeName;
112+
}
113+
114+
this.logger.debug('All properties fulfilled, routing to fulfilled node', {
115+
targetNode: this.propertiesFulfilledNodeName,
116+
totalProperties: Object.keys(this.requiredProperties).length,
117+
});
118+
return this.propertiesFulfilledNodeName;
119+
}
120+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
export { CheckPropertiesFulfilledRouter } from './checkPropertiesFulfilledRouter.js';

0 commit comments

Comments
 (0)