adk-typescript
Version:
TypeScript port of Google's Agent Development Kit (ADK)
335 lines (334 loc) • 12.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RestApiTool = void 0;
exports.snakeToLowerCamel = snakeToLowerCamel;
exports.normalizeJsonSchemaType = normalizeJsonSchemaType;
exports.toGeminiSchema = toGeminiSchema;
const axios_1 = __importDefault(require("axios"));
const BaseTool_1 = require("../../BaseTool");
const common_1 = require("../common/common");
/**
* Converts a snake_case string to a lowerCamelCase string
* @param snakeCaseString The input snake_case string
* @returns The lowerCamelCase string
*/
function snakeToLowerCamel(snakeCaseString) {
if (!snakeCaseString.includes('_')) {
return snakeCaseString;
}
return snakeCaseString.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
}
/**
* Normalizes a JSON Schema type into a Gemini Schema type and checks if it's nullable
* @param jsonSchemaType The JSON Schema type (string or array of strings)
* @returns A tuple of [normalizedType, isNullable]
*/
function normalizeJsonSchemaType(jsonSchemaType) {
if (!jsonSchemaType) {
return [null, false];
}
if (typeof jsonSchemaType === 'string') {
if (jsonSchemaType === 'null') {
return [null, true];
}
return [jsonSchemaType, false];
}
// Handle array of types
const nonNullTypes = [];
let nullable = false;
// If JSON schema type is an array, pick the first non null type
for (const typeValue of jsonSchemaType) {
if (typeValue === 'null') {
nullable = true;
}
else {
nonNullTypes.push(typeValue);
}
}
const nonNullType = nonNullTypes.length > 0 ? nonNullTypes[0] : null;
return [nonNullType, nullable];
}
/**
* Converts an OpenAPI schema to a Gemini Schema
* @param openApiSchema The OpenAPI schema object
* @returns A Gemini Schema object
*/
function toGeminiSchema(openApiSchema) {
if (!openApiSchema) {
return null;
}
if (typeof openApiSchema !== 'object') {
throw new TypeError('openApiSchema must be an object');
}
const geminiSchema = {};
// Force adding a type to an empty dict to avoid validation errors
if (!openApiSchema.type) {
openApiSchema.type = 'object';
}
// Convert OpenAPI schema fields to Gemini schema fields
for (const [key, value] of Object.entries(openApiSchema)) {
// Convert key from camelCase to snake_case
const snakeCaseKey = (0, common_1.toSnakeCase)(key);
// Skip certain fields that Gemini doesn't recognize
if (['title', 'default', 'format'].includes(snakeCaseKey)) {
continue;
}
if (snakeCaseKey === 'type') {
const [schemaType, nullable] = normalizeJsonSchemaType(openApiSchema.type);
// Adding this to force adding a type to an empty dict
// This avoids "... one_of or any_of must specify a type" error
geminiSchema['type'] = schemaType ? schemaType.toUpperCase() : 'OBJECT';
if (nullable) {
geminiSchema['nullable'] = true;
}
}
else if (snakeCaseKey === 'properties' && typeof value === 'object') {
geminiSchema[snakeCaseKey] = Object.entries(value).reduce((acc, [propKey, propValue]) => {
acc[propKey] = toGeminiSchema(propValue);
return acc;
}, {});
}
else if (snakeCaseKey === 'items' && typeof value === 'object') {
geminiSchema[snakeCaseKey] = toGeminiSchema(value);
}
else if (snakeCaseKey === 'any_of' && Array.isArray(value)) {
geminiSchema[snakeCaseKey] = value.map(item => toGeminiSchema(item));
}
else if (Array.isArray(value) && !['enum', 'required', 'property_ordering'].includes(snakeCaseKey)) {
// Handle arrays that need recursive processing
geminiSchema[snakeCaseKey] = value.map(item => typeof item === 'object' ? toGeminiSchema(item) : item);
}
else if (typeof value === 'object' && value !== null && snakeCaseKey !== 'properties') {
// Handle nested objects
geminiSchema[snakeCaseKey] = toGeminiSchema(value);
}
else {
// Simple value assignment
geminiSchema[snakeCaseKey] = value;
}
}
return geminiSchema;
}
/**
* A generic tool that interacts with a REST API
*/
class RestApiTool extends BaseTool_1.BaseTool {
/**
* Create a new RestApiTool
* @param options Options for the REST API tool
*/
constructor(options) {
super({
name: options.name.substring(0, 60), // Gemini restricts function names to 64 characters
description: options.description,
isLongRunning: false
});
/**
* Parameters for the API call
*/
this.parameters = [];
this.name = options.name.substring(0, 60);
this.description = options.description;
// Parse endpoint if it's a string
this.endpoint = typeof options.endpoint === 'string'
? JSON.parse(options.endpoint)
: options.endpoint;
// Parse operation if it's a string
this.operation = typeof options.operation === 'string'
? JSON.parse(options.operation)
: options.operation;
this.authScheme = options.authScheme;
this.authCredential = options.authCredential;
if (options.shouldParseOperation !== false) {
this._parseOperation();
}
}
/**
* Create a RestApiTool from a ParsedOperation
* @param parsed The parsed operation
* @returns A new RestApiTool
*/
static fromParsedOperation(parsed) {
const tool = new RestApiTool({
name: parsed.name,
description: parsed.description,
endpoint: parsed.endpoint,
operation: parsed.operation,
authScheme: parsed.authScheme,
authCredential: parsed.authCredential,
shouldParseOperation: false
});
tool.parameters = parsed.parameters;
tool.returnValue = parsed.returnValue;
return tool;
}
/**
* Parse the operation to extract parameters and return value
*/
_parseOperation() {
// This would be implemented using OperationParser in a full implementation
// For now, using a placeholder implementation
console.warn('Operation parsing not fully implemented');
// Simple parameter extraction from operation parameters
const parameters = this.operation.parameters || [];
for (const param of parameters) {
this.parameters.push(new common_1.ApiParameter(param.name, param.in || '', param.schema || {}, param.description || ''));
}
// Simple return value extraction from first 2xx response
const responses = this.operation.responses || {};
const successCodes = Object.keys(responses).filter(code => code.startsWith('2'));
if (successCodes.length > 0) {
const response = responses[successCodes[0]];
if (response && response.content) {
const contentType = Object.keys(response.content)[0];
if (contentType && response.content[contentType].schema) {
this.returnValue = new common_1.ApiParameter('', '', response.content[contentType].schema, response.description || '');
}
}
}
if (!this.returnValue) {
// Default return value
this.returnValue = new common_1.ApiParameter('', '', { type: 'object' });
}
}
/**
* Get the function declaration for the LLM
* @returns The function declaration
*/
_getDeclaration() {
// Build parameters object
const properties = {};
const required = [];
for (const param of this.parameters) {
properties[param.pyName] = toGeminiSchema(param.paramSchema);
// Mark non-optional parameters as required
// There are two ways a parameter can be marked as required in OpenAPI:
// 1. A boolean 'required' property set to true
// 2. The parameter name is in the parent schema's 'required' array
const hasRequiredBoolean = typeof param.paramSchema.required === 'boolean' && param.paramSchema.required;
if (hasRequiredBoolean) {
required.push(param.pyName);
}
}
return {
name: this.name,
description: this.description,
parameters: {
type: 'object',
properties,
required
}
};
}
/**
* Configure the authentication scheme
* @param authScheme The authentication scheme
*/
configureAuthScheme(authScheme) {
this.authScheme = authScheme;
}
/**
* Configure the authentication credential
* @param authCredential The authentication credential
*/
configureAuthCredential(authCredential) {
this.authCredential = authCredential;
}
/**
* Prepare request parameters for the API call
* @param params The API parameters
* @param args The function arguments
* @returns The prepared request parameters
*/
_prepareRequestParams(params, args) {
const requestParams = {
headers: {},
params: {}, // Query parameters
data: {} // Body parameters
};
for (const param of params) {
const paramName = param.pyName;
if (!(paramName in args)) {
continue;
}
const paramValue = args[paramName];
if (param.paramLocation === 'query') {
requestParams.params[param.originalName] = paramValue;
}
else if (param.paramLocation === 'header') {
requestParams.headers[param.originalName] = paramValue;
}
else if (param.paramLocation === 'path') {
// Path parameters will be handled when building the URL
}
else if (param.paramLocation === 'body') {
if (param.originalName) {
// Named body parameter
requestParams.data[param.originalName] = paramValue;
}
else {
// If it's an unnamed body parameter, use the value directly as the body
requestParams.data = paramValue;
}
}
}
return requestParams;
}
/**
* Execute the tool
* @param args The function arguments
* @param context The tool context
* @returns The API response
*/
async execute(args, context) {
// Authentication would be handled here in a full implementation
// Prepare request parameters
const requestParams = this._prepareRequestParams(this.parameters, args);
// Build URL
let url = this.endpoint.baseUrl + this.endpoint.path;
// Replace path parameters
const pathParams = this.parameters.filter(p => p.paramLocation === 'path');
for (const param of pathParams) {
if (param.pyName in args) {
url = url.replace(`{${param.originalName}}`, encodeURIComponent(args[param.pyName]));
}
}
// Prepare request config
const config = {
url,
method: this.endpoint.method,
headers: requestParams.headers,
params: requestParams.params,
data: Object.keys(requestParams.data).length > 0 ? requestParams.data : undefined
};
// Execute request
try {
const response = await (0, axios_1.default)(config);
return response.data;
}
catch (error) {
if (error.response) {
return {
error: true,
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data
};
}
return {
error: true,
message: error.message
};
}
}
/**
* String representation of the tool
*/
toString() {
return `RestApiTool(${this.name})`;
}
}
exports.RestApiTool = RestApiTool;