From ebd043bf347a004a488a406705a8eeafb587df1e Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Mon, 9 Dec 2024 17:40:40 -0500 Subject: [PATCH] [FEATURE] Google Gemini - Add support for structure JSON output --- .../actions/common/generate-content.mjs | 92 +++++++++++++++++++ .../generate-content-from-text-and-image.mjs | 48 +++++++--- .../generate-content-from-text.mjs | 42 +++++++-- components/google_gemini/common/constants.mjs | 7 +- components/google_gemini/common/utils.mjs | 24 +++++ .../google_gemini/google_gemini.app.mjs | 41 +++++++-- components/google_gemini/package.json | 4 +- pnpm-lock.yaml | 12 +-- 8 files changed, 230 insertions(+), 40 deletions(-) create mode 100644 components/google_gemini/actions/common/generate-content.mjs create mode 100644 components/google_gemini/common/utils.mjs diff --git a/components/google_gemini/actions/common/generate-content.mjs b/components/google_gemini/actions/common/generate-content.mjs new file mode 100644 index 0000000000000..3417d27d239c2 --- /dev/null +++ b/components/google_gemini/actions/common/generate-content.mjs @@ -0,0 +1,92 @@ +import app from "../../google_gemini.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + model: { + propDefinition: [ + app, + "model", + () => ({ + filter: ({ + description, + supportedGenerationMethods, + }) => ![ + "discontinued", + "deprecated", + ].some((keyword) => description.includes(keyword)) + && supportedGenerationMethods?.includes(constants.MODEL_METHODS.GENERATE_CONTENT), + }), + ], + }, + }, + async additionalProps() { + const { + model, + responseFormat, + } = this; + + const { + outputTokenLimit, + temperature, + topP, + topK, + maxTemperature, + } = await this.app.getModel({ + model, + }); + + return { + ...(responseFormat && { + responseSchema: { + type: "string", + label: "Response Schema", + description: "Define the structure of the JSON response. Must be a valid JSON schema object. Leave empty to let Gemini determine the structure.", + optional: true, + }, + }), + ...(outputTokenLimit && { + maxOutputTokens: { + type: "integer", + label: "Max Output Tokens", + description: `The maximum number of tokens to generate in the response. Eg. \`${outputTokenLimit}\`.`, + optional: true, + max: outputTokenLimit, + }, + }), + ...(temperature && { + temperature: { + type: "string", + label: "Temperature", + description: `Controls the randomness of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${temperature}\`.${maxTemperature + ? ` Where max temperature is \`${maxTemperature}\`.` + : ""}`, + optional: true, + }, + }), + ...(topP && { + topP: { + type: "string", + label: "Top P", + description: `Controls the diversity of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${topP}\`.`, + optional: true, + }, + }), + ...(topK && { + topK: { + type: "integer", + label: "Top K", + description: `Controls the diversity of the generated text. Lower values make the text more deterministic, while higher values make it more random. Eg. \`${topK}\`.`, + optional: true, + }, + }), + stopSequences: { + type: "string[]", + label: "Stop Sequences", + description: "The set of character sequences (up to 5) that will stop output generation. If specified, the API will stop at the first appearance of a `stop_sequence`. The stop sequence will not be included as part of the response.", + optional: true, + }, + }; + }, +}; diff --git a/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs b/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs index 57361b45b3dbd..e3faed0ad6bde 100644 --- a/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs +++ b/components/google_gemini/actions/generate-content-from-text-and-image/generate-content-from-text-and-image.mjs @@ -1,39 +1,41 @@ import fs from "fs"; import { ConfigurationError } from "@pipedream/platform"; -import app from "../../google_gemini.app.mjs"; +import common from "../common/generate-content.mjs"; +import utils from "../../common/utils.mjs"; export default { + ...common, key: "google_gemini-generate-content-from-text-and-image", name: "Generate Content from Text and Image", description: "Generates content from both text and image input using the Gemini API. [See the documentation](https://ai.google.dev/tutorials/rest_quickstart#text-and-image_input)", - version: "0.1.0", + version: "0.1.1", type: "action", props: { - app, - model: { - propDefinition: [ - app, - "model", - ], - }, + ...common.props, text: { propDefinition: [ - app, + common.props.app, "text", ], }, mimeType: { propDefinition: [ - app, + common.props.app, "mimeType", ], }, imagePaths: { propDefinition: [ - app, + common.props.app, "imagePaths", ], }, + responseFormat: { + propDefinition: [ + common.props.app, + "responseFormat", + ], + }, }, methods: { fileToGenerativePart(path, mimeType) { @@ -55,6 +57,13 @@ export default { text, imagePaths, mimeType, + responseFormat, + responseSchema, + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, } = this; if (!Array.isArray(imagePaths)) { @@ -79,6 +88,21 @@ export default { ], }, ], + ...( + responseFormat || maxOutputTokens || temperature || topP || topK || stopSequences?.length + ? { + generationConfig: { + responseMimeType: "application/json", + responseSchema: utils.parse(responseSchema), + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, + }, + } + : {} + ), }, }); diff --git a/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs b/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs index c24a1f89aacbd..5a0c3da386a30 100644 --- a/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs +++ b/components/google_gemini/actions/generate-content-from-text/generate-content-from-text.mjs @@ -1,23 +1,25 @@ -import app from "../../google_gemini.app.mjs"; +import common from "../common/generate-content.mjs"; +import utils from "../../common/utils.mjs"; export default { + ...common, key: "google_gemini-generate-content-from-text", name: "Generate Content from Text", description: "Generates content from text input using the Google Gemini API. [See the documentation](https://ai.google.dev/tutorials/rest_quickstart#text-only_input)", - version: "0.1.0", + version: "0.1.1", type: "action", props: { - app, - model: { + ...common.props, + text: { propDefinition: [ - app, - "model", + common.props.app, + "text", ], }, - text: { + responseFormat: { propDefinition: [ - app, - "text", + common.props.app, + "responseFormat", ], }, }, @@ -26,6 +28,13 @@ export default { app, model, text, + responseFormat, + responseSchema, + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, } = this; const response = await app.generateContent({ @@ -41,6 +50,21 @@ export default { ], }, ], + ...( + responseFormat || maxOutputTokens || temperature || topP || topK || stopSequences?.length + ? { + generationConfig: { + responseMimeType: "application/json", + responseSchema: utils.parse(responseSchema), + maxOutputTokens, + temperature, + topP, + topK, + stopSequences, + }, + } + : {} + ), }, }); diff --git a/components/google_gemini/common/constants.mjs b/components/google_gemini/common/constants.mjs index 20ac1c2facc8c..e673d65556807 100644 --- a/components/google_gemini/common/constants.mjs +++ b/components/google_gemini/common/constants.mjs @@ -1,7 +1,12 @@ const BASE_URL = "https://generativelanguage.googleapis.com"; -const VERSION_PATH = "/v1"; +const VERSION_PATH = "/v1beta"; + +const MODEL_METHODS = { + GENERATE_CONTENT: "generateContent", +}; export default { BASE_URL, VERSION_PATH, + MODEL_METHODS, }; diff --git a/components/google_gemini/common/utils.mjs b/components/google_gemini/common/utils.mjs new file mode 100644 index 0000000000000..899f34cfaf092 --- /dev/null +++ b/components/google_gemini/common/utils.mjs @@ -0,0 +1,24 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function emptyStrToUndefined(value) { + const trimmed = typeof(value) === "string" && value.trim(); + return trimmed === "" + ? undefined + : value; +} + +function parse(value) { + const valueToParse = emptyStrToUndefined(value); + if (typeof(valueToParse) === "object" || valueToParse === undefined) { + return valueToParse; + } + try { + return JSON.parse(valueToParse); + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid object"); + } +} + +export default { + parse, +}; diff --git a/components/google_gemini/google_gemini.app.mjs b/components/google_gemini/google_gemini.app.mjs index 2f4d6ef0cbe16..98132ef3abf63 100644 --- a/components/google_gemini/google_gemini.app.mjs +++ b/components/google_gemini/google_gemini.app.mjs @@ -34,7 +34,11 @@ export default { type: "string", label: "Model", description: "The model to use for content generation", - async options({ prevContext: { pageToken } }) { + reloadProps: true, + async options({ + prevContext: { pageToken }, + filter = (model) => model, + }) { if (pageToken === null) { return []; } @@ -46,13 +50,16 @@ export default { pageToken, }, }); - const options = models.map(({ - name: value, - displayName: label, - }) => ({ - label, - value, - })); + + const options = models + .filter(filter) + .map(({ + name: value, + displayName: label, + }) => ({ + label, + value, + })); return { options, @@ -62,6 +69,14 @@ export default { }; }, }, + responseFormat: { + type: "boolean", + label: "JSON Output", + description: "Enable to receive responses in structured JSON format instead of plain text. Useful for automated processing, data extraction, or when you need to parse the response programmatically. You can optionally define a specific schema for the response structure.", + optional: true, + default: false, + reloadProps: true, + }, }, methods: { getUrl(path) { @@ -99,7 +114,7 @@ export default { ? model : `models/${model}`; return this.post({ - path: `/${pathPrefix}:generateContent`, + path: `/${pathPrefix}:${constants.MODEL_METHODS.GENERATE_CONTENT}`, ...args, }); }, @@ -109,5 +124,13 @@ export default { ...args, }); }, + getModel({ + model, ...args + } = {}) { + return this.makeRequest({ + path: `/${model}`, + ...args, + }); + }, }, }; diff --git a/components/google_gemini/package.json b/components/google_gemini/package.json index 3a8ee0b448837..d7b0ef2e3b5f1 100644 --- a/components/google_gemini/package.json +++ b/components/google_gemini/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_gemini", - "version": "0.2.0", + "version": "0.2.1", "description": "Pipedream Google Gemini Components", "main": "google_gemini.app.mjs", "keywords": [ @@ -13,6 +13,6 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.0.0" + "@pipedream/platform": "^3.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62738145368dd..d5bc77c4f03af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4260,7 +4260,7 @@ importers: components/google_gemini: dependencies: '@pipedream/platform': - specifier: ^3.0.0 + specifier: ^3.0.3 version: 3.0.3 components/google_maps_platform: {} @@ -24474,22 +24474,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} @@ -30647,8 +30647,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: