test("Test Azure ChatOpenAI in streaming mode with n > 1 and multiple prompts", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    let nrNewTokens = 0;
    const streamedCompletions = [
      ["", ""],
      ["", ""],
    ];

    const model = new AzureChatOpenAI({
      modelName: "gpt-3.5-turbo",
      streaming: true,
      maxTokens: 10,
      n: 2,
      callbacks: [
        {
          async handleLLMNewToken(token: string, idx: NewTokenIndices) {
            nrNewTokens += 1;
            streamedCompletions[idx.prompt][idx.completion] += token;
          },
        },
      ],
    });
    const message1 = new HumanMessage("Hello!");
    const message2 = new HumanMessage("Bye!");
    const result = await model.generate([[message1], [message2]]);

    expect(nrNewTokens > 0).toBe(true);
    expect(result.generations.map((g) => g.map((gg) => gg.text))).toEqual(
      streamedCompletions
    );
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
}, 10000);

test("Test Azure ChatOpenAI prompt value", async () => {
  const chat = new AzureChatOpenAI({
    modelName: "gpt-3.5-turbo",
    maxTokens: 10,
    n: 2,
  });
  const message = new HumanMessage("Hello!");
  const res = await chat.generatePrompt([new ChatPromptValue([message])]);
  expect(res.generations.length).toBe(1);
  for (const generation of res.generations) {
    expect(generation.length).toBe(2);
    // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
    // @ts-expect-error unused var
    for (const g of generation) {
      // console.log(g.text);
    }
  }
  // console.log({ res });
});

test("Test Azure OpenAI Chat, docs, prompt templates", async () => {
  const chat = new AzureChatOpenAI({ temperature: 0, maxTokens: 10 });

  const systemPrompt = PromptTemplate.fromTemplate(
    "You are a helpful assistant that translates {input_language} to {output_language}."
  );

  const chatPrompt = ChatPromptTemplate.fromMessages([
    new SystemMessagePromptTemplate(systemPrompt),
    HumanMessagePromptTemplate.fromTemplate("{text}"),
  ]);

  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  const responseA = await chat.generatePrompt([
    await chatPrompt.formatPromptValue({
      input_language: "English",
      output_language: "French",
      text: "I love programming.",
    }),
  ]);

  // console.log(responseA.generations);
}, 5000);

test("Test Azure ChatOpenAI with stop", async () => {
  const model = new AzureChatOpenAI({ maxTokens: 5 });
  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  const res = await model.call(
    [new HumanMessage("Print hello world")],
    ["world"]
  );
  // console.log({ res });
});

test("Test Azure ChatOpenAI with stop in object", async () => {
  const model = new AzureChatOpenAI({ maxTokens: 5 });
  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  const res = await model.invoke([new HumanMessage("Print hello world")], {
    stop: ["world"],
  });
  // console.log({ res });
});

test("Test Azure ChatOpenAI with timeout in call options", async () => {
  const model = new AzureChatOpenAI({ maxTokens: 5 });
  await expect(() =>
    model.invoke([new HumanMessage("Print hello world")], { timeout: 10 })
  ).rejects.toThrow();
}, 5000);

test("Test Azure ChatOpenAI with timeout in call options and node adapter", async () => {
  const model = new AzureChatOpenAI({ maxTokens: 5 });
  await expect(() =>
    model.invoke([new HumanMessage("Print hello world")], { timeout: 10 })
  ).rejects.toThrow();
}, 5000);

test("Test Azure ChatOpenAI with signal in call options", async () => {
  const model = new AzureChatOpenAI({ maxTokens: 5 });
  const controller = new AbortController();
  await expect(() => {
    const ret = model.invoke([new HumanMessage("Print hello world")], {
      signal: controller.signal,
    });

    controller.abort();

    return ret;
  }).rejects.toThrow();
}, 5000);

test("Test Azure ChatOpenAI with signal in call options and node adapter", async () => {
  const model = new AzureChatOpenAI({
    maxTokens: 5,
    modelName: "gpt-3.5-turbo-instruct",
  });
  const controller = new AbortController();
  await expect(() => {
    const ret = model.invoke([new HumanMessage("Print hello world")], {
      signal: controller.signal,
    });

    controller.abort();

    return ret;
  }).rejects.toThrow();
}, 5000);

test("Test Azure ChatOpenAI with specific roles in ChatMessage", async () => {
  const chat = new AzureChatOpenAI({
    modelName: "gpt-3.5-turbo",
    maxTokens: 10,
  });
  const system_message = new ChatMessage(
    "You are to chat with a user.",
    "system"
  );
  const user_message = new ChatMessage("Hello!", "user");
  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  const res = await chat.call([system_message, user_message]);
  // console.log({ res });
});

test("Test Azure ChatOpenAI stream method", async () => {
  const model = new AzureChatOpenAI({
    maxTokens: 50,
    modelName: "gpt-3.5-turbo",
  });
  const stream = await model.stream("Print hello world.");
  const chunks = [];
  for await (const chunk of stream) {
    // console.log(chunk);
    chunks.push(chunk);
  }
  expect(chunks.length).toBeGreaterThan(1);
});

test("Test Azure ChatOpenAI stream method with abort", async () => {
  await expect(async () => {
    const model = new AzureChatOpenAI({
      maxTokens: 100,
      modelName: "gpt-3.5-turbo",
    });
    const stream = await model.stream(
      "How is your day going? Be extremely verbose.",
      {
        signal: AbortSignal.timeout(500),
      }
    );
    // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
    // @ts-expect-error unused var
    for await (const chunk of stream) {
      // console.log(chunk);
    }
  }).rejects.toThrow();
});

test("Test Azure ChatOpenAI stream method with early break", async () => {
  const model = new AzureChatOpenAI({
    maxTokens: 50,
    modelName: "gpt-3.5-turbo",
  });
  const stream = await model.stream(
    "How is your day going? Be extremely verbose."
  );
  let i = 0;
  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  for await (const chunk of stream) {
    // console.log(chunk);
    i += 1;
    if (i > 10) {
      break;
    }
  }
});

test("Test Azure ChatOpenAI stream method, timeout error thrown from SDK", async () => {
  await expect(async () => {
    const model = new AzureChatOpenAI({
      maxTokens: 50,
      modelName: "gpt-3.5-turbo",
      timeout: 1,
      maxRetries: 0,
    });
    const stream = await model.stream(
      "How is your day going? Be extremely verbose."
    );
    // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
    // @ts-expect-error unused var
    for await (const chunk of stream) {
      // console.log(chunk);
    }
  }).rejects.toThrow();
});


test("Test Azure ChatOpenAI token usage reporting for streaming function calls", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    let streamingTokenUsed = -1;

    let nonStreamingTokenUsed = -1;

    const humanMessage = "What a beautiful day!";

    const extractionFunctionSchema = {
      name: "extractor",
      description: "Extracts fields from the input.",
      parameters: {
        type: "object",
        properties: {
          tone: {
            type: "string",
            enum: ["positive", "negative"],
            description: "The overall tone of the input",
          },
          word_count: {
            type: "number",
            description: "The number of words in the input",
          },
          chat_response: {
            type: "string",
            description: "A response to the human's input",
          },
        },
        required: ["tone", "word_count", "chat_response"],
      },
    };

    const streamingModel = new AzureChatOpenAI({
      modelName: "gpt-3.5-turbo",
      streaming: true,
      maxRetries: 10,
      maxConcurrency: 10,
      temperature: 0,
      topP: 0,
      callbacks: [
        {
          handleLLMEnd: async (output) => {
            streamingTokenUsed =
              output.llmOutput?.estimatedTokenUsage?.totalTokens;
            // console.log(
            //   "streaming usage",
            //   output.llmOutput?.estimatedTokenUsage
            // );
          },
          handleLLMError: async (_err) => {
            // console.error(err);
          },
        },
      ],
    }).bind({
      seed: 42,
      functions: [extractionFunctionSchema],
      function_call: { name: "extractor" },
    });

    const nonStreamingModel = new AzureChatOpenAI({
      modelName: "gpt-3.5-turbo",
      streaming: false,
      maxRetries: 10,
      maxConcurrency: 10,
      temperature: 0,
      topP: 0,
      callbacks: [
        {
          handleLLMEnd: async (output) => {
            nonStreamingTokenUsed = output.llmOutput?.tokenUsage?.totalTokens;
            // console.log("non-streaming usage", output.llmOutput?.tokenUsage);
          },
          handleLLMError: async (_err) => {
            // console.error(err);
          },
        },
      ],
    }).bind({
      functions: [extractionFunctionSchema],
      function_call: { name: "extractor" },
    });

    const [nonStreamingResult, streamingResult] = await Promise.all([
      nonStreamingModel.invoke([new HumanMessage(humanMessage)]),
      streamingModel.invoke([new HumanMessage(humanMessage)]),
    ]);

    if (
      nonStreamingResult.additional_kwargs.function_call?.arguments &&
      streamingResult.additional_kwargs.function_call?.arguments
    ) {
      // console.log(
      //   `Function Call: ${JSON.stringify(
      //     nonStreamingResult.additional_kwargs.function_call
      //   )}`
      // );
      const nonStreamingArguments = JSON.stringify(
        JSON.parse(nonStreamingResult.additional_kwargs.function_call.arguments)
      );
      const streamingArguments = JSON.stringify(
        JSON.parse(streamingResult.additional_kwargs.function_call.arguments)
      );
      if (nonStreamingArguments === streamingArguments) {
        expect(streamingTokenUsed).toEqual(nonStreamingTokenUsed);
      }
    }

    expect(streamingTokenUsed).toBeGreaterThan(-1);
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
});

test("Test Azure ChatOpenAI token usage reporting for streaming calls", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    let streamingTokenUsed = -1;

    let nonStreamingTokenUsed = -1;

    const systemPrompt = "You are a helpful assistant";

    const question = "What is the color of the night sky?";

    const streamingModel = new AzureChatOpenAI({
      modelName: "gpt-3.5-turbo",
      streaming: true,
      maxRetries: 10,
      maxConcurrency: 10,
      temperature: 0,
      topP: 0,
      callbacks: [
        {
          handleLLMEnd: async (output) => {
            streamingTokenUsed =
              output.llmOutput?.estimatedTokenUsage?.totalTokens;
            // console.log(
            //   "streaming usage",
            //   output.llmOutput?.estimatedTokenUsage
            // );
          },
          handleLLMError: async (_err) => {
            // console.error(err);
          },
        },
      ],
    });

    const nonStreamingModel = new AzureChatOpenAI({
      modelName: "gpt-3.5-turbo",
      streaming: false,
      maxRetries: 10,
      maxConcurrency: 10,
      temperature: 0,
      topP: 0,
      callbacks: [
        {
          handleLLMEnd: async (output) => {
            nonStreamingTokenUsed = output.llmOutput?.tokenUsage?.totalTokens;
            // console.log("non-streaming usage", output.llmOutput?.estimated);
          },
          handleLLMError: async (_err) => {
            // console.error(err);
          },
        },
      ],
    });

    const [nonStreamingResult, streamingResult] = await Promise.all([
      nonStreamingModel.generate([
        [new SystemMessage(systemPrompt), new HumanMessage(question)],
      ]),
      streamingModel.generate([
        [new SystemMessage(systemPrompt), new HumanMessage(question)],
      ]),
    ]);

    expect(streamingTokenUsed).toBeGreaterThan(-1);
    if (
      nonStreamingResult.generations[0][0].text ===
      streamingResult.generations[0][0].text
    ) {
      expect(streamingTokenUsed).toEqual(nonStreamingTokenUsed);
    }
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
});

// This test should be skipped if the required environment variables are not set
// instead of failing the test.
const tenantId: string = getEnvironmentVariable("AZURE_TENANT_ID") ?? "";
const clientId: string = getEnvironmentVariable("AZURE_CLIENT_ID") ?? "";
const clientSecret: string =
  getEnvironmentVariable("AZURE_CLIENT_SECRET") ?? "";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let testFn: any = test;
if (!tenantId || !clientId || !clientSecret) {
  // console.warn(`One or more required environment variables are not set.
  // Skipping "Test Azure ChatOpenAI with bearer token provider".`);
  testFn = test.skip;
}

testFn("Test Azure ChatOpenAI with bearer token provider", async () => {
  const credentials = new ClientSecretCredential(
    tenantId,
    clientId,
    clientSecret
  );
  const azureADTokenProvider = getBearerTokenProvider(
    credentials,
    "https://cognitiveservices.azure.com/.default"
  );

  const chat = new AzureChatOpenAI({
    modelName: "gpt-3.5-turbo",
    maxTokens: 5,
    azureADTokenProvider,
  });
  const message = new HumanMessage("Hello!");
  // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
  // @ts-expect-error unused var
  const res = await chat.invoke([["system", "Say hi"], message]);
  // console.log(res);
});


export class PineconeStore extends VectorStore {
  declare FilterType: PineconeMetadata;

  textKey: string;

  namespace?: string;

  pineconeIndex: PineconeIndex;

  filter?: PineconeMetadata;

  caller: AsyncCaller;

  _vectorstoreType(): string {
    return "pinecone";
  }

  constructor(embeddings: EmbeddingsInterface, params: PineconeStoreParams) {
    super(embeddings, params);
    this.embeddings = embeddings;

    const {
      namespace,
      pineconeIndex,
      textKey,
      filter,
      pineconeConfig,
      ...asyncCallerArgs
    } = params;
    this.namespace = namespace;
    if (!pineconeIndex && !pineconeConfig) {
      throw new Error("pineconeConfig or pineconeIndex must be provided.");
    }
    if (pineconeIndex && pineconeConfig) {
      throw new Error(
        "Only one of pineconeConfig or pineconeIndex can be provided."
      );
    }

    if (pineconeIndex) {
      this.pineconeIndex = pineconeIndex;
    } else if (pineconeConfig) {
      this.pineconeIndex = new PineconeIndex(
        pineconeConfig.indexName,
        {
          ...pineconeConfig.config,
          sourceTag: "langchainjs",
        },
        pineconeConfig.namespace,
        pineconeConfig.indexHostUrl,
        pineconeConfig.additionalHeaders
      );
    }

    this.textKey = textKey ?? "text";
    this.filter = filter;
    this.caller = new AsyncCaller(asyncCallerArgs);
  }

  /**
   * Method that adds documents to the Pinecone database.
   *
   * @param documents Array of documents to add to the Pinecone database.
   * @param options Optional ids for the documents.
   * @returns Promise that resolves with the ids of the added documents.
   */
  async addDocuments(
    documents: Document[],
    options?: { ids?: string[]; namespace?: string } | string[]
  ): Promise<string[]> {
    const texts = documents.map(({ pageContent }) => pageContent);
    return this.addVectors(
      await this.embeddings.embedDocuments(texts),
      documents,
      options
    );
  }

  /**
   * Method that adds vectors to the Pinecone database.
   *
   * @param vectors Array of vectors to add to the Pinecone database.
   * @param documents Array of documents associated with the vectors.
   * @param options Optional ids for the vectors.
   * @returns Promise that resolves with the ids of the added vectors.
   */
  async addVectors(
    vectors: number[][],
    documents: Document[],
    options?: { ids?: string[]; namespace?: string } | string[]
  ) {
    const ids = Array.isArray(options) ? options : options?.ids;
    const documentIds = ids == null ? documents.map(() => uuid.v4()) : ids;
    const pineconeVectors = vectors.map((values, idx) => {
      // Pinecone doesn't support nested objects, so we flatten them
      const documentMetadata = { ...documents[idx].metadata };
      // preserve string arrays which are allowed
      const stringArrays: Record<string, string[]> = {};
      for (const key of Object.keys(documentMetadata)) {
        if (
          Array.isArray(documentMetadata[key]) &&
          // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
          documentMetadata[key].every((el: any) => typeof el === "string")
        ) {
          stringArrays[key] = documentMetadata[key];
          delete documentMetadata[key];
        }
      }
      const metadata: {
        [key: string]: string | number | boolean | string[] | null;
      } = {
        ...flatten(documentMetadata),
        ...stringArrays,
        [this.textKey]: documents[idx].pageContent,
      };
      // Pinecone doesn't support null values, so we remove them
      for (const key of Object.keys(metadata)) {
        if (metadata[key] == null) {
          delete metadata[key];
        } else if (
          typeof metadata[key] === "object" &&
          Object.keys(metadata[key] as unknown as object).length === 0
        ) {
          delete metadata[key];
        }
      }

      return {
        id: documentIds[idx],
        metadata,
        values,
      } as PineconeRecord<RecordMetadata>;
    });

    const optionsNamespace =
      !Array.isArray(options) && options?.namespace
        ? options.namespace
        : this.namespace;
    const namespace = this.pineconeIndex.namespace(optionsNamespace ?? "");
    // Pinecone recommends a limit of 100 vectors per upsert request
    const chunkSize = 100;
    const chunkedVectors = chunkArray(pineconeVectors, chunkSize);
    const batchRequests = chunkedVectors.map((chunk) =>
      this.caller.call(async () => namespace.upsert(chunk))
    );

    await Promise.all(batchRequests);

    return documentIds;
  }

  /**
   * Method that deletes vectors from the Pinecone database.
   * @param params Parameters for the delete operation.
   * @returns Promise that resolves when the delete operation is complete.
   */
  async delete(params: PineconeDeleteParams): Promise<void> {
    const { deleteAll, ids, filter } = params;
    const optionsNamespace = params.namespace ?? this.namespace;
    const namespace = this.pineconeIndex.namespace(optionsNamespace ?? "");

    if (deleteAll) {
      await namespace.deleteAll();
    } else if (ids) {
      const batchSize = 1000;
      for (let i = 0; i < ids.length; i += batchSize) {
        const batchIds = ids.slice(i, i + batchSize);
        await namespace.deleteMany(batchIds);
      }
    } else if (filter) {
      await namespace.deleteMany(filter);
    } else {
      throw new Error("Either ids or delete_all must be provided.");
    }
  }

  protected async _runPineconeQuery(
    query: number[],
    k: number,
    filter?: PineconeMetadata,
    options?: { includeValues: boolean }
  ) {
    if (filter && this.filter) {
      throw new Error("cannot provide both `filter` and `this.filter`");
    }
    const _filter = filter ?? this.filter;

    let optionsNamespace = this.namespace ?? "";
    if (_filter && "namespace" in _filter) {
      optionsNamespace = _filter.namespace;
      delete _filter.namespace;
    }

    const namespace = this.pineconeIndex.namespace(optionsNamespace ?? "");

    const results = await namespace.query({
      includeMetadata: true,
      topK: k,
      vector: query,
      filter: _filter,
      ...options,
    });
    return results;
  }

  /**
   * Format the matching results from the Pinecone query.
   * @param matches Matching results from the Pinecone query.
   * @returns An array of arrays, where each inner array contains a document and its score.
   */
  private _formatMatches(
    matches: ScoredPineconeRecord<RecordMetadata>[] = []
  ): [Document, number][] {
    const documentsWithScores: [Document, number][] = [];

    for (const record of matches) {
      const {
        id,
        score,
        metadata: { [this.textKey]: pageContent, ...metadata } = {
          [this.textKey]: "",
        },
      } = record;

      if (score) {
        documentsWithScores.push([
          new Document({
            id,
            pageContent: pageContent?.toString() ?? "",
            metadata,
          }),
          score,
        ]);
      }
    }

    return documentsWithScores;
  }

  /**
   * Method that performs a similarity search in the Pinecone database and
   * returns the results along with their scores.
   * @param query Query vector for the similarity search.
   * @param k Number of top results to return.
   * @param filter Optional filter to apply to the search.
   * @returns Promise that resolves with an array of documents and their scores.
   */
  async similaritySearchVectorWithScore(
    query: number[],
    k: number,
    filter?: PineconeMetadata
  ): Promise<[Document, number][]> {
    const { matches = [] } = await this._runPineconeQuery(query, k, filter);
    const records = this._formatMatches(matches);

    return records;
  }

  /**
   * Return documents selected using the maximal marginal relevance.
   * Maximal marginal relevance optimizes for similarity to the query AND diversity
   * among selected documents.
   *
   * @param {string} query - Text to look up documents similar to.
   * @param {number} options.k - Number of documents to return.
   * @param {number} options.fetchK=20 - Number of documents to fetch before passing to the MMR algorithm.
   * @param {number} options.lambda=0.5 - Number between 0 and 1 that determines the degree of diversity among the results,
   *                 where 0 corresponds to maximum diversity and 1 to minimum diversity.
   * @param {PineconeMetadata} options.filter - Optional filter to apply to the search.
   *
   * @returns {Promise<DocumentInterface[]>} - List of documents selected by maximal marginal relevance.
   */


/* eslint-disable no-process-env */

import { test } from "@jest/globals";
import * as fs from "node:fs/promises";
import { fileURLToPath } from "node:url";
import * as path from "node:path";
import {
  AIMessage,
  AIMessageChunk,
  HumanMessage,
  SystemMessage,
  ToolMessage,
} from "@langchain/core/messages";
import {
  ChatPromptTemplate,
  MessagesPlaceholder,
} from "@langchain/core/prompts";
import { StructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { FunctionDeclarationSchemaType } from "@google/generative-ai";
import { ChatGoogleGenerativeAI } from "../chat_models.js";

// Save the original value of the 'LANGCHAIN_CALLBACKS_BACKGROUND' environment variable
const originalBackground = process.env.LANGCHAIN_CALLBACKS_BACKGROUND;

const dummyToolResponse = `[{"title":"Weather in New York City","url":"https://www.weatherapi.com/","content":"{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718659486, 'localtime': '2024-06-17 17:24'}, 'current': {'last_updated_epoch': 1718658900, 'last_updated': '2024-06-17 17:15', 'temp_c': 27.8, 'temp_f': 82.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 159, 'wind_dir': 'SSE', 'pressure_mb': 1021.0, 'pressure_in': 30.15, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 58, 'cloud': 25, 'feelslike_c': 29.0, 'feelslike_f': 84.2, 'windchill_c': 26.9, 'windchill_f': 80.5, 'heatindex_c': 27.9, 'heatindex_f': 82.2, 'dewpoint_c': 17.1, 'dewpoint_f': 62.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 18.3, 'gust_kph': 29.4}}","score":0.98192,"raw_content":null},{"title":"New York, NY Monthly Weather | AccuWeather","url":"https://www.accuweather.com/en/us/new-york/10021/june-weather/349727","content":"Get the monthly weather forecast for New York, NY, including daily high/low, historical averages, to help you plan ahead.","score":0.97504,"raw_content":null}]`;

test("Test Google AI", async () => {
  const model = new ChatGoogleGenerativeAI({});
  const res = await model.invoke("what is 1 + 1?");
  expect(res).toBeTruthy();
});

test("Test Google AI generation", async () => {
  const model = new ChatGoogleGenerativeAI({});
  const res = await model.generate([
    [["human", `Translate "I love programming" into Korean.`]],
  ]);
  expect(res).toBeTruthy();
});

test("Test Google AI generation with a stop sequence", async () => {
  const model = new ChatGoogleGenerativeAI({
    stopSequences: ["two", "2"],
  });
  const res = await model.invoke([
    ["human", `What are the first three positive whole numbers?`],
  ]);
  expect(res).toBeTruthy();
  expect(res.additional_kwargs.finishReason).toBe("STOP");
  expect(res.content).not.toContain("2");
  expect(res.content).not.toContain("two");
});

test("Test Google AI generation with a system message", async () => {
  const model = new ChatGoogleGenerativeAI({});
  const res = await model.generate([
    [
      ["system", `You are an amazing translator.`],
      ["human", `Translate "I love programming" into Korean.`],
    ],
  ]);
  expect(res).toBeTruthy();
});

test("Test Google AI multimodal generation", async () => {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const imageData = (
    await fs.readFile(path.join(__dirname, "/data/hotdog.jpg"))
  ).toString("base64");
  const model = new ChatGoogleGenerativeAI({
    modelName: "gemini-1.5-flash",
  });
  const res = await model.invoke([
    new HumanMessage({
      content: [
        {
          type: "text",
          text: "Describe the following image:",
        },
        {
          type: "image_url",
          image_url: `data:image/png;base64,${imageData}`,
        },
      ],
    }),
  ]);
  expect(res).toBeTruthy();
});

test("Test Google AI handleLLMNewToken callback", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    const model = new ChatGoogleGenerativeAI({});
    let tokens = "";
    const res = await model.call(
      [new HumanMessage("what is 1 + 1?")],
      undefined,
      [
        {
          handleLLMNewToken(token: string) {
            tokens += token;
          },
        },
      ]
    );
    const responseContent = typeof res.content === "string" ? res.content : "";
    expect(tokens).toBe(responseContent);
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
});

test("Test Google AI handleLLMNewToken callback with streaming", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    const model = new ChatGoogleGenerativeAI({});
    let tokens = "";
    const res = await model.stream([new HumanMessage("what is 1 + 1?")], {
      callbacks: [
        {
          handleLLMNewToken(token: string) {
            tokens += token;
          },
        },
      ],
    });
    let responseContent = "";
    for await (const streamItem of res) {
      responseContent += streamItem.content;
    }
    expect(tokens).toBe(responseContent);
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
});

test("Test Google AI in streaming mode", async () => {
  // Running LangChain callbacks in the background will sometimes cause the callbackManager to execute
  // after the test/llm call has already finished & returned. Set that environment variable to false
  // to prevent that from happening.
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  try {
    const model = new ChatGoogleGenerativeAI({ streaming: true });
    let tokens = "";
    let nrNewTokens = 0;
    const res = await model.invoke([new HumanMessage("Write a haiku?")], {
      callbacks: [
        {
          handleLLMNewToken(token: string) {
            nrNewTokens += 1;
            tokens += token;
          },
        },
      ],
    });
    expect(nrNewTokens).toBeGreaterThanOrEqual(1);
    expect(res.content).toBe(tokens);
  } finally {
    // Reset the environment variable
    process.env.LANGCHAIN_CALLBACKS_BACKGROUND = originalBackground;
  }
});

async function fileToBase64(filePath: string): Promise<string> {
  const fileData = await fs.readFile(filePath);
  const base64String = Buffer.from(fileData).toString("base64");
  return base64String;
}


import {
  AsyncCaller,
  AsyncCallerCallOptions,
  AsyncCallerParams,
} from "@langchain/core/utils/async_caller";
import { getEnvironmentVariable } from "@langchain/core/utils/env";
import {
  MediaBlob,
  BlobStore,
  BlobStoreOptions,
  MediaBlobData,
} from "./utils/media_core.js";
import {
  GoogleConnectionParams,
  GoogleRawResponse,
  GoogleResponse,
} from "../types.js";
import { GoogleHostConnection, GoogleRawConnection } from "../connection.js";
import {
  ApiKeyGoogleAuth,
  GoogleAbstractedClient,
  GoogleAbstractedClientOpsMethod,
} from "../auth.js";

export interface GoogleUploadConnectionParams<AuthOptions>
  extends GoogleConnectionParams<AuthOptions> {}

export abstract class GoogleMultipartUploadConnection<
  CallOptions extends AsyncCallerCallOptions,
  ResponseType extends GoogleResponse,
  AuthOptions
> extends GoogleHostConnection<CallOptions, ResponseType, AuthOptions> {
  constructor(
    fields: GoogleConnectionParams<AuthOptions> | undefined,
    caller: AsyncCaller,
    client: GoogleAbstractedClient
  ) {
    super(fields, caller, client);
  }

  async _body(
    separator: string,
    data: MediaBlob,
    metadata: Record<string, unknown>
  ): Promise<string> {
    const contentType = data.mimetype;
    const { encoded, encoding } = await data.encode();
    const body = [
      `--${separator}`,
      "Content-Type: application/json; charset=UTF-8",
      "",
      JSON.stringify(metadata),
      "",
      `--${separator}`,
      `Content-Type: ${contentType}`,
      `Content-Transfer-Encoding: ${encoding}`,
      "",
      encoded,
      `--${separator}--`,
    ];
    return body.join("\n");
  }

  async request(
    data: MediaBlob,
    metadata: Record<string, unknown>,
    options: CallOptions
  ): Promise<ResponseType> {
    const separator = `separator-${Date.now()}`;
    const body = await this._body(separator, data, metadata);
    const requestHeaders = {
      "Content-Type": `multipart/related; boundary=${separator}`,
      "X-Goog-Upload-Protocol": "multipart",
    };
    const response = this._request(body, options, requestHeaders);
    return response;
  }
}

export abstract class GoogleDownloadConnection<
  CallOptions extends AsyncCallerCallOptions,
  ResponseType extends GoogleResponse,
  AuthOptions
> extends GoogleHostConnection<CallOptions, ResponseType, AuthOptions> {
  async request(options: CallOptions): Promise<ResponseType> {
    return this._request(undefined, options);
  }
}

export abstract class GoogleDownloadRawConnection<
  CallOptions extends AsyncCallerCallOptions,
  AuthOptions
> extends GoogleRawConnection<CallOptions, AuthOptions> {
  buildMethod(): GoogleAbstractedClientOpsMethod {
    return "GET";
  }

  async request(options: CallOptions): Promise<GoogleRawResponse> {
    return this._request(undefined, options);
  }
}

export interface BlobStoreGoogleParams<AuthOptions>
  extends GoogleConnectionParams<AuthOptions>,
    AsyncCallerParams,
    BlobStoreOptions {}

export abstract class BlobStoreGoogle<
  ResponseType extends GoogleResponse,
  AuthOptions
> extends BlobStore {
  caller: AsyncCaller;

  client: GoogleAbstractedClient;

  constructor(fields?: BlobStoreGoogleParams<AuthOptions>) {
    super(fields);
    this.caller = new AsyncCaller(fields ?? {});
    this.client = this.buildClient(fields);
  }

  abstract buildClient(
    fields?: BlobStoreGoogleParams<AuthOptions>
  ): GoogleAbstractedClient;

  abstract buildSetMetadata([key, blob]: [string, MediaBlob]): Record<
    string,
    unknown
  >;

  abstract buildSetConnection([key, blob]: [
    string,
    MediaBlob
  ]): GoogleMultipartUploadConnection<
    AsyncCallerCallOptions,
    ResponseType,
    AuthOptions
  >;

  async _set(keyValuePair: [string, MediaBlob]): Promise<ResponseType> {
    const [, blob] = keyValuePair;
    const setMetadata = this.buildSetMetadata(keyValuePair);
    const metadata = setMetadata;
    const options = {};
    const connection = this.buildSetConnection(keyValuePair);
    const response = await connection.request(blob, metadata, options);
    return response;
  }

  async mset(keyValuePairs: [string, MediaBlob][]): Promise<void> {
    const ret = keyValuePairs.map((keyValue) => this._set(keyValue));
    await Promise.all(ret);
  }

  abstract buildGetMetadataConnection(
    key: string
  ): GoogleDownloadConnection<
    AsyncCallerCallOptions,
    ResponseType,
    AuthOptions
  >;

  async _getMetadata(key: string): Promise<Record<string, unknown>> {
    const connection = this.buildGetMetadataConnection(key);
    const options = {};
    const response = await connection.request(options);
    return response.data;
  }

  abstract buildGetDataConnection(
    key: string
  ): GoogleDownloadRawConnection<AsyncCallerCallOptions, AuthOptions>;

  async _getData(key: string): Promise<Blob> {
    const connection = this.buildGetDataConnection(key);
    const options = {};
    const response = await connection.request(options);
    return response.data;
  }

  _getMimetypeFromMetadata(metadata: Record<string, unknown>): string {
    return metadata.contentType as string;
  }

  async _get(key: string): Promise<MediaBlob | undefined> {
    const metadata = await this._getMetadata(key);
    const data = await this._getData(key);
    if (data && metadata) {
      const ret = await MediaBlob.fromBlob(data, { metadata, path: key });
      return ret;
    } else {
      return undefined;
    }
  }

  async mget(keys: string[]): Promise<(MediaBlob | undefined)[]> {
    const ret = keys.map((key) => this._get(key));
    return await Promise.all(ret);
  }

  abstract buildDeleteConnection(
    key: string
  ): GoogleDownloadConnection<
    AsyncCallerCallOptions,
    GoogleResponse,
    AuthOptions
  >;

  async _del(key: string): Promise<void> {
    const connection = this.buildDeleteConnection(key);
    const options = {};
    await connection.request(options);
  }

  async mdelete(keys: string[]): Promise<void> {
    const ret = keys.map((key) => this._del(key));
    await Promise.all(ret);
  }

  // eslint-disable-next-line require-yield
  async *yieldKeys(_prefix: string | undefined): AsyncGenerator<string> {
    // TODO: Implement. Most have an implementation that uses nextToken.
    throw new Error("yieldKeys is not implemented");
  }
}

/**
 * Based on https://cloud.google.com/storage/docs/json_api/v1/objects#resource
 */
export interface GoogleCloudStorageObject extends Record<string, unknown> {
  id?: string;
  name?: string;
  contentType?: string;
  metadata?: Record<string, unknown>;
  // This is incomplete.
}

export interface GoogleCloudStorageResponse extends GoogleResponse {
  data: GoogleCloudStorageObject;
}

export type BucketAndPath = {
  bucket: string;
  path: string;
};

export class GoogleCloudStorageUri {
  static uriRegexp = /gs:\/\/([a-z0-9][a-z0-9._-]+[a-z0-9])\/(.*)/;

  bucket: string;

  path: string;

  constructor(uri: string) {
    const bucketAndPath = GoogleCloudStorageUri.uriToBucketAndPath(uri);
    this.bucket = bucketAndPath.bucket;
    this.path = bucketAndPath.path;
  }

  get uri() {
    return `gs://${this.bucket}/${this.path}`;
  }

  get isValid() {
    return (
      typeof this.bucket !== "undefined" && typeof this.path !== "undefined"
    );
  }

  static uriToBucketAndPath(uri: string): BucketAndPath {
    const match = this.uriRegexp.exec(uri);
    if (!match) {
      throw new Error(`Invalid gs:// URI: ${uri}`);
    }
    return {
      bucket: match[1],
      path: match[2],
    };
  }

  static isValidUri(uri: string): boolean {
    return this.uriRegexp.test(uri);
  }
}

export interface GoogleCloudStorageConnectionParams {
  uri: string;
}

export interface GoogleCloudStorageUploadConnectionParams<AuthOptions>
  extends GoogleUploadConnectionParams<AuthOptions>,
    GoogleCloudStorageConnectionParams {}

export class GoogleCloudStorageUploadConnection<
  AuthOptions
> extends GoogleMultipartUploadConnection<
  AsyncCallerCallOptions,
  GoogleCloudStorageResponse,
  AuthOptions
> {
  uri: GoogleCloudStorageUri;

  constructor(
    fields: GoogleCloudStorageUploadConnectionParams<AuthOptions>,
    caller: AsyncCaller,
    client: GoogleAbstractedClient
  ) {
    super(fields, caller, client);
    this.uri = new GoogleCloudStorageUri(fields.uri);
  }

  async buildUrl(): Promise<string> {
    return `https://storage.googleapis.com/upload/storage/${this.apiVersion}/b/${this.uri.bucket}/o?uploadType=multipart`;
  }
}


export interface GoogleCloudStorageDownloadConnectionParams<AuthOptions>
  extends GoogleCloudStorageConnectionParams,
    GoogleConnectionParams<AuthOptions> {
  method: GoogleAbstractedClientOpsMethod;
  alt: "media" | undefined;
}

export class GoogleCloudStorageDownloadConnection<
  ResponseType extends GoogleResponse,
  AuthOptions
> extends GoogleDownloadConnection<
  AsyncCallerCallOptions,
  ResponseType,
  AuthOptions
> {
  uri: GoogleCloudStorageUri;

  method: GoogleAbstractedClientOpsMethod;

  alt: "media" | undefined;

  constructor(
    fields: GoogleCloudStorageDownloadConnectionParams<AuthOptions>,
    caller: AsyncCaller,
    client: GoogleAbstractedClient
  ) {
    super(fields, caller, client);
    this.uri = new GoogleCloudStorageUri(fields.uri);
    this.method = fields.method;
    this.alt = fields.alt;
  }

  buildMethod(): GoogleAbstractedClientOpsMethod {
    return this.method;
  }

  async buildUrl(): Promise<string> {
    const path = encodeURIComponent(this.uri.path);
    const ret = `https://storage.googleapis.com/storage/${this.apiVersion}/b/${this.uri.bucket}/o/${path}`;
    return this.alt ? `${ret}?alt=${this.alt}` : ret;
  }
}

export interface GoogleCloudStorageRawConnectionParams<AuthOptions>
  extends GoogleCloudStorageConnectionParams,
    GoogleConnectionParams<AuthOptions> {}

export class GoogleCloudStorageRawConnection<
  AuthOptions
> extends GoogleDownloadRawConnection<AsyncCallerCallOptions, AuthOptions> {
  uri: GoogleCloudStorageUri;

  constructor(
    fields: GoogleCloudStorageRawConnectionParams<AuthOptions>,
    caller: AsyncCaller,
    client: GoogleAbstractedClient
  ) {
    super(fields, caller, client);
    this.uri = new GoogleCloudStorageUri(fields.uri);
  }

  async buildUrl(): Promise<string> {
    const path = encodeURIComponent(this.uri.path);
    const ret = `https://storage.googleapis.com/storage/${this.apiVersion}/b/${this.uri.bucket}/o/${path}?alt=media`;
    return ret;
  }
}

export interface BlobStoreGoogleCloudStorageBaseParams<AuthOptions>
  extends BlobStoreGoogleParams<AuthOptions> {
  uriPrefix: GoogleCloudStorageUri;
}

export abstract class BlobStoreGoogleCloudStorageBase<
  AuthOptions
> extends BlobStoreGoogle<GoogleCloudStorageResponse, AuthOptions> {
  params: BlobStoreGoogleCloudStorageBaseParams<AuthOptions>;

  constructor(fields: BlobStoreGoogleCloudStorageBaseParams<AuthOptions>) {
    super(fields);
    this.params = fields;
    this.defaultStoreOptions = {
      ...this.defaultStoreOptions,
      pathPrefix: fields.uriPrefix.uri,
    };
  }

  buildSetConnection([key, _blob]: [
    string,
    MediaBlob
  ]): GoogleMultipartUploadConnection<
    AsyncCallerCallOptions,
    GoogleCloudStorageResponse,
    AuthOptions
  > {
    const params: GoogleCloudStorageUploadConnectionParams<AuthOptions> = {
      ...this.params,
      uri: key,
    };
    return new GoogleCloudStorageUploadConnection<AuthOptions>(
      params,
      this.caller,
      this.client
    );
  }

  buildSetMetadata([key, blob]: [string, MediaBlob]): Record<string, unknown> {
    const uri = new GoogleCloudStorageUri(key);
    const ret: GoogleCloudStorageObject = {
      name: uri.path,
      metadata: blob.metadata,
      contentType: blob.mimetype,
    };
    return ret;
  }

  buildGetMetadataConnection(
    key: string
  ): GoogleDownloadConnection<
    AsyncCallerCallOptions,
    GoogleCloudStorageResponse,
    AuthOptions
  > {
    const params: GoogleCloudStorageDownloadConnectionParams<AuthOptions> = {
      uri: key,
      method: "GET",
      alt: undefined,
    };
    return new GoogleCloudStorageDownloadConnection<
      GoogleCloudStorageResponse,
      AuthOptions
    >(params, this.caller, this.client);
  }

  buildGetDataConnection(
    key: string
  ): GoogleDownloadRawConnection<AsyncCallerCallOptions, AuthOptions> {
    const params: GoogleCloudStorageRawConnectionParams<AuthOptions> = {
      uri: key,
    };
    return new GoogleCloudStorageRawConnection<AuthOptions>(
      params,
      this.caller,
      this.client
    );
  }

  buildDeleteConnection(
    key: string
  ): GoogleDownloadConnection<
    AsyncCallerCallOptions,
    GoogleResponse,
    AuthOptions
  > {
    const params: GoogleCloudStorageDownloadConnectionParams<AuthOptions> = {
      uri: key,
      method: "DELETE",
      alt: undefined,
    };
    return new GoogleCloudStorageDownloadConnection<
      GoogleResponse,
      AuthOptions
    >(params, this.caller, this.client);
  }
}

export type AIStudioFileState =
  | "PROCESSING"
  | "ACTIVE"
  | "FAILED"
  | "STATE_UNSPECIFIED";

export type AIStudioFileVideoMetadata = {
  videoMetadata: {
    videoDuration: string; // Duration in seconds, possibly with fractional, ending in "s"
  };
};

export type AIStudioFileMetadata = AIStudioFileVideoMetadata;

export interface AIStudioFileObject {
  name?: string;
  displayName?: string;
  mimeType?: string;
  sizeBytes?: string; // int64 format
  createTime?: string; // timestamp format
  updateTime?: string; // timestamp format
  expirationTime?: string; // timestamp format
  sha256Hash?: string; // base64 encoded
  uri?: string;
  state?: AIStudioFileState;
  error?: {
    code: number;
    message: string;
    details: Record<string, unknown>[];
  };
  metadata?: AIStudioFileMetadata;
}

export class AIStudioMediaBlob extends MediaBlob {
  _valueAsDate(value: string): Date {
    if (!value) {
      return new Date(0);
    }
    return new Date(value);
  }

  _metadataFieldAsDate(field: string): Date {
    return this._valueAsDate(this.metadata?.[field]);
  }

  get createDate(): Date {
    return this._metadataFieldAsDate("createTime");
  }

  get updateDate(): Date {
    return this._metadataFieldAsDate("updateTime");
  }

  get expirationDate(): Date {
    return this._metadataFieldAsDate("expirationTime");
  }

  get isExpired(): boolean {
    const now = new Date().toISOString();
    const exp = this.metadata?.expirationTime ?? now;
    return exp <= now;
  }
}

export interface AIStudioFileGetResponse extends GoogleResponse {
  data: AIStudioFileObject;
}

export interface AIStudioFileSaveResponse extends GoogleResponse {
  data: {
    file: AIStudioFileObject;
  };
}

export interface AIStudioFileListResponse extends GoogleResponse {
  data: {
    files: AIStudioFileObject[];
    nextPageToken: string;
  };
}

export type AIStudioFileResponse =
  | AIStudioFileGetResponse
  | AIStudioFileSaveResponse
  | AIStudioFileListResponse;

export interface AIStudioFileConnectionParams {}

export interface AIStudioFileUploadConnectionParams<AuthOptions>
  extends GoogleUploadConnectionParams<AuthOptions>,
    AIStudioFileConnectionParams {}

export class AIStudioFileUploadConnection<
  AuthOptions
> extends GoogleMultipartUploadConnection<
  AsyncCallerCallOptions,
  AIStudioFileSaveResponse,
  AuthOptions
> {
  apiVersion = "v1beta";

  async buildUrl(): Promise<string> {
    return `https://generativelanguage.googleapis.com/upload/${this.apiVersion}/files`;
  }
}

export interface AIStudioFileDownloadConnectionParams<AuthOptions>
  extends AIStudioFileConnectionParams,
    GoogleConnectionParams<AuthOptions> {
  method: GoogleAbstractedClientOpsMethod;
  name: string;
}

export class AIStudioFileDownloadConnection<
  ResponseType extends GoogleResponse,
  AuthOptions
> extends GoogleDownloadConnection<
  AsyncCallerCallOptions,
  ResponseType,
  AuthOptions
> {
  method: GoogleAbstractedClientOpsMethod;

  name: string;

  apiVersion = "v1beta";

  constructor(
    fields: AIStudioFileDownloadConnectionParams<AuthOptions>,
    caller: AsyncCaller,
    client: GoogleAbstractedClient
  ) {
    super(fields, caller, client);
    this.method = fields.method;
    this.name = fields.name;
  }

  buildMethod(): GoogleAbstractedClientOpsMethod {
    return this.method;
  }

  async buildUrl(): Promise<string> {
    return `https://generativelanguage.googleapis.com/${this.apiVersion}/files/${this.name}`;
  }
}

export interface BlobStoreAIStudioFileBaseParams<AuthOptions>
  extends BlobStoreGoogleParams<AuthOptions> {
  retryTime?: number;
}


import type { BaseLanguageModelCallOptions } from "@langchain/core/language_models/base";
import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
import { GenerationChunk } from "@langchain/core/outputs";
import type { StringWithAutocomplete } from "@langchain/core/utils/types";
import { LLM, type BaseLLMParams } from "@langchain/core/language_models/llms";
import { Ollama as OllamaClient } from "ollama/browser";
import { OllamaCamelCaseOptions } from "./types.js";

export interface OllamaCallOptions extends BaseLanguageModelCallOptions {
  images?: string[];
}

export interface OllamaInput extends BaseLLMParams, OllamaCamelCaseOptions {
  /**
   * The model to use when making requests.
   * @default "llama3"
   */
  model?: string;
  /**
   * Optionally override the base URL to make request to.
   * This should only be set if your Ollama instance is being
   * server from a non-standard location.
   * @default "http://localhost:11434"
   */
  baseUrl?: string;
  format?: string;
}

/**
 * Class that represents the Ollama language model. It extends the base
 * LLM class and implements the OllamaInput interface.
 * @example
 * ```typescript
 * const ollama = new Ollama({
 *   baseUrl: "http://api.example.com",
 *   model: "llama3",
 * });
 *
 * // Streaming translation from English to German
 * const stream = await ollama.stream(
 *   `Translate "I love programming" into German.`
 * );
 *
 * const chunks = [];
 * for await (const chunk of stream) {
 *   chunks.push(chunk);
 * }
 *
 * console.log(chunks.join(""));
 * ```
 */
export class Ollama extends LLM<OllamaCallOptions> implements OllamaInput {
  static lc_name() {
    return "Ollama";
  }

  lc_serializable = true;

  model = "llama3";

  baseUrl = "http://localhost:11434";

  keepAlive: string | number = "5m";

  embeddingOnly?: boolean;

  f16KV?: boolean;

  frequencyPenalty?: number;

  logitsAll?: boolean;

  lowVram?: boolean;

  mainGpu?: number;

  mirostat?: number;

  mirostatEta?: number;

  mirostatTau?: number;

  numBatch?: number;

  numCtx?: number;

  numGpu?: number;

  numKeep?: number;

  numPredict?: number;

  numThread?: number;

  penalizeNewline?: boolean;

  presencePenalty?: number;

  repeatLastN?: number;

  repeatPenalty?: number;

  temperature?: number;

  stop?: string[];

  tfsZ?: number;

  topK?: number;

  topP?: number;

  typicalP?: number;

  useMLock?: boolean;

  useMMap?: boolean;

  vocabOnly?: boolean;

  format?: StringWithAutocomplete<"json">;

  client: OllamaClient;

  constructor(fields?: OllamaInput & BaseLLMParams) {
    super(fields ?? {});
    this.model = fields?.model ?? this.model;
    this.baseUrl = fields?.baseUrl?.endsWith("/")
      ? fields?.baseUrl.slice(0, -1)
      : fields?.baseUrl ?? this.baseUrl;
    this.client = new OllamaClient({
      host: this.baseUrl,
    });
    this.keepAlive = fields?.keepAlive ?? this.keepAlive;

    this.embeddingOnly = fields?.embeddingOnly;
    this.f16KV = fields?.f16Kv;
    this.frequencyPenalty = fields?.frequencyPenalty;
    this.logitsAll = fields?.logitsAll;
    this.lowVram = fields?.lowVram;
    this.mainGpu = fields?.mainGpu;
    this.mirostat = fields?.mirostat;
    this.mirostatEta = fields?.mirostatEta;
    this.mirostatTau = fields?.mirostatTau;
    this.numBatch = fields?.numBatch;
    this.numCtx = fields?.numCtx;
    this.numGpu = fields?.numGpu;
    this.numKeep = fields?.numKeep;
    this.numPredict = fields?.numPredict;
    this.numThread = fields?.numThread;
    this.penalizeNewline = fields?.penalizeNewline;
    this.presencePenalty = fields?.presencePenalty;
    this.repeatLastN = fields?.repeatLastN;
    this.repeatPenalty = fields?.repeatPenalty;
    this.temperature = fields?.temperature;
    this.stop = fields?.stop;
    this.tfsZ = fields?.tfsZ;
    this.topK = fields?.topK;
    this.topP = fields?.topP;
    this.typicalP = fields?.typicalP;
    this.useMLock = fields?.useMlock;
    this.useMMap = fields?.useMmap;
    this.vocabOnly = fields?.vocabOnly;
    this.format = fields?.format;
  }

  _llmType() {
    return "ollama";
  }

  invocationParams(options?: this["ParsedCallOptions"]) {
    return {
      model: this.model,
      format: this.format,
      keep_alive: this.keepAlive,
      images: options?.images,
      options: {
        embedding_only: this.embeddingOnly,
        f16_kv: this.f16KV,
        frequency_penalty: this.frequencyPenalty,
        logits_all: this.logitsAll,
        low_vram: this.lowVram,
        main_gpu: this.mainGpu,
        mirostat: this.mirostat,
        mirostat_eta: this.mirostatEta,
        mirostat_tau: this.mirostatTau,
        num_batch: this.numBatch,
        num_ctx: this.numCtx,
        num_gpu: this.numGpu,
        num_keep: this.numKeep,
        num_predict: this.numPredict,
        num_thread: this.numThread,
        penalize_newline: this.penalizeNewline,
        presence_penalty: this.presencePenalty,
        repeat_last_n: this.repeatLastN,
        repeat_penalty: this.repeatPenalty,
        temperature: this.temperature,
        stop: options?.stop ?? this.stop,
        tfs_z: this.tfsZ,
        top_k: this.topK,
        top_p: this.topP,
        typical_p: this.typicalP,
        use_mlock: this.useMLock,
        use_mmap: this.useMMap,
        vocab_only: this.vocabOnly,
      },
    };
  }

  async *_streamResponseChunks(
    prompt: string,
    options: this["ParsedCallOptions"],
    runManager?: CallbackManagerForLLMRun
  ): AsyncGenerator<GenerationChunk> {
    const stream = await this.caller.call(async () =>
      this.client.generate({
        ...this.invocationParams(options),
        prompt,
        stream: true,
      })
    );
    for await (const chunk of stream) {
      if (options.signal?.aborted) {
        throw new Error("This operation was aborted");
      }

      if (!chunk.done) {
        yield new GenerationChunk({
          text: chunk.response,
          generationInfo: {
            ...chunk,
            response: undefined,
          },
        });
        await runManager?.handleLLMNewToken(chunk.response ?? "");
      } else {
        yield new GenerationChunk({
          text: "",
          generationInfo: {
            model: chunk.model,
            total_duration: chunk.total_duration,
            load_duration: chunk.load_duration,
            prompt_eval_count: chunk.prompt_eval_count,
            prompt_eval_duration: chunk.prompt_eval_duration,
            eval_count: chunk.eval_count,
            eval_duration: chunk.eval_duration,
          },
        });
      }
    }
  }

  /** @ignore */
  async _call(
    prompt: string,
    options: this["ParsedCallOptions"],
    runManager?: CallbackManagerForLLMRun
  ): Promise<string> {
    const chunks = [];
    for await (const chunk of this._streamResponseChunks(
      prompt,
      options,
      runManager
    )) {
      chunks.push(chunk.text);
    }
    return chunks.join("");
  }
}


import { test, expect } from "@jest/globals";
import * as fs from "node:fs/promises";
import { fileURLToPath } from "node:url";
import * as path from "node:path";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
import { PromptTemplate } from "@langchain/core/prompts";
import {
  BytesOutputParser,
  StringOutputParser,
} from "@langchain/core/output_parsers";
import { ChatOllama } from "../chat_models.js";

test("test invoke", async () => {
  const ollama = new ChatOllama({
    maxRetries: 1,
  });
  const result = await ollama.invoke([
    "human",
    "What is a good name for a company that makes colorful socks?",
  ]);
  expect(result).toBeDefined();
  expect(typeof result.content).toBe("string");
  expect(result.content.length).toBeGreaterThan(1);
});

test("test call with callback", async () => {
  const ollama = new ChatOllama({
    maxRetries: 1,
  });
  const tokens: string[] = [];
  const result = await ollama.invoke(
    "What is a good name for a company that makes colorful socks?",
    {
      callbacks: [
        {
          handleLLMNewToken(token: string) {
            tokens.push(token);
          },
        },
      ],
    }
  );
  expect(tokens.length).toBeGreaterThan(1);
  expect(result.content).toEqual(tokens.join(""));
});

test("test streaming call", async () => {
  const ollama = new ChatOllama({
    maxRetries: 1,
  });
  const stream = await ollama.stream(
    `Translate "I love programming" into German.`
  );
  const chunks = [];
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  expect(chunks.length).toBeGreaterThan(1);
});

test("should abort the request", async () => {
  const ollama = new ChatOllama({
    maxRetries: 1,
  });
  const controller = new AbortController();

  await expect(() => {
    const ret = ollama.invoke("Respond with an extremely verbose response", {
      signal: controller.signal,
    });
    controller.abort();
    return ret;
  }).rejects.toThrow("This operation was aborted");
});

test("Test multiple messages", async () => {
  const model = new ChatOllama({
    maxRetries: 1,
  });
  const res = await model.invoke([
    new HumanMessage({ content: "My name is Jonas" }),
  ]);
  expect(res).toBeDefined();
  expect(res.content).toBeDefined();
  const res2 = await model.invoke([
    new HumanMessage("My name is Jonas"),
    new AIMessage(
      "Hello Jonas! It's nice to meet you. Is there anything I can help you with?"
    ),
    new HumanMessage("What did I say my name was?"),
  ]);

  expect(res2).toBeDefined();
  expect(res2.content).toBeDefined();
});

test("should stream through with a bytes output parser", async () => {
  const TEMPLATE = `You are a pirate named Patchy. All responses must be extremely verbose and in pirate dialect.

User: {input}
AI:`;

  // Infer the input variables from the template
  const prompt = PromptTemplate.fromTemplate(TEMPLATE);

  const ollama = new ChatOllama({
    maxRetries: 1,
  });
  const outputParser = new BytesOutputParser();
  const chain = prompt.pipe(ollama).pipe(outputParser);
  const stream = await chain.stream({
    input: `Translate "I love programming" into German.`,
  });
  const chunks = [];
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  expect(chunks.length).toBeGreaterThan(1);
});

test("JSON mode", async () => {
  const TEMPLATE = `You are a pirate named Patchy. All responses must be in pirate dialect and in JSON format, with a property named "response" followed by the value.

User: {input}
AI:`;

  // Infer the input variables from the template
  const prompt = PromptTemplate.fromTemplate(TEMPLATE);

  const ollama = new ChatOllama({
    model: "llama3",
    format: "json",
    maxRetries: 1,
  });
  const outputParser = new StringOutputParser();
  const chain = prompt.pipe(ollama).pipe(outputParser);
  const res = await chain.invoke({
    input: `Translate "I love programming" into German.`,
  });
  expect(JSON.parse(res).response).toBeDefined();
});

test.skip("Test ChatOllama with an image", async () => {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const imageData = await fs.readFile(path.join(__dirname, "/data/hotdog.jpg"));
  const chat = new ChatOllama({
    model: "llava",
    maxRetries: 1,
  });
  const res = await chat.invoke([
    new HumanMessage({
      content: [
        {
          type: "text",
          text: "What is in this image?",
        },
        {
          type: "image_url",
          image_url: `data:image/jpeg;base64,${imageData.toString("base64")}`,
        },
      ],
    }),
  ]);
  expect(res).toBeDefined();
  expect(res.content).toBeDefined();
});

test("test max tokens (numPredict)", async () => {
  const ollama = new ChatOllama({
    numPredict: 10,
    maxRetries: 1,
  }).pipe(new StringOutputParser());
  const stream = await ollama.stream(
    "explain quantum physics to me in as many words as possible"
  );
  let numTokens = 0;
  let response = "";
  for await (const s of stream) {
    numTokens += 1;
    response += s;
  }

  // Ollama doesn't always stream back the exact number of tokens, so we
  // check for a number which is slightly above the `numPredict`.
  expect(numTokens).toBeLessThanOrEqual(12);
});


> [!IMPORTANT]
> This package is now deprecated in favor of the new Azure integration in the OpenAI SDK. Please use the package [`@langchain/openai`](https://www.npmjs.com/package/@langchain/openai) instead.
> You can find the migration guide [here](https://js.langchain.com/docs/integrations/llms/azure#migration-from-azure-openai-sdk).

# @langchain/azure-openai

This package contains the Azure SDK for OpenAI LangChain.js integrations.

It provides Azure OpenAI support through the [Azure SDK for OpenAI](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/openai/openai) library.

## Installation

```bash npm2yarn
npm install @langchain/azure-openai @langchain/core
```

This package, along with the main LangChain package, depends on [`@langchain/core`](https://npmjs.com/package/@langchain/core/).
If you are using this package with other LangChain packages, you should make sure that all of the packages depend on the same instance of @langchain/core.
You can do so by adding appropriate fields to your project's `package.json` like this:

```json
{
  "name": "your-project",
  "version": "0.0.0",
  "dependencies": {
    "@langchain/azure-openai": "^0.0.4",
    "@langchain/core": "^0.3.0"
  },
  "resolutions": {
    "@langchain/core": "^0.3.0"
  },
  "overrides": {
    "@langchain/core": "^0.3.0"
  },
  "pnpm": {
    "overrides": {
      "@langchain/core": "^0.3.0"
    }
  }
}
```

The field you need depends on the package manager you're using, but we recommend adding a field for the common `yarn`, `npm`, and `pnpm` to maximize compatibility.

## Chat Models

This package contains the `AzureChatOpenAI` class, which is the recommended way to interface with deployed models on Azure OpenAI.

To use, install the requirements, and configure your environment.

```bash
export AZURE_OPENAI_API_ENDPOINT=<your_endpoint>
export AZURE_OPENAI_API_KEY=<your_key>
export AZURE_OPENAI_API_DEPLOYMENT_NAME=<your_deployment_name>
```

Then initialize the model and make the calls:

```typescript
import { AzureChatOpenAI } from "@langchain/azure-openai";

const model = new AzureChatOpenAI({
  // Note that the following are optional, and will default to the values below
  // if not provided.
  azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_ENDPOINT,
  azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
});
const response = await model.invoke(new HumanMessage("Hello world!"));
```

### Streaming

```typescript
import { AzureChatOpenAI } from "@langchain/azure-openai";

const model = new AzureChatOpenAI({
  // Note that the following are optional, and will default to the values below
  // if not provided.
  azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_ENDPOINT,
  azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
});
const response = await model.stream(new HumanMessage("Hello world!"));
```

## Embeddings

This package also supports embeddings with Azure OpenAI.

```typescript
import { AzureOpenAIEmbeddings } from "@langchain/azure-openai";

const embeddings = new AzureOpenAIEmbeddings({
  // Note that the following are optional, and will default to the values below
  // if not provided.
  azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_ENDPOINT,
  azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME,
});
const res = await embeddings.embedQuery("Hello world");
```

## Using Azure managed identity

If you're using [Azure Managed Identity](https://learn.microsoft.com/azure/ai-services/openai/how-to/managed-identity), you can also pass the credentials directly to the constructor:

```typescript
import { DefaultAzureCredential } from "@azure/identity";
import { AzureOpenAI } from "@langchain/azure-openai";

const credentials = new DefaultAzureCredential();

const model = new AzureOpenAI({
  credentials,
  azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_ENDPOINT,
  azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
});
```

## Compatibility with OpenAI API

This library is provides compatibility with the OpenAI API. You can use an API key from OpenAI's developer portal like in the example below:

```typescript
import { AzureOpenAI, OpenAIKeyCredential } from "@langchain/azure-openai";

const model = new AzureOpenAI({
  modelName: "gpt-3.5-turbo",
  credentials: new OpenAIKeyCredential("<your_openai_api_key>"),
});
```

## Development

To develop the Azure OpenAI package, you'll need to follow these instructions:

### Install dependencies

```bash
yarn install
```

### Build the package

```bash
yarn build
```

Or from the repo root:

```bash
yarn build --filter=@langchain/azure-openai
```

### Run tests

Test files should live within a `tests/` file in the `src/` folder. Unit tests should end in `.test.ts` and integration tests should
end in `.int.test.ts`:

```bash
$ yarn test
$ yarn test:int
```

### Lint & Format

Run the linter & formatter to ensure your code is up to standard:

```bash
yarn lint && yarn format
```

### Adding new entrypoints

If you add a new file to be exported, either import & re-export from `src/index.ts`, or add it to `scripts/create-entrypoints.js` and run `yarn build` to generate the new entrypoint.


import { Embeddings } from "@langchain/core/embeddings";
import {
  type OpenAIClientOptions as AzureOpenAIClientOptions,
  OpenAIClient as AzureOpenAIClient,
  AzureKeyCredential,
  OpenAIKeyCredential,
} from "@azure/openai";
import {
  KeyCredential,
  TokenCredential,
  isTokenCredential,
} from "@azure/core-auth";
import { getEnvironmentVariable } from "@langchain/core/utils/env";
import { chunkArray } from "@langchain/core/utils/chunk_array";
import { AzureOpenAIInput, AzureOpenAIEmbeddingsParams } from "./types.js";
import { USER_AGENT_PREFIX } from "./constants.js";

/** @deprecated Import from "@langchain/openai" instead. */
export class AzureOpenAIEmbeddings
  extends Embeddings
  implements AzureOpenAIEmbeddingsParams, AzureOpenAIInput
{
  modelName = "text-embedding-ada-002";

  model = "text-embedding-ada-002";

  batchSize = 512;

  stripNewLines = false;

  timeout?: number;

  user?: string;

  azureOpenAIApiKey?: string;

  apiKey?: string;

  azureOpenAIEndpoint?: string;

  azureOpenAIApiDeploymentName?: string;

  private client: AzureOpenAIClient;

  constructor(
    fields?: Partial<AzureOpenAIEmbeddingsParams> &
      Partial<AzureOpenAIInput> & {
        configuration?: AzureOpenAIClientOptions;
      }
  ) {
    const fieldsWithDefaults = { maxConcurrency: 2, ...fields };

    super(fieldsWithDefaults);

    this.azureOpenAIApiDeploymentName =
      (fieldsWithDefaults?.azureOpenAIEmbeddingsApiDeploymentName ||
        fieldsWithDefaults?.azureOpenAIApiDeploymentName) ??
      (getEnvironmentVariable("AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME") ||
        getEnvironmentVariable("AZURE_OPENAI_API_DEPLOYMENT_NAME"));

    this.azureOpenAIEndpoint =
      fields?.azureOpenAIEndpoint ??
      getEnvironmentVariable("AZURE_OPENAI_API_ENDPOINT");

    const openAiApiKey =
      fields?.apiKey ??
      fields?.openAIApiKey ??
      getEnvironmentVariable("OPENAI_API_KEY");

    this.azureOpenAIApiKey =
      fields?.apiKey ??
      fields?.azureOpenAIApiKey ??
      getEnvironmentVariable("AZURE_OPENAI_API_KEY") ??
      openAiApiKey;
    this.apiKey = this.azureOpenAIApiKey;

    const azureCredential =
      fields?.credentials ??
      (this.apiKey === openAiApiKey
        ? new OpenAIKeyCredential(this.apiKey ?? "")
        : new AzureKeyCredential(this.apiKey ?? ""));

    // eslint-disable-next-line no-instanceof/no-instanceof
    const isOpenAIApiKey = azureCredential instanceof OpenAIKeyCredential;

    if (!this.apiKey && !fields?.credentials) {
      throw new Error("Azure OpenAI API key not found");
    }

    if (!this.azureOpenAIEndpoint && !isOpenAIApiKey) {
      throw new Error("Azure OpenAI Endpoint not found");
    }

    if (!this.azureOpenAIApiDeploymentName && !isOpenAIApiKey) {
      throw new Error("Azure OpenAI Deployment name not found");
    }

    this.modelName =
      fieldsWithDefaults?.model ?? fieldsWithDefaults?.modelName ?? this.model;
    this.model = this.modelName;

    this.batchSize =
      fieldsWithDefaults?.batchSize ?? (this.apiKey ? 1 : this.batchSize);

    this.stripNewLines =
      fieldsWithDefaults?.stripNewLines ?? this.stripNewLines;

    this.timeout = fieldsWithDefaults?.timeout;

    const options = {
      userAgentOptions: { userAgentPrefix: USER_AGENT_PREFIX },
    };

    if (isOpenAIApiKey) {
      this.client = new AzureOpenAIClient(
        azureCredential as OpenAIKeyCredential
      );
    } else if (isTokenCredential(azureCredential)) {
      this.client = new AzureOpenAIClient(
        this.azureOpenAIEndpoint ?? "",
        azureCredential as TokenCredential,
        options
      );
    } else {
      this.client = new AzureOpenAIClient(
        this.azureOpenAIEndpoint ?? "",
        azureCredential as KeyCredential,
        options
      );
    }
  }

  async embedDocuments(texts: string[]): Promise<number[][]> {
    const batches = chunkArray(
      this.stripNewLines ? texts.map((t) => t.replace(/\n/g, " ")) : texts,
      this.batchSize
    );

    const batchRequests = batches.map((batch) => this.getEmbeddings(batch));
    const embeddings = await Promise.all(batchRequests);
    return embeddings.flat();
  }

  async embedQuery(document: string): Promise<number[]> {
    const input = [
      this.stripNewLines ? document.replace(/\n/g, " ") : document,
    ];
    const embeddings = await this.getEmbeddings(input);
    return embeddings.flat();
  }

  private async getEmbeddings(input: string[]): Promise<number[][]> {
    const deploymentName = this.azureOpenAIApiDeploymentName || this.model;

    const res = await this.caller.call(() =>
      this.client.getEmbeddings(deploymentName, input, {
        user: this.user,
        model: this.model,
        requestOptions: {
          timeout: this.timeout,
        },
      })
    );

    return res.data.map((data) => data.embedding);
  }
}


import type {
  OpenAIClientOptions,
  AzureExtensionsOptions,
  ChatRequestMessage,
} from "@azure/openai";
import type { BaseLanguageModelCallOptions } from "@langchain/core/language_models/base";
import type { TiktokenModel } from "js-tiktoken/lite";
import type { EmbeddingsParams } from "@langchain/core/embeddings";
import type { KeyCredential, TokenCredential } from "@azure/core-auth";

// reexport this type from the included package so we can easily override and extend it if needed in the future
// also makes it easier for folks to import this type without digging around into the dependent packages
export type { TiktokenModel };

export declare interface AzureOpenAIInput {
  openAIApiKey?: string;

  /**
   * API key to use when making requests to Azure OpenAI.
   * Alias for `apiKey`
   */
  azureOpenAIApiKey?: string;
  /**
   * API key to use when making requests to Azure OpenAI.
   */
  apiKey?: string;

  /**
   * Endpoint to use when making requests to Azure OpenAI
   */
  azureOpenAIEndpoint?: string;

  /**
   * Azure OpenAI API deployment name to use for completions when making requests to Azure OpenAI.
   * This is the name of the deployment you created in the Azure portal.
   * e.g. "my-openai-deployment"
   * this will be used in the endpoint URL: https://{InstanceName}.openai.azure.com/openai/deployments/my-openai-deployment/
   */
  azureOpenAIApiDeploymentName?: string;

  /** @deprecated Use "azureOpenAIApiDeploymentName" instead. */
  azureOpenAIEmbeddingsApiDeploymentName?: string;

  /**
   * API version to use when making requests to Azure OpenAI.
   */
  azureOpenAIApiVersion?: string;

  credentials?: KeyCredential | TokenCredential;
}

export declare interface OpenAIBaseInput {
  /**
   * Maximum number of tokens to generate in the completion. -1 returns as many
   * tokens as possible given the prompt and the model's maximum context size.
   */
  maxTokens?: number;

  /**
   * The sampling temperature to use that controls the apparent creativity of generated completions.
   * Higher values will make output more random while lower values will make results more focused
   * and deterministic.
   * It is not recommended to modify temperature and top_p for the same completions request as the
   * interaction of these two settings is difficult to predict.
   */
  temperature: number;

  /**
   * An alternative to sampling with temperature called nucleus sampling. This value causes the
   * model to consider the results of tokens with the provided probability mass. As an example, a
   * value of 0.15 will cause only the tokens comprising the top 15% of probability mass to be
   * considered.
   * It is not recommended to modify temperature and top_p for the same completions request as the
   * interaction of these two settings is difficult to predict.
   */
  topP: number;

  /**
   * A map between GPT token IDs and bias scores that influences the probability of specific tokens
   * appearing in a completions response. Token IDs are computed via external tokenizer tools, while
   * bias scores reside in the range of -100 to 100 with minimum and maximum values corresponding to
   * a full ban or exclusive selection of a token, respectively. The exact behavior of a given bias
   * score varies by model.
   */
  logitBias?: Record<string, number>;

  /**
   * An identifier for the caller or end user of the operation. This may be used for tracking
   * or rate-limiting purposes.
   */
  user?: string;

  /**
   * The number of completions choices that should be generated per provided prompt as part of an
   * overall completions response.
   * Because this setting can generate many completions, it may quickly consume your token quota.
   * Use carefully and ensure reasonable settings for max_tokens and stop.
   */
  n: number;

  /**
   * A value that influences the probability of generated tokens appearing based on their existing
   * presence in generated text.
   * Positive values will make tokens less likely to appear when they already exist and increase the
   * model's likelihood to output new topics.
   */
  presencePenalty: number;

  /**
   * A value that influences the probability of generated tokens appearing based on their cumulative
   * frequency in generated text.
   * Positive values will make tokens less likely to appear as their frequency increases and
   * decrease the likelihood of the model repeating the same statements verbatim.
   */
  frequencyPenalty: number;

  /** A collection of textual sequences that will end completions generation. */
  stop?: string[];
  /** A collection of textual sequences that will end completions generation. */
  stopSequences?: string[];

  /** Whether to stream the results or not. Enabling disables tokenUsage reporting */
  streaming: boolean;

  /**
   * Model name to use
   * Alias for `model`
   */
  modelName: string;
  /** Model name to use */
  model?: string;

  /** Holds any additional parameters that are valid to pass to {@link
   * https://platform.openai.com/docs/api-reference/completions/create |
   * `openai.createCompletion`} that are not explicitly specified on this class.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  modelKwargs?: Record<string, any>;

  /**
   * Timeout to use when making requests to OpenAI.
   */
  timeout?: number;
}

export declare interface OpenAIInput extends OpenAIBaseInput {
  /**
   * A value that controls the emission of log probabilities for the provided number of most likely
   * tokens within a completions response.
   */
  logprobs?: number;

  /**
   * A value specifying whether completions responses should include input prompts as prefixes to
   * their generated output.
   */
  echo?: boolean;

  /**
   * A value that controls how many completions will be internally generated prior to response
   * formulation.
   * When used together with n, best_of controls the number of candidate completions and must be
   * greater than n.
   * Because this setting can generate many completions, it may quickly consume your token quota.
   * Use carefully and ensure reasonable settings for max_tokens and stop.
   */
  bestOf?: number;

  /** Batch size to use when passing multiple documents to generate */
  batchSize: number;
}

export interface OpenAICallOptions extends BaseLanguageModelCallOptions {
  /**
   * Additional options to pass to the underlying axios request.
   */
  options?: OpenAIClientOptions;
}

export interface OpenAIChatInput extends OpenAIBaseInput {
  /** ChatGPT messages to pass as a prefix to the prompt */
  prefixMessages?: ChatRequestMessage[];

  azureExtensionOptions?: AzureExtensionsOptions;
}

export interface OpenAIChatCallOptions extends OpenAICallOptions {
  promptIndex?: number;
}

export interface AzureOpenAIEmbeddingsParams extends EmbeddingsParams {
  /**
   * An identifier for the caller or end user of the operation. This may be used for tracking
   * or rate-limiting purposes.
   */
  user?: string;
  /**
   * The model name to provide as part of this embeddings request.
   * Not applicable to Azure OpenAI, where deployment information should be included in the Azure
   * resource URI that's connected to.
   * Alias for `model`
   */
  modelName?: string;
  /**
   * The model name to provide as part of this embeddings request.
   * Not applicable to Azure OpenAI, where deployment information should be included in the Azure
   * resource URI that's connected to.
   */
  model?: string;

  /**
   * The maximum number of documents to embed in a single request. This is
   * limited by the OpenAI API to a maximum of 2048.
   */
  batchSize?: number;

  /**
   * Whether to strip new lines from the input text. This is recommended by
   * OpenAI for older models, but may not be suitable for all use cases.
   * See: https://github.com/openai/openai-python/issues/418#issuecomment-1525939500
   */
  stripNewLines?: boolean;

  /**
   * Timeout to use when making requests to OpenAI.
   */
  timeout?: number;
}


import { SelfQueryRetriever } from "langchain/retrievers/self_query";
import { OpenAI } from "@langchain/openai";
import { VectaraStore } from "@langchain/community/vectorstores/vectara";
import { FakeEmbeddings } from "@langchain/core/utils/testing";
import { Document } from "@langchain/core/documents";
import { VectaraTranslator } from "@langchain/community/structured_query/vectara";
import type { AttributeInfo } from "langchain/chains/query_constructor";

/**
 * First, we create a bunch of documents. You can load your own documents here instead.
 * Each document has a pageContent and a metadata field. Make sure your metadata matches the AttributeInfo below.
 */
const docs = [
  new Document({
    pageContent:
      "A bunch of scientists bring back dinosaurs and mayhem breaks loose",
    metadata: { year: 1993, rating: 7.7, genre: "science fiction" },
  }),
  new Document({
    pageContent:
      "Leo DiCaprio gets lost in a dream within a dream within a dream within a ...",
    metadata: { year: 2010, director: "Christopher Nolan", rating: 8.2 },
  }),
  new Document({
    pageContent:
      "A psychologist / detective gets lost in a series of dreams within dreams within dreams and Inception reused the idea",
    metadata: { year: 2006, director: "Satoshi Kon", rating: 8.6 },
  }),
  new Document({
    pageContent:
      "A bunch of normal-sized women are supremely wholesome and some men pine after them",
    metadata: { year: 2019, director: "Greta Gerwig", rating: 8.3 },
  }),
  new Document({
    pageContent: "Toys come alive and have a blast doing so",
    metadata: { year: 1995, genre: "animated" },
  }),
  new Document({
    pageContent: "Three men walk into the Zone, three men walk out of the Zone",
    metadata: {
      year: 1979,
      rating: 9.9,
      director: "Andrei Tarkovsky",
      genre: "science fiction",
    },
  }),
];

/**
 * Next, we define the attributes we want to be able to query on.
 * in this case, we want to be able to query on the genre, year, director, rating, and length of the movie.
 * We also provide a description of each attribute and the type of the attribute.
 * This is used to generate the query prompts.
 *
 * We need to setup the filters in the vectara as well otherwise filter won't work.
 * To setup the filter in vectara, go to Data -> {your_created_corpus} -> overview
 * In the overview section edit the filters section and all the following attributes in
 * the filters.
 */
const attributeInfo: AttributeInfo[] = [
  {
    name: "genre",
    description: "The genre of the movie",
    type: "string or array of strings",
  },
  {
    name: "year",
    description: "The year the movie was released",
    type: "number",
  },
  {
    name: "director",
    description: "The director of the movie",
    type: "string",
  },
  {
    name: "rating",
    description: "The rating of the movie (1-10)",
    type: "number",
  },
];

/**
 * Next, we instantiate a vector store. This is where we store the embeddings of the documents.
 * We also need to provide an embeddings object. This is used to embed the documents.
 */

const config = {
  customerId: Number(process.env.VECTARA_CUSTOMER_ID),
  corpusId: Number(process.env.VECTARA_CORPUS_ID),
  apiKey: String(process.env.VECTARA_API_KEY),
  verbose: true,
};

const vectorStore = await VectaraStore.fromDocuments(
  docs,
  new FakeEmbeddings(),
  config
);

const llm = new OpenAI();
const documentContents = "Brief summary of a movie";

const selfQueryRetriever = SelfQueryRetriever.fromLLM({
  llm,
  vectorStore,
  documentContents,
  attributeInfo,
  /**
   * We need to create a basic translator that translates the queries into a
   * filter format that the vector store can understand. We provide a basic translator
   * here, but you can create your own translator by extending BaseTranslator
   * abstract class. Note that the vector store needs to support filtering on the metadata
   * attributes you want to query on.
   */
  structuredQueryTranslator: new VectaraTranslator<VectaraStore>(),
});

/**
 * Now we can query the vector store.
 * We can ask questions like "Which movies are less than 90 minutes?" or "Which movies are rated higher than 8.5?".
 * We can also ask questions like "Which movies are either comedy or drama and are less than 90 minutes?".
 * The retriever will automatically convert these questions into queries that can be used to retrieve documents.
 */
const query1 = await selfQueryRetriever.invoke(
  "What are some movies about dinosaurs"
);
const query2 = await selfQueryRetriever.invoke(
  "I want to watch a movie rated higher than 8.5"
);
const query3 = await selfQueryRetriever.invoke(
  "Which movies are directed by Greta Gerwig?"
);
const query4 = await selfQueryRetriever.invoke(
  "Which movies are either comedy or science fiction and are rated higher than 8.5?"
);
console.log(query1, query2, query3, query4);


import { EnsembleRetriever } from "langchain/retrievers/ensemble";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "@langchain/openai";
import { BaseRetriever, BaseRetrieverInput } from "@langchain/core/retrievers";
import { Document } from "@langchain/core/documents";

class SimpleCustomRetriever extends BaseRetriever {
  lc_namespace = [];

  documents: Document[];

  constructor(fields: { documents: Document[] } & BaseRetrieverInput) {
    super(fields);
    this.documents = fields.documents;
  }

  async _getRelevantDocuments(query: string): Promise<Document[]> {
    return this.documents.filter((document) =>
      document.pageContent.includes(query)
    );
  }
}

const docs1 = [
  new Document({ pageContent: "I like apples", metadata: { source: 1 } }),
  new Document({ pageContent: "I like oranges", metadata: { source: 1 } }),
  new Document({
    pageContent: "apples and oranges are fruits",
    metadata: { source: 1 },
  }),
];

const keywordRetriever = new SimpleCustomRetriever({ documents: docs1 });

const docs2 = [
  new Document({ pageContent: "You like apples", metadata: { source: 2 } }),
  new Document({ pageContent: "You like oranges", metadata: { source: 2 } }),
];

const vectorstore = await MemoryVectorStore.fromDocuments(
  docs2,
  new OpenAIEmbeddings()
);

const vectorstoreRetriever = vectorstore.asRetriever();

const retriever = new EnsembleRetriever({
  retrievers: [vectorstoreRetriever, keywordRetriever],
  weights: [0.5, 0.5],
});

const query = "apples";
const retrievedDocs = await retriever.invoke(query);

console.log(retrievedDocs);

/*
  [
    Document { pageContent: 'You like apples', metadata: { source: 2 } },
    Document { pageContent: 'I like apples', metadata: { source: 1 } },
    Document { pageContent: 'You like oranges', metadata: { source: 2 } },
    Document {
      pageContent: 'apples and oranges are fruits',
      metadata: { source: 1 }
    }
  ]
*/


import * as uuid from "uuid";

import { MultiVectorRetriever } from "langchain/retrievers/multi_vector";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { InMemoryStore } from "@langchain/core/stores";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { Document } from "@langchain/core/documents";

const textLoader = new TextLoader("../examples/state_of_the_union.txt");
const parentDocuments = await textLoader.load();

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 10000,
  chunkOverlap: 20,
});

const docs = await splitter.splitDocuments(parentDocuments);

const idKey = "doc_id";
const docIds = docs.map((_) => uuid.v4());

const childSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400,
  chunkOverlap: 0,
});

const subDocs = [];
for (let i = 0; i < docs.length; i += 1) {
  const childDocs = await childSplitter.splitDocuments([docs[i]]);
  const taggedChildDocs = childDocs.map((childDoc) => {
    // eslint-disable-next-line no-param-reassign
    childDoc.metadata[idKey] = docIds[i];
    return childDoc;
  });
  subDocs.push(...taggedChildDocs);
}

// The byteStore to use to store the original chunks
const byteStore = new InMemoryStore<Uint8Array>();

// The vectorstore to use to index the child chunks
const vectorstore = await FaissStore.fromDocuments(
  subDocs,
  new OpenAIEmbeddings()
);

const retriever = new MultiVectorRetriever({
  vectorstore,
  byteStore,
  idKey,
  // Optional `k` parameter to search for more child documents in VectorStore.
  // Note that this does not exactly correspond to the number of final (parent) documents
  // retrieved, as multiple child documents can point to the same parent.
  childK: 20,
  // Optional `k` parameter to limit number of final, parent documents returned from this
  // retriever and sent to LLM. This is an upper-bound, and the final count may be lower than this.
  parentK: 5,
});

const keyValuePairs: [string, Document][] = docs.map((originalDoc, i) => [
  docIds[i],
  originalDoc,
]);

// Use the retriever to add the original chunks to the document store
await retriever.docstore.mset(keyValuePairs);

// Vectorstore alone retrieves the small chunks
const vectorstoreResult = await retriever.vectorstore.similaritySearch(
  "justice breyer"
);
console.log(vectorstoreResult[0].pageContent.length);
/*
  390
*/

// Retriever returns larger result
const retrieverResult = await retriever.invoke("justice breyer");
console.log(retrieverResult[0].pageContent.length);
/*
  9770
*/


import {
  BaseRetriever,
  type BaseRetrieverInput,
} from "@langchain/core/retrievers";
import type { CallbackManagerForRetrieverRun } from "@langchain/core/callbacks/manager";
import { Document } from "@langchain/core/documents";

/**
 * interface BaseRetrieverInput {
 *   callbacks?: Callbacks;
 *   tags?: string[];
 *   metadata?: Record<string, unknown>;
 *   verbose?: boolean;
 * }
 */
export interface CustomRetrieverInput extends BaseRetrieverInput {}

export class CustomRetriever extends BaseRetriever {
  lc_namespace = ["langchain", "retrievers"];

  constructor(fields?: CustomRetrieverInput) {
    super(fields);
  }

  async _getRelevantDocuments(
    query: string,
    // Use with sub runs for tracing
    _runManager?: CallbackManagerForRetrieverRun
  ): Promise<Document[]> {
    // You can invoke other runnables like this to pass tracing config through:
    // const additionalDocs = await someOtherRunnable.invoke({}, runManager?.getChild());
    return [
      // ...additionalDocs,
      new Document({
        pageContent: `Some document pertaining to ${query}`,
        metadata: {},
      }),
      new Document({
        pageContent: `Some other document pertaining to ${query}`,
        metadata: {},
      }),
    ];
  }
}

const retriever = new CustomRetriever({});

console.log(await retriever.invoke("LangChain docs"));


import * as uuid from "uuid";

import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MultiVectorRetriever } from "langchain/retrievers/multi_vector";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { InMemoryStore } from "@langchain/core/stores";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
import { Document } from "@langchain/core/documents";

const textLoader = new TextLoader("../examples/state_of_the_union.txt");
const parentDocuments = await textLoader.load();

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 10000,
  chunkOverlap: 20,
});

const docs = await splitter.splitDocuments(parentDocuments);

const chain = RunnableSequence.from([
  { content: (doc: Document) => doc.pageContent },
  PromptTemplate.fromTemplate(`Summarize the following document:\n\n{content}`),
  new ChatOpenAI({
    maxRetries: 0,
  }),
  new StringOutputParser(),
]);

const summaries = await chain.batch(docs, {
  maxConcurrency: 5,
});

const idKey = "doc_id";
const docIds = docs.map((_) => uuid.v4());
const summaryDocs = summaries.map((summary, i) => {
  const summaryDoc = new Document({
    pageContent: summary,
    metadata: {
      [idKey]: docIds[i],
    },
  });
  return summaryDoc;
});

// The byteStore to use to store the original chunks
const byteStore = new InMemoryStore<Uint8Array>();

// The vectorstore to use to index the child chunks
const vectorstore = await FaissStore.fromDocuments(
  summaryDocs,
  new OpenAIEmbeddings()
);

const retriever = new MultiVectorRetriever({
  vectorstore,
  byteStore,
  idKey,
});

const keyValuePairs: [string, Document][] = docs.map((originalDoc, i) => [
  docIds[i],
  originalDoc,
]);

// Use the retriever to add the original chunks to the document store
await retriever.docstore.mset(keyValuePairs);

// We could also add the original chunks to the vectorstore if we wish
// const taggedOriginalDocs = docs.map((doc, i) => {
//   doc.metadata[idKey] = docIds[i];
//   return doc;
// });
// retriever.vectorstore.addDocuments(taggedOriginalDocs);

// Vectorstore alone retrieves the small chunks
const vectorstoreResult = await retriever.vectorstore.similaritySearch(
  "justice breyer"
);
console.log(vectorstoreResult[0].pageContent.length);
/*
  1118
*/

// Retriever returns larger result
const retrieverResult = await retriever.invoke("justice breyer");
console.log(retrieverResult[0].pageContent.length);
/*
  9770
*/


import type { ChatPromptTemplate } from "@langchain/core/prompts";
import { pull } from "langchain/hub";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { SessionsPythonREPLTool } from "@langchain/azure-dynamic-sessions";
import { AzureChatOpenAI } from "@langchain/openai";

const tools = [
  new SessionsPythonREPLTool({
    poolManagementEndpoint:
      process.env.AZURE_CONTAINER_APP_SESSION_POOL_MANAGEMENT_ENDPOINT || "",
  }),
];

// Note: you need a model deployment that supports function calling,
// like `gpt-35-turbo` version `1106`.
const llm = new AzureChatOpenAI({
  temperature: 0,
});

// Get the prompt to use - you can modify this!
// If you want to see the prompt in full, you can at:
// https://smith.langchain.com/hub/jacob/tool-calling-agent
const prompt = await pull<ChatPromptTemplate>("jacob/tool-calling-agent");

const agent = await createToolCallingAgent({
  llm,
  tools,
  prompt,
});

const agentExecutor = new AgentExecutor({
  agent,
  tools,
});

const result = await agentExecutor.invoke({
  input:
    "Create a Python program that prints the Python version and return the result.",
});

console.log(result);


import { Redis } from "ioredis";

import { OpenAIEmbeddings } from "@langchain/openai";
import { CacheBackedEmbeddings } from "langchain/embeddings/cache_backed";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { RedisByteStore } from "@langchain/community/storage/ioredis";
import { TextLoader } from "langchain/document_loaders/fs/text";

const underlyingEmbeddings = new OpenAIEmbeddings();

// Requires a Redis instance running at http://localhost:6379.
// See https://github.com/redis/ioredis for full config options.
const redisClient = new Redis();
const redisStore = new RedisByteStore({
  client: redisClient,
});

const cacheBackedEmbeddings = CacheBackedEmbeddings.fromBytesStore(
  underlyingEmbeddings,
  redisStore,
  {
    namespace: underlyingEmbeddings.modelName,
  }
);

const loader = new TextLoader("./state_of_the_union.txt");
const rawDocuments = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 0,
});
const documents = await splitter.splitDocuments(rawDocuments);

let time = Date.now();
const vectorstore = await FaissStore.fromDocuments(
  documents,
  cacheBackedEmbeddings
);
console.log(`Initial creation time: ${Date.now() - time}ms`);
/*
  Initial creation time: 1808ms
*/

// The second time is much faster since the embeddings for the input docs have already been added to the cache
time = Date.now();
const vectorstore2 = await FaissStore.fromDocuments(
  documents,
  cacheBackedEmbeddings
);
console.log(`Cached creation time: ${Date.now() - time}ms`);
/*
  Cached creation time: 33ms
*/

// Many keys logged with hashed values
const keys = [];
for await (const key of redisStore.yieldKeys()) {
  keys.push(key);
}

console.log(keys.slice(0, 5));
/*
  [
    'text-embedding-ada-002fa9ac80e1bf226b7b4dfc03ea743289a65a727b2',
    'text-embedding-ada-0027dbf9c4b36e12fe1768300f145f4640342daaf22',
    'text-embedding-ada-002ea9b59e760e64bec6ee9097b5a06b0d91cb3ab64',
    'text-embedding-ada-002fec5d021611e1527297c5e8f485876ea82dcb111',
    'text-embedding-ada-002c00f818c345da13fed9f2697b4b689338143c8c7'
  ]
*/


import { OpenAIEmbeddings } from "@langchain/openai";
import { CacheBackedEmbeddings } from "langchain/embeddings/cache_backed";
import { InMemoryStore } from "@langchain/core/stores";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { TextLoader } from "langchain/document_loaders/fs/text";

const underlyingEmbeddings = new OpenAIEmbeddings();

const inMemoryStore = new InMemoryStore();

const cacheBackedEmbeddings = CacheBackedEmbeddings.fromBytesStore(
  underlyingEmbeddings,
  inMemoryStore,
  {
    namespace: underlyingEmbeddings.modelName,
  }
);

const loader = new TextLoader("./state_of_the_union.txt");
const rawDocuments = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 0,
});
const documents = await splitter.splitDocuments(rawDocuments);

// No keys logged yet since the cache is empty
for await (const key of inMemoryStore.yieldKeys()) {
  console.log(key);
}

let time = Date.now();
const vectorstore = await FaissStore.fromDocuments(
  documents,
  cacheBackedEmbeddings
);
console.log(`Initial creation time: ${Date.now() - time}ms`);
/*
  Initial creation time: 1905ms
*/

// The second time is much faster since the embeddings for the input docs have already been added to the cache
time = Date.now();
const vectorstore2 = await FaissStore.fromDocuments(
  documents,
  cacheBackedEmbeddings
);
console.log(`Cached creation time: ${Date.now() - time}ms`);
/*
  Cached creation time: 8ms
*/

// Many keys logged with hashed values
const keys = [];
for await (const key of inMemoryStore.yieldKeys()) {
  keys.push(key);
}

console.log(keys.slice(0, 5));
/*
  [
    'text-embedding-ada-002ea9b59e760e64bec6ee9097b5a06b0d91cb3ab64',
    'text-embedding-ada-0023b424f5ed1271a6f5601add17c1b58b7c992772e',
    'text-embedding-ada-002fec5d021611e1527297c5e8f485876ea82dcb111',
    'text-embedding-ada-00262f72e0c2d711c6b861714ee624b28af639fdb13',
    'text-embedding-ada-00262d58882330038a4e6e25ea69a938f4391541874'
  ]
*/


"use node";

import { TextLoader } from "langchain/document_loaders/fs/text";
import { CacheBackedEmbeddings } from "langchain/embeddings/cache_backed";
import { OpenAIEmbeddings } from "@langchain/openai";
import { ConvexKVStore } from "@langchain/community/storage/convex";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { ConvexVectorStore } from "@langchain/community/vectorstores/convex";
import { action } from "./_generated/server.js";

export const ask = action({
  args: {},
  handler: async (ctx) => {
    const underlyingEmbeddings = new OpenAIEmbeddings();

    const cacheBackedEmbeddings = CacheBackedEmbeddings.fromBytesStore(
      underlyingEmbeddings,
      new ConvexKVStore({ ctx }),
      {
        namespace: underlyingEmbeddings.modelName,
      }
    );

    const loader = new TextLoader("./state_of_the_union.txt");
    const rawDocuments = await loader.load();
    const splitter = new RecursiveCharacterTextSplitter({
      chunkSize: 1000,
      chunkOverlap: 0,
    });
    const documents = await splitter.splitDocuments(rawDocuments);

    let time = Date.now();
    const vectorstore = await ConvexVectorStore.fromDocuments(
      documents,
      cacheBackedEmbeddings,
      { ctx }
    );
    console.log(`Initial creation time: ${Date.now() - time}ms`);
    /*
      Initial creation time: 1808ms
    */

    // The second time is much faster since the embeddings for the input docs have already been added to the cache
    time = Date.now();
    const vectorstore2 = await ConvexVectorStore.fromDocuments(
      documents,
      cacheBackedEmbeddings,
      { ctx }
    );
    console.log(`Cached creation time: ${Date.now() - time}ms`);
    /*
      Cached creation time: 33ms
    */
  },
});