Skip to content

Commit fcb41a6

Browse files
authored
Merge pull request #286 from awslabs/custom-user-agent
added support for custom user agent in TS
2 parents bc5474a + af1a6ee commit fcb41a6

File tree

11 files changed

+177
-5
lines changed

11 files changed

+177
-5
lines changed

typescript/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
},
2121
"homepage": "https://github.com/awslabs/multi-agent-orchestrator",
2222
"scripts": {
23+
"prebuild": "npm run generateVersionFile",
2324
"build": "tsc",
2425
"test": "jest",
26+
"generateVersionFile": "echo \"// this file is auto generated, do not modify\nexport const MAOTS_VERSION = '$(jq -r '.version' package.json)';\" > src/common/src/version.ts",
2527
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
2628
"coverage": "jest --coverage"
2729
},

typescript/src/agents/amazonBedrockAgent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BedrockAgentRuntimeClient, InvokeAgentCommand, InvokeAgentCommandOutput
22
import { ConversationMessage, ParticipantRole } from "../types";
33
import { Agent, AgentOptions } from "./agent";
44
import { Logger } from "../utils/logger";
5+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils';
56

67
/**
78
* Options for configuring an Amazon Bedrock agent.
@@ -40,6 +41,7 @@ export class AmazonBedrockAgent extends Agent {
4041
this.client = options.client ? options.client : options.region
4142
? new BedrockAgentRuntimeClient({ region: options.region })
4243
: new BedrockAgentRuntimeClient();
44+
addUserAgentMiddleware(this.client, "bedrock-agent");
4345
this.enableTrace = options.enableTrace || false;
4446
this.streaming = options.streaming || false;
4547
}

typescript/src/agents/bedrockFlowsAgent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ConversationMessage,
55
ParticipantRole
66
} from "../types";
7+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils';
78

89
export interface BedrockFlowsAgentOptions extends AgentOptions {
910
region?: string;
@@ -34,6 +35,8 @@ import {
3435
: new BedrockAgentRuntimeClient()
3536
);
3637

38+
addUserAgentMiddleware(this.bedrockAgentClient, "bedrock-flows-agent");
39+
3740
this.flowIdentifier = options.flowIdentifier;
3841
this.flowAliasIdentifier = options.flowAliasIdentifier;
3942
this.enableTrace = options.enableTrace ?? false;

typescript/src/agents/bedrockLLMAgent.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Retriever } from "../retrievers/retriever";
1515
import { Logger } from "../utils/logger";
1616
import { AgentToolResult, AgentTools } from "../utils/tool";
1717
import { isConversationMessage } from "../utils/helpers";
18+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils';
1819

1920
export interface BedrockLLMAgentOptions extends AgentOptions {
2021
modelId?: string;
@@ -101,6 +102,8 @@ export class BedrockLLMAgent extends Agent {
101102
? new BedrockRuntimeClient({ region: options.region })
102103
: new BedrockRuntimeClient();
103104

105+
addUserAgentMiddleware(this.client, "bedrock-llm-agent")
106+
104107
// Initialize the modelId
105108
this.modelId = options.modelId ?? BEDROCK_MODEL_ID_CLAUDE_3_HAIKU;
106109

@@ -480,4 +483,4 @@ export class BedrockLLMAgent extends Agent {
480483
return match; // If no replacement found, leave the placeholder as is
481484
});
482485
}
483-
}
486+
}

typescript/src/agents/lambdaAgent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ConversationMessage, ParticipantRole } from "../types";
22
import { Agent, AgentOptions } from "./agent";
33
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
4+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils';
45

56
export interface LambdaAgentOptions extends AgentOptions {
67
functionName: string;
78
functionRegion: string;
8-
inputPayloadEncoder?: (inputText: string, ...additionalParams: any) => any;
9+
inputPayloadEncoder?: (inputText: string, ...additionalParams: any) => any;
910
outputPayloadDecoder?: (response: any) => ConversationMessage;
1011
}
1112

@@ -17,6 +18,7 @@ export class LambdaAgent extends Agent {
1718
super(options);
1819
this.options = options;
1920
this.lambdaClient = new LambdaClient({region:this.options.functionRegion});
21+
addUserAgentMiddleware(this.lambdaClient, "lambda-agent");
2022
}
2123

2224
private defaultInputPayloadEncoder(inputText: string, chatHistory: ConversationMessage[], userId: string, sessionId:string, additionalParams?: Record<string, string>):string {
@@ -51,9 +53,9 @@ export class LambdaAgent extends Agent {
5153
FunctionName: this.options.functionName,
5254
Payload: payload,
5355
};
54-
56+
5557
const response = await this.lambdaClient.send(new InvokeCommand(invokeParams));
56-
58+
5759
return new Promise((resolve) => {
5860
const message = this.options.outputPayloadDecoder ? this.options.outputPayloadDecoder(response) : this.defaultOutputPayloaderDecoder(response);
5961
resolve(message);

typescript/src/agents/lexBotAgent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
RecognizeTextCommandOutput,
77
} from "@aws-sdk/client-lex-runtime-v2";
88
import { Logger } from "../utils/logger";
9+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils';
910

1011
/**
1112
* Options for configuring an Amazon Lex Bot agent.
@@ -39,6 +40,8 @@ export class LexBotAgent extends Agent {
3940
this.botAliasId = options.botAliasId;
4041
this.localeId = options.localeId;
4142

43+
addUserAgentMiddleware(this.lexClient, "lex-agent");
44+
4245
// Validate required fields
4346
if (!this.botId || !this.botAliasId || !this.localeId) {
4447
throw new Error("botId, botAliasId, and localeId are required for LexBotAgent");

typescript/src/classifiers/bedrockClassifier.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { Classifier, ClassifierResult } from "./classifier";
1414
import { isClassifierToolInput } from "../utils/helpers";
1515
import { Logger } from "../utils/logger";
1616

17+
import { addUserAgentMiddleware } from '../common/src/awsSdkUtils'
18+
1719

1820
export interface BedrockClassifierOptions {
1921
// Optional: The ID of the Bedrock model to use for classification
@@ -96,6 +98,7 @@ export class BedrockClassifier extends Classifier{
9698
// Initialize default values or use provided options
9799
this.region = options.region || process.env.REGION;
98100
this.client = new BedrockRuntimeClient({region:this.region});
101+
addUserAgentMiddleware(this.client, "bedrock-classifier");
99102
this.modelId = options.modelId || BEDROCK_MODEL_ID_CLAUDE_3_5_SONNET;
100103
// Initialize inferenceConfig only if it's provided in options
101104
this.inferenceConfig = {
@@ -194,4 +197,4 @@ export class BedrockClassifier extends Classifier{
194197
}
195198

196199

197-
}
200+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { MiddlewareArgsLike, SdkClient } from './types/awsSdk';
2+
import { MAOTS_VERSION } from './version';
3+
4+
const EXEC_ENV = process.env.AWS_EXECUTION_ENV || 'NA';
5+
const middlewareOptions = {
6+
relation: 'after',
7+
toMiddleware: 'getUserAgentMiddleware',
8+
name: 'addMaoToUserAgent',
9+
tags: ['MAOTS', 'USER_AGENT'],
10+
};
11+
12+
/**
13+
* Type guard to check if the client provided is a valid AWS SDK v3 client.
14+
*
15+
* @internal
16+
*/
17+
const isSdkClient = (client: unknown): client is SdkClient =>
18+
typeof client === 'object' &&
19+
client !== null &&
20+
'send' in client &&
21+
typeof client.send === 'function' &&
22+
'config' in client &&
23+
client.config !== undefined &&
24+
typeof client.config === 'object' &&
25+
client.config !== null &&
26+
'middlewareStack' in client &&
27+
client.middlewareStack !== undefined &&
28+
typeof client.middlewareStack === 'object' &&
29+
client.middlewareStack !== null &&
30+
'identify' in client.middlewareStack &&
31+
typeof client.middlewareStack.identify === 'function' &&
32+
'addRelativeTo' in client.middlewareStack &&
33+
typeof client.middlewareStack.addRelativeTo === 'function';
34+
35+
/**
36+
* Helper function to create a custom user agent middleware for the AWS SDK v3 clients.
37+
*
38+
* The middleware will append the provided feature name and the current version of
39+
* the Mao for AWS Lambda library to the user agent string.
40+
*
41+
* @example "MAOTS/Bedrock-classifier/2.1.0 PTEnv/nodejs20x"
42+
*
43+
* @param feature The feature name to be added to the user agent
44+
*
45+
* @internal
46+
*/
47+
const customUserAgentMiddleware = (feature: string) => {
48+
return <T extends MiddlewareArgsLike>(next: (arg0: T) => Promise<T>) =>
49+
async (args: T) => {
50+
const existingUserAgent = args.request.headers['user-agent'] || '';
51+
if (existingUserAgent.includes('MAOTS/NO-OP')) {
52+
const featureSpecificUserAgent = existingUserAgent.replace(
53+
'MAOTS/NO-OP',
54+
`MAOTS/${feature}/${MAOTS_VERSION} MAOTSEnv/${EXEC_ENV}`
55+
);
56+
57+
args.request.headers['user-agent'] = featureSpecificUserAgent;
58+
return await next(args);
59+
}
60+
if (existingUserAgent.includes('MAOTS/')) {
61+
return await next(args);
62+
}
63+
args.request.headers['user-agent'] =
64+
existingUserAgent === ''
65+
? `MAOTS/${feature}/${MAOTS_VERSION} MAOTSEnv/${EXEC_ENV}`
66+
: `${existingUserAgent} MAOTS/${feature}/${MAOTS_VERSION} MAOTSEnv/${EXEC_ENV}`;
67+
68+
return await next(args);
69+
};
70+
};
71+
72+
/**
73+
* Check if the provided middleware stack already has the Mao for AWS Lambda
74+
* user agent middleware.
75+
*
76+
* @param middlewareStack The middleware stack to check
77+
*
78+
* @internal
79+
*/
80+
const hasMao = (middlewareStack: string[]): boolean => {
81+
let found = false;
82+
for (const middleware of middlewareStack) {
83+
if (middleware.includes('addMaoToUserAgent')) {
84+
found = true;
85+
}
86+
}
87+
88+
return found;
89+
};
90+
91+
/**
92+
* Add the MAo for AWS Lambda user agent middleware to the
93+
* AWS SDK v3 client provided.
94+
*
95+
* We use this middleware to unbotrusively track the usage of the library
96+
* and secure continued investment in the project.
97+
*
98+
* @param client The AWS SDK v3 client to add the middleware to
99+
* @param feature The feature name to be added to the user agent
100+
*/
101+
const addUserAgentMiddleware = (client: unknown, feature: string): void => {
102+
try {
103+
if (isSdkClient(client)) {
104+
if (hasMao(client.middlewareStack.identify())) {
105+
return;
106+
}
107+
client.middlewareStack.addRelativeTo(
108+
customUserAgentMiddleware(feature),
109+
middlewareOptions
110+
);
111+
} else {
112+
throw new Error(
113+
'The client provided does not match the expected interface'
114+
);
115+
}
116+
} catch (error) {
117+
console.warn('Failed to add user agent middleware', error);
118+
}
119+
};
120+
121+
export { customUserAgentMiddleware, addUserAgentMiddleware, isSdkClient };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Minimal interface for an AWS SDK v3 client.
3+
*
4+
* @internal
5+
*/
6+
interface SdkClient {
7+
send: (args: unknown) => Promise<unknown>;
8+
config: {
9+
serviceId: string;
10+
};
11+
middlewareStack: {
12+
identify: () => string[];
13+
addRelativeTo: (middleware: unknown, options: unknown) => void;
14+
};
15+
}
16+
17+
/**
18+
* Minimal type for the arguments passed to a middleware function
19+
*
20+
* @internal
21+
*/
22+
type MiddlewareArgsLike = { request: { headers: { [key: string]: string } } };
23+
24+
export type { SdkClient, MiddlewareArgsLike };
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// this file is auto generated, do not modify
2+
export const MAOTS_VERSION = '0.1.5';

0 commit comments

Comments
 (0)