diff --git a/libs/providers/langchain-google-common/src/chat_models.ts b/libs/providers/langchain-google-common/src/chat_models.ts index 61f6bd2049e0..282f074918dc 100644 --- a/libs/providers/langchain-google-common/src/chat_models.ts +++ b/libs/providers/langchain-google-common/src/chat_models.ts @@ -65,6 +65,18 @@ import { schemaToGeminiParameters, } from "./utils/zod_to_gemini_parameters.js"; +// --------------------------------------------------------------------------- +// Added helper: infer Gemini propertyOrdering from schema if absent +// --------------------------------------------------------------------------- + +function withPropertyOrdering>(schema: T): T { + const out = { ...schema } as T & { propertyOrdering?: string[] }; + const props = (out as any)?.parameters?.properties ?? out.properties; + if (!out.propertyOrdering && props && typeof props === "object") { + out.propertyOrdering = Object.keys(props); + } + return out; +} export class ChatConnection extends AbstractGoogleLLMConnection< BaseMessage[], AuthOptions @@ -89,9 +101,6 @@ export class ChatConnection extends AbstractGoogleLLMConnection< } get computeUseSystemInstruction(): boolean { - // This works on models from April 2024 and later - // Vertex AI: gemini-1.5-pro and gemini-1.0-002 and later - // AI Studio: gemini-1.5-pro-latest if (this.modelFamily === "palm") { return false; } else if (this.modelName === "gemini-1.0-pro-001") { @@ -101,11 +110,8 @@ export class ChatConnection extends AbstractGoogleLLMConnection< } else if (this.modelName.startsWith("gemini-1.0-pro-vision")) { return false; } else if (this.modelName === "gemini-pro" && this.platform === "gai") { - // on AI Studio gemini-pro is still pointing at gemini-1.0-pro-001 return false; } else if (this.modelFamily === "gemma") { - // At least as of 12 Mar 2025 gemma 3 on AIS, trying to use system instructions yields an error: - // "Developer instruction is not enabled for models/gemma-3-27b-it" return false; } return true; @@ -158,9 +164,6 @@ export class ChatConnection extends AbstractGoogleLLMConnection< } } -/** - * Input to chat model class. - */ export interface ChatGoogleBaseInput extends BaseChatModelParams, GoogleConnectionParams, @@ -169,14 +172,10 @@ export interface ChatGoogleBaseInput GoogleAIAPIParams, Pick {} -/** - * Integration with a Google chat model. - */ export abstract class ChatGoogleBase extends BaseChatModel implements ChatGoogleBaseInput { - // Used for tracing, replace with the same name as your class static lc_name() { return "ChatGoogle"; } @@ -188,58 +187,32 @@ export abstract class ChatGoogleBase } lc_serializable = true; - - // Set based on modelName model: string; - modelName = "gemini-pro"; - temperature: number; - maxOutputTokens: number; - maxReasoningTokens: number; - topP: number; - topK: number; - seed: number; - presencePenalty: number; - frequencyPenalty: number; - stopSequences: string[] = []; - logprobs: boolean; - topLogprobs: number = 0; - safetySettings: GoogleAISafetySetting[] = []; - responseModalities?: GoogleAIModelModality[]; - - // May intentionally be undefined, meaning to compute this. convertSystemMessageToHumanContent: boolean | undefined; - safetyHandler: GoogleAISafetyHandler; - speechConfig: GoogleSpeechConfig; - streamUsage = true; - streaming = false; - labels?: Record; - protected connection: ChatConnection; - protected streamedConnection: ChatConnection; constructor(fields?: ChatGoogleBaseInput) { super(ensureParams(fields)); - copyAndValidateModelParamsInto(fields, this); this.safetyHandler = fields?.safetyHandler ?? new DefaultGeminiSafetyHandler(); @@ -317,14 +290,10 @@ export abstract class ChatGoogleBase return this.withConfig({ tools: convertToGeminiTools(tools), ...kwargs }); } - // Replace _llmType() { return "chat_integration"; } - /** - * Get the parameters used to invoke the model - */ override invocationParams(options?: this["ParsedCallOptions"]) { return copyAIModelParams(this, options); } @@ -341,12 +310,8 @@ export abstract class ChatGoogleBase for await (const chunk of stream) { finalChunk = !finalChunk ? chunk : concat(finalChunk, chunk); } - if (!finalChunk) { - throw new Error("No chunks were returned from the stream."); - } - return { - generations: [finalChunk], - }; + if (!finalChunk) throw new Error("No chunks were returned from the stream."); + return { generations: [finalChunk] }; } const response = await this.connection.request( @@ -357,9 +322,7 @@ export abstract class ChatGoogleBase ); const ret = this.connection.api.responseToChatResult(response); const chunk = ret?.generations?.[0]; - if (chunk) { - await runManager?.handleLLMNewToken(chunk.text || ""); - } + if (chunk) await runManager?.handleLLMNewToken(chunk.text || ""); return ret; } @@ -368,7 +331,6 @@ export abstract class ChatGoogleBase options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): AsyncGenerator { - // Make the call as a streaming request const parameters = this.invocationParams(options); const response = await this.streamedConnection.request( _messages, @@ -376,20 +338,13 @@ export abstract class ChatGoogleBase options, runManager ); - - // Get the streaming parser of the response const stream = response.data as JsonStream; let usageMetadata: UsageMetadata | undefined; - // Loop until the end of the stream - // During the loop, yield each time we get a chunk from the streaming parser - // that is either available or added to the queue while (!stream.streamDone) { const output = await stream.nextChunk(); await runManager?.handleCustomEvent( `google-chunk-${this.constructor.name}`, - { - output, - } + { output } ); if ( output && @@ -416,63 +371,31 @@ export abstract class ChatGoogleBase }); if (chunk) { yield chunk; - await runManager?.handleLLMNewToken( - chunk.text ?? "", - undefined, - undefined, - undefined, - undefined, - { chunk } - ); + await runManager?.handleLLMNewToken(chunk.text ?? "", undefined, undefined, undefined, undefined, { chunk }); } } } - /** @ignore */ _combineLLMOutput() { return []; } - withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - RunOutput extends Record = Record - >( - outputSchema: - | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Record, - config?: StructuredOutputMethodOptions - ): Runnable; + // --------------------------------------------------------------------------- + // Patched withStructuredOutput (adds Gemini propertyOrdering inference) + // --------------------------------------------------------------------------- withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any RunOutput extends Record = Record >( outputSchema: | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Record, - config?: StructuredOutputMethodOptions - ): Runnable; - - withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - RunOutput extends Record = Record - >( - outputSchema: - | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any | Record, config?: StructuredOutputMethodOptions ): | Runnable - | Runnable< - BaseLanguageModelInput, - { raw: BaseMessage; parsed: RunOutput } - > { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const schema: InteropZodType | Record = - outputSchema; + | Runnable + { + const schema: InteropZodType | Record = outputSchema; const name = config?.name; const method = config?.method; const includeRaw = config?.includeRaw; @@ -483,16 +406,18 @@ export abstract class ChatGoogleBase let functionName = name ?? "extract"; let outputParser: BaseLLMOutputParser; let tools: GeminiTool[]; + if (isInteropZodSchema(schema)) { const jsonSchema = schemaToGeminiParameters(schema); + const jsonSchemaWithOrdering = withPropertyOrdering(jsonSchema); tools = [ { functionDeclarations: [ { name: functionName, description: - jsonSchema.description ?? "A function available to call.", - parameters: jsonSchema as GeminiFunctionSchema, + jsonSchemaWithOrdering.description ?? "A function available to call.", + parameters: jsonSchemaWithOrdering as GeminiFunctionSchema, }, ], }, @@ -512,24 +437,21 @@ export abstract class ChatGoogleBase geminiFunctionDefinition = schema as GeminiFunctionDeclaration; functionName = schema.name; } else { - // We are providing the schema for *just* the parameters, probably - const parameters: GeminiJsonSchema = removeAdditionalProperties(schema); + let parameters: GeminiJsonSchema = removeAdditionalProperties(schema); + parameters = withPropertyOrdering(parameters); geminiFunctionDefinition = { name: functionName, description: schema.description ?? "", parameters, }; } - tools = [ - { - functionDeclarations: [geminiFunctionDefinition], - }, - ]; + tools = [{ functionDeclarations: [geminiFunctionDefinition] }]; outputParser = new JsonOutputKeyToolsParser({ returnSingle: true, keyName: functionName, }); } + const llm = this.bindTools(tools).withConfig({ tool_choice: functionName }); if (!includeRaw) { @@ -539,25 +461,16 @@ export abstract class ChatGoogleBase } const parserAssign = RunnablePassthrough.assign({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any parsed: (input: any, config) => outputParser.invoke(input.raw, config), }); - const parserNone = RunnablePassthrough.assign({ - parsed: () => null, - }); - const parsedWithFallback = parserAssign.withFallbacks({ - fallbacks: [parserNone], - }); + const parserNone = RunnablePassthrough.assign({ parsed: () => null }); + const parsedWithFallback = parserAssign.withFallbacks({ fallbacks: [parserNone] }); return RunnableSequence.from< BaseLanguageModelInput, { raw: BaseMessage; parsed: RunOutput } - >([ - { - raw: llm, - }, - parsedWithFallback, - ]).withConfig({ + >([{ raw: llm }, parsedWithFallback]).withConfig({ runName: "StructuredOutputRunnable", }); } } +export default ChatGoogleBase; \ No newline at end of file