diff --git a/docs/docs/integrations/chat/naver.ipynb b/docs/docs/integrations/chat/naver.ipynb new file mode 100644 index 0000000000000..834f34cfddb8b --- /dev/null +++ b/docs/docs/integrations/chat/naver.ipynb @@ -0,0 +1,411 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "afaf8039", + "metadata": {}, + "source": [ + "---\n", + "sidebar_label: Naver\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e49f1e0d", + "metadata": {}, + "source": [ + "# ChatClovaX\n", + "\n", + "This notebook provides a quick overview for getting started with Naver’s HyperCLOVA X [chat models](https://python.langchain.com/v0.2/docs/concepts/#chat-models) via CLOVA Studio. For detailed documentation of all ChatClovaX features and configurations head to the [API reference](https://api.python.langchain.com/en/latest/community/chat_models/langchain_community.chat_models.naver.ChatClovaX.html).\n", + "\n", + "CLOVA Studio has several chat models. You can find information about latest models and their costs, context windows, and supported input types in the CLOVA Studio API Guide [documentation](https://api.ncloud-docs.com/docs/clovastudio-chatcompletions).\n", + "\n", + "## Overview\n", + "### Integration details\n", + "\n", + "| Class | Package | Local | Serializable | [JS support](https://js.langchain.com/v0.2/docs/integrations/chat/naver) | Package downloads | Package latest |\n", + "| :--- | :--- |:-----:| :---: |:------------------------------------------------------------------------:| :---: | :---: |\n", + "| [ChatClovaX](https://api.python.langchain.com/en/latest/chat_models/langchain_community.chat_models.naver.ChatClovaX.html) | [langchain-community](https://api.python.langchain.com/en/latest/community_api_reference.html) | ❌ | beta/❌ | ❌ | ![PyPI - Downloads](https://img.shields.io/pypi/dm/langchain-naver?style=flat-square&label=%20) | ![PyPI - Version](https://img.shields.io/pypi/v/langchain-naver?style=flat-square&label=%20) |\n", + "\n", + "### Model features\n", + "| [Tool calling](/docs/how_to/tool_calling/) | [Structured output](/docs/how_to/structured_output/) | JSON mode | [Image input](/docs/how_to/multimodal_inputs/) | Audio input | Video input | [Token-level streaming](/docs/how_to/chat_streaming/) | Native async | [Token usage](/docs/how_to/chat_token_usage_tracking/) | [Logprobs](/docs/how_to/logprobs/) |\n", + "|:------------------------------------------:| :---: | :---: | :---: | :---: | :---: |:-----------------------------------------------------:| :---: |:------------------------------------------------------:|:----------------------------------:|\n", + "|❌| ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | \n", + "\n", + "## Setup\n", + "\n", + "Before using the chat model, you must go through the three steps below.\n", + "\n", + "1. Creating [NAVER Cloud Platform](https://www.ncloud.com/) account \n", + "2. Apply to use [CLOVA Studio](https://www.ncloud.com/product/aiService/clovaStudio)\n", + "3. Find API Keys after creating CLOVA Studio Test App or Service App (See [here](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#테스트앱생성).)\n", + "\n", + "### Credentials\n", + "\n", + "CLOVA Studio requires 2 keys (`NCP_CLOVASTUDIO_API_KEY` and `NCP_APIGW_API_KEY`).\n", + " - `NCP_CLOVASTUDIO_API_KEY` is issued per Test App or Service App\n", + " - `NCP_APIGW_API_KEY` is issued per account, could be optional depending on the region you are using\n", + "\n", + "The two API Keys could be found by clicking `App Request Status` > `Service App, Test App List` > `‘Details’ button for each app` in [CLOVA Studio](https://clovastudio.ncloud.com/studio-application/service-app)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2def81b5-b023-4f40-a97b-b2c5ca59d6a9", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"NCP_CLOVASTUDIO_API_KEY\"] = getpass.getpass(\"NCP CLOVA Studio API Key: \")\n", + "os.environ[\"NCP_APIGW_API_KEY\"] = getpass.getpass(\"NCP API Gateway API Key: \")" + ] + }, + { + "cell_type": "markdown", + "id": "17bf9053-90c5-4955-b239-55a35cb07566", + "metadata": {}, + "source": [ + "### Installation\n", + "\n", + "The LangChain Naver integration lives in the `langchain-community` package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a15d341e-3e26-4ca3-830b-5aab30ed66de", + "metadata": {}, + "outputs": [], + "source": [ + "# install package\n", + "!pip install -qU langchain-community" + ] + }, + { + "cell_type": "markdown", + "id": "a38cde65-254d-4219-a441-068766c0d4b5", + "metadata": {}, + "source": [ + "## Instantiation\n", + "\n", + "Now we can instantiate our model object and generate chat completions:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cb09c344-1836-4e0c-acf8-11d13ac1dbae", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.chat_models import ChatClovaX\n", + "\n", + "chat = ChatClovaX(\n", + " model=\"HCX-DASH-001\",\n", + " temperature=0.5,\n", + " max_tokens=None,\n", + " max_retries=2,\n", + " # clovastudio_api_key=\"...\" # if you prefer to pass api key in directly instead of using env vars\n", + " # task_id=\"...\" # if you want to use fine-tuned model\n", + " # service_app=False # True if using Service App. Default value is False (means using Test App)\n", + " # include_ai_filters=False # True if you want to detect inappropriate content. Default value is False\n", + " # other params...\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2b4f3e15", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "`ChatClovaX` supports all `invoke`, `batch`, `stream` methods of [`ChatModel`](/docs/how_to#chat-models) including async APIs." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "62e0dbc3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "messages = [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful assistant that translates English to Korean. Translate the user sentence.\",\n", + " ),\n", + " (\"human\", \"I love using NAVER AI.\"),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "47752b59", + "metadata": {}, + "source": [ + "## Invocation\n", + "\n", + "In addition to invoke, we also support batch and stream functionalities." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "d86145b3-bfef-46e8-b227-4dda5c9c2705", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='저는 NAVER AI를 사용하는 것이 너무 좋아요.', response_metadata={'stop_reason': 'stop_before', 'input_length': 25, 'output_length': 15, 'seed': 1655372866, 'ai_filter': None}, id='run-412be878-f8a2-4917-ad71-f23ee250771a-0')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#using invoke\n", + "chat.invoke(messages)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "876d2c2a-1b29-4b9e-92ed-381223c9439f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[AIMessage(content='나는 NAVER AI를 사용하는 것이 좋아요.', response_metadata={'stop_reason': 'stop_before', 'input_length': 25, 'output_length': 13, 'seed': 1938804380, 'ai_filter': None}, id='run-b5349497-a959-4ab9-a3b0-28483b0f223d-0'),\n", + " AIMessage(content='나는 NAVER AI를 사용하는 것이 좋아요.', response_metadata={'stop_reason': 'stop_before', 'input_length': 25, 'output_length': 13, 'seed': 2603129649, 'ai_filter': None}, id='run-ab428833-d6b4-43d8-8342-d034ed6c6447-0')]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#using batch\n", + "chat.batch([messages, messages])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c07af21-dda5-4514-b4de-1f214c2cebcd", + "metadata": {}, + "outputs": [], + "source": [ + "#using stream\n", + "for chunk in chat.stream(messages):\n", + " print(chunk.content, end=\"\", flush=True)" + ] + }, + { + "cell_type": "markdown", + "id": "18e2bfc0-7e78-4528-a73f-499ac150dca8", + "metadata": {}, + "source": [ + "## Chaining\n", + "\n", + "We can [chain](/docs/how_to/sequence/) our model with a prompt template like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e197d1d7-a070-4c96-9f8a-a0e86d046e0b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='나는 NAVER AI를 사용하는 것이 좋아요.', response_metadata={'stop_reason': 'stop_before', 'input_length': 25, 'output_length': 13, 'seed': 4065547179, 'ai_filter': None}, id='run-2662b461-382e-4aac-a59f-8cb824b8cc51-0', usage_metadata={'input_tokens': 25, 'output_tokens': 13, 'total_tokens': 38})" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful assistant that translates {input_language} to {output_language}. Translate the user sentence.\",\n", + " ),\n", + " (\n", + " \"human\", \n", + " \"{input}\"\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "chain = prompt | chat\n", + "chain.invoke(\n", + " {\n", + " \"input_language\": \"English\",\n", + " \"output_language\": \"Korean\",\n", + " \"input\": \"I love using NAVER AI.\",\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d1ee55bc-ffc8-4cfa-801c-993953a08cfd", + "metadata": {}, + "source": [ + "## Additional functionalities\n", + "\n", + "### Fine-tuning\n", + "\n", + "You can call fine-tuned CLOVA X models by passing in your corresponding `task_id` parameter. (You don’t need to specify the model_name parameter when calling fine-tuned model.)\n", + "\n", + "You can check `task_id` from corresponding Test App or Service App details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb436788", + "metadata": {}, + "outputs": [], + "source": [ + "fine_tuned_model = ChatClovaX(\n", + " task_id='abcd123e',\n", + " temperature=0.5,\n", + ")\n", + "\n", + "fine_tuned_model.invoke(messages)" + ] + }, + { + "cell_type": "markdown", + "id": "f428deaf", + "metadata": {}, + "source": [ + "### Service App\n", + "\n", + "When going live with production-level application using CLOVA Studio, you should apply for and use Service App. (See [here](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#서비스앱신청).)\n", + "\n", + "For a Service App, a corresponding `NCP_CLOVASTUDIO_API_KEY` is issued and can only be called with the API Key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcf566df", + "metadata": {}, + "outputs": [], + "source": [ + "#### Update environment variables\n", + "\n", + "os.environ[\"NCP_CLOVASTUDIO_API_KEY\"] = getpass.getpass(\"NCP CLOVA Studio API Key for Service App: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cebe27ae", + "metadata": {}, + "outputs": [], + "source": [ + "chat = ChatClovaX(\n", + " service_app=True, # True if you want to use your service app, default value is False.\n", + " # clovastudio_api_key=\"...\" # if you prefer to pass api key in directly instead of using env vars\n", + " model=\"HCX-DASH-001\",\n", + " temperature=0.5,\n", + " max_tokens=None,\n", + " max_retries=2,\n", + " # other params...\n", + ")\n", + "ai_msg = chat.invoke(messages)" + ] + }, + { + "cell_type": "markdown", + "id": "d73e7140", + "metadata": {}, + "source": [ + "### AI Filter\n", + "\n", + "AI Filter detects inappropriate output such as profanity from the test app (or service app included) created in Playground and informs the user. See [here](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#AIFilter) for details. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32bfbc93", + "metadata": {}, + "outputs": [], + "source": [ + "chat = ChatClovaX(\n", + " model=\"HCX-DASH-001\",\n", + " temperature=0.5,\n", + " max_tokens=None,\n", + " max_retries=2,\n", + " include_ai_filters=True, # True if you want to enabled ai filter\n", + " # other params...\n", + ")\n", + "\n", + "ai_msg = chat.invoke(messages)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bd9e179", + "metadata": {}, + "outputs": [], + "source": [ + "print(ai_msg.response_metadata['ai_filter'])" + ] + }, + { + "cell_type": "markdown", + "id": "3a5bb5ca-c3ae-4a58-be67-2cd18574b9a3", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all ChatNaver features and configurations head to the API reference: https://api.python.langchain.com/en/latest/community/chat_models/langchain_community.chat_models.naver.ChatClovaX.html" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/providers/naver.mdx b/docs/docs/integrations/providers/naver.mdx new file mode 100644 index 0000000000000..33b6bc77ac171 --- /dev/null +++ b/docs/docs/integrations/providers/naver.mdx @@ -0,0 +1,38 @@ +# NAVER + +All functionality related to `Naver` including HyperCLOVA X models, especially via `Naver Cloud` [CLOVA Studio](https://clovastudio.ncloud.com/). + +> [Naver](https://navercorp.com/) is a global technology company with cutting-edge technologies and a diverse business portfolio including search, commerce, fintech, content, cloud, and AI. + +> [Naver Cloud](https://www.navercloudcorp.com/lang/en/) is the cloud computing arm of Naver, a leading cloud service provider offering a comprehensive suite of cloud services to businesses through its [Naver Cloud Platform (NCP)](https://www.ncloud.com/). + +Please refer to [NCP User Guide](https://guide.ncloud-docs.com/docs/clovastudio-overview) for more detailed instructions (also in Korean). + +## Installation and Setup + +- Get both CLOVA Studio API Key and API Gateway Key by [creating your app](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#create-test-app) and set them as environment variables respectively (`NCP_CLOVASTUDIO_API_KEY`, `NCP_APIGW_API_KEY`). +- Install the integration Python package with: + +```bash +pip install -U langchain-community +``` + +## Chat models + +### ChatClovaX + +See a [usage example](/docs/integrations/chat/naver). + +```python +from langchain_community.chat_models import ChatClovaX +``` + +## Embedding models + +### ClovaXEmbeddings + +See a [usage example](/docs/integrations/text_embedding/naver). + +```python +from langchain_community.embeddings import ClovaXEmbeddings +``` \ No newline at end of file diff --git a/docs/docs/integrations/text_embedding/naver.ipynb b/docs/docs/integrations/text_embedding/naver.ipynb new file mode 100644 index 0000000000000..f0cd61edd448c --- /dev/null +++ b/docs/docs/integrations/text_embedding/naver.ipynb @@ -0,0 +1,310 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "afaf8039", + "metadata": {}, + "source": [ + "---\n", + "sidebar_label: Naver\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e49f1e0d", + "metadata": {}, + "source": [ + "# ClovaXEmbeddings\n", + "\n", + "This notebook covers how to get started with embedding models provided by CLOVA Studio. For detailed documentation on `ClovaXEmbeddings` features and configuration options, please refer to the [API reference](https://python.langchain.com/latest/api_reference/community/embeddings/langchain_community.embeddings.naver.ClovaXEmbeddings.html).\n", + "\n", + "## Overview\n", + "### Integration details\n", + "\n", + "| Provider | Package |\n", + "|:--------:|:-------:|\n", + "| [Naver](/docs/integrations/providers/naver.mdx) | [ClovaXEmbeddings](https://python.langchain.com/latest/api_reference/community/embeddings/langchain_community.embeddings.naver.ClovaXEmbeddings.html) |\n", + "\n", + "## Setup\n", + "\n", + "Before using embedding models provided by CLOVA Studio, you must go through the three steps below.\n", + "\n", + "1. Creating [NAVER Cloud Platform](https://www.ncloud.com/) account \n", + "2. Apply to use [CLOVA Studio](https://www.ncloud.com/product/aiService/clovaStudio)\n", + "3. Find API Keys after creating CLOVA Studio Test App or Service App (See [here](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#테스트앱생성).)\n", + "\n", + "### Credentials\n", + "\n", + "CLOVA Studio requires 3 keys (`NCP_CLOVASTUDIO_API_KEY`, `NCP_APIGW_API_KEY` and `NCP_CLOVASTUDIO_APP_ID`) for embeddings.\n", + "- `NCP_CLOVASTUDIO_API_KEY` and `NCP_CLOVASTUDIO_APP_ID` is issued per serviceApp or testApp\n", + "- `NCP_APIGW_API_KEY` is issued per account\n", + "\n", + "The two API Keys could be found by clicking `App Request Status` > `Service App, Test App List` > `‘Details’ button for each app` in [CLOVA Studio](https://clovastudio.ncloud.com/studio-application/service-app)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c52e8a50-3e67-4272-bc80-3954d98f8dea", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"NCP_CLOVASTUDIO_API_KEY\"] = getpass.getpass(\"NCP CLOVA Studio API Key: \")\n", + "os.environ[\"NCP_APIGW_API_KEY\"] = getpass.getpass(\"NCP API Gateway API Key: \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83520d8e-ecf8-4e47-b3bc-1ac205b3a2ab", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"NCP_CLOVASTUDIO_APP_ID\"] = input(\"NCP CLOVA Studio App ID: \")" + ] + }, + { + "cell_type": "markdown", + "id": "ff00653e", + "metadata": {}, + "source": [ + "### Installation\n", + "\n", + "ClovaXEmbeddings integration lives in the `langchain_community` package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99400c9b", + "metadata": {}, + "outputs": [], + "source": [ + "# install package\n", + "!pip install -U langchain-community" + ] + }, + { + "cell_type": "markdown", + "id": "2651e611-9d5b-4315-9bbd-f99f56be4e19", + "metadata": {}, + "source": [ + "## Instantiation\n", + "\n", + "Now we can instantiate our embeddings object and embed query or document:\n", + "\n", + "- There are several embedding models available in CLOVA Studio. Please refer [here](https://guide.ncloud-docs.com/docs/en/clovastudio-explorer03#임베딩API) for further details.\n", + "- Note that you might need to normalize the embeddings depending on your specific use case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62e0dbc3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain_community.embeddings import ClovaXEmbeddings\n", + "\n", + "embeddings = ClovaXEmbeddings(\n", + " #model=\"clir-emb-dolphin\" #default is `clir-emb-dolphin`. change with the model name of corresponding App ID if needed.\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0493b4a8", + "metadata": {}, + "source": [ + "## Indexing and Retrieval\n", + "\n", + "Embedding models are often used in retrieval-augmented generation (RAG) flows, both as part of indexing data as well as later retrieving it. For more detailed instructions, please see our RAG tutorials under the [working with external knowledge tutorials](/docs/tutorials/#working-with-external-knowledge).\n", + "\n", + "Below, see how to index and retrieve data using the `embeddings` object we initialized above. In this example, we will index and retrieve a sample document in the `InMemoryVectorStore`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4d59653", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a vector store with a sample text\n", + "from langchain_core.vectorstores import InMemoryVectorStore\n", + "\n", + "text = \"CLOVA Studio is an AI development tool that allows you to customize your own HyperCLOVA X models.\"\n", + "\n", + "vectorstore = InMemoryVectorStore.from_texts(\n", + " [text],\n", + " embedding=embeddings,\n", + ")\n", + "\n", + "# Use the vectorstore as a retriever\n", + "retriever = vectorstore.as_retriever()\n", + "\n", + "# Retrieve the most similar text\n", + "retrieved_documents = retriever.invoke(\"What is CLOVA Studio?\")\n", + "\n", + "# show the retrieved document's content\n", + "retrieved_documents[0].page_content" + ] + }, + { + "cell_type": "markdown", + "id": "b1a249e1", + "metadata": {}, + "source": [ + "## Direct Usage\n", + "\n", + "Under the hood, the vectorstore and retriever implementations are calling `embeddings.embed_documents(...)` and `embeddings.embed_query(...)` to create embeddings for the text(s) used in `from_texts` and retrieval `invoke` operations, respectively.\n", + "\n", + "You can directly call these methods to get embeddings for your own use cases.\n", + "\n", + "### Embed single texts\n", + "\n", + "You can embed single texts or documents with `embed_query`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12fcfb4b", + "metadata": {}, + "outputs": [], + "source": [ + "embeddings.embed_query(\"My query to look up\")" + ] + }, + { + "cell_type": "markdown", + "id": "8b383b53", + "metadata": {}, + "source": [ + "### Embed multiple texts\n", + "\n", + "You can embed multiple texts with `embed_documents`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f2e6104", + "metadata": {}, + "outputs": [], + "source": [ + "embeddings.embed_documents(\n", + " [\"This is a content of the document\", \"This is another document\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "464a4aae", + "metadata": {}, + "source": [ + "### Embed with async\n", + "\n", + "There are also async functionalities:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46739f68", + "metadata": {}, + "outputs": [], + "source": [ + "# async embed query\n", + "await embeddings.aembed_query(\"My query to look up\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e48632ea", + "metadata": {}, + "outputs": [], + "source": [ + "# async embed documents\n", + "await embeddings.aembed_documents(\n", + " [\"This is a content of the document\", \"This is another document\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "eee40d32367cc5c4", + "metadata": {}, + "source": [ + "## Additional functionalities\n", + "\n", + "### Service App\n", + "\n", + "When going live with production-level application using CLOVA Studio, you should apply for and use Service App. (See [here](https://guide.ncloud-docs.com/docs/en/clovastudio-playground01#서비스앱신청).)\n", + "\n", + "For a Service App, corresponding `NCP_CLOVASTUDIO_API_KEY` and `NCP_CLOVASTUDIO_APP_ID` are issued and can only be called with the API Keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08f9f44e-c6a4-4163-8caf-27a0cda345b7", + "metadata": {}, + "outputs": [], + "source": [ + "#### Update environment variables\n", + "\n", + "os.environ[\"NCP_CLOVASTUDIO_API_KEY\"] = getpass.getpass(\"NCP CLOVA Studio API Key for Service App: \")\n", + "os.environ[\"NCP_CLOVASTUDIO_APP_ID\"] = input(\"NCP CLOVA Studio Service App ID: \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86f59698-b3f4-4b19-a9d4-4facfcea304b", + "metadata": {}, + "outputs": [], + "source": [ + "embeddings = ClovaXEmbeddings(service_app=True)" + ] + }, + { + "cell_type": "markdown", + "id": "1ddeaee9", + "metadata": {}, + "source": [ + "## API Reference\n", + "\n", + "For detailed documentation on `ClovaXEmbeddings` features and configuration options, please refer to the [API reference](https://python.langchain.com/latest/api_reference/community/embeddings/langchain_community.embeddings.naver.ClovaXEmbeddings.html)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/libs/community/langchain_community/chat_models/__init__.py b/libs/community/langchain_community/chat_models/__init__.py index 021f6f5c9d1cf..2466e85c76380 100644 --- a/libs/community/langchain_community/chat_models/__init__.py +++ b/libs/community/langchain_community/chat_models/__init__.py @@ -125,6 +125,9 @@ from langchain_community.chat_models.moonshot import ( MoonshotChat, ) + from langchain_community.chat_models.naver import ( + ChatClovaX, + ) from langchain_community.chat_models.oci_generative_ai import ( ChatOCIGenAI, # noqa: F401 ) @@ -187,6 +190,7 @@ "ChatAnthropic", "ChatAnyscale", "ChatBaichuan", + "ChatClovaX", "ChatCohere", "ChatCoze", "ChatOctoAI", @@ -247,6 +251,7 @@ "ChatAnthropic": "langchain_community.chat_models.anthropic", "ChatAnyscale": "langchain_community.chat_models.anyscale", "ChatBaichuan": "langchain_community.chat_models.baichuan", + "ChatClovaX": "langchain_community.chat_models.naver", "ChatCohere": "langchain_community.chat_models.cohere", "ChatCoze": "langchain_community.chat_models.coze", "ChatDatabricks": "langchain_community.chat_models.databricks", diff --git a/libs/community/langchain_community/chat_models/naver.py b/libs/community/langchain_community/chat_models/naver.py new file mode 100644 index 0000000000000..593cad8fa3232 --- /dev/null +++ b/libs/community/langchain_community/chat_models/naver.py @@ -0,0 +1,524 @@ +import logging +from typing import ( + Any, + AsyncContextManager, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Optional, + Self, + Tuple, + Type, + Union, + cast, +) + +import httpx +from langchain_core.callbacks import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) +from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams +from langchain_core.language_models.llms import create_base_retry_decorator +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + BaseMessageChunk, + ChatMessage, + ChatMessageChunk, + HumanMessage, + HumanMessageChunk, + SystemMessage, + SystemMessageChunk, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.utils import convert_to_secret_str, get_from_env +from pydantic import AliasChoices, Field, SecretStr, model_validator + +_DEFAULT_BASE_URL = "https://clovastudio.stream.ntruss.com" + +logger = logging.getLogger(__name__) + + +def _convert_chunk_to_message_chunk( + sse: Any, default_class: Type[BaseMessageChunk] +) -> BaseMessageChunk: + sse_data = sse.json() + message = sse_data.get("message") + role = message.get("role") + content = message.get("content") or "" + + if role == "user" or default_class == HumanMessageChunk: + return HumanMessageChunk(content=content) + elif role == "assistant" or default_class == AIMessageChunk: + return AIMessageChunk(content=content) + elif role == "system" or default_class == SystemMessageChunk: + return SystemMessageChunk(content=content) + elif role or default_class == ChatMessageChunk: + return ChatMessageChunk(content=content, role=role) + else: + return default_class(content=content) # type: ignore[call-arg] + + +def _convert_message_to_naver_chat_message( + message: BaseMessage, +) -> Dict: + if isinstance(message, ChatMessage): + return dict(role=message.role, content=message.content) + elif isinstance(message, HumanMessage): + return dict(role="user", content=message.content) + elif isinstance(message, SystemMessage): + return dict(role="system", content=message.content) + elif isinstance(message, AIMessage): + return dict(role="assistant", content=message.content) + else: + logger.warning( + "FunctionMessage, ToolMessage not yet supported " + "(https://api.ncloud-docs.com/docs/clovastudio-chatcompletions)" + ) + raise ValueError(f"Got unknown type {message}") + + +def _convert_naver_chat_message_to_message( + _message: Dict, +) -> BaseMessage: + role = _message["role"] + assert role in ( + "assistant", + "system", + "user", + ), f"Expected role to be 'assistant', 'system', 'user', got {role}" + content = cast(str, _message["content"]) + additional_kwargs: Dict = {} + + if role == "user": + return HumanMessage( + content=content, + additional_kwargs=additional_kwargs, + ) + elif role == "system": + return SystemMessage( + content=content, + additional_kwargs=additional_kwargs, + ) + elif role == "assistant": + return AIMessage( + content=content, + additional_kwargs=additional_kwargs, + ) + else: + logger.warning("Got unknown role %s", role) + raise ValueError(f"Got unknown role {role}") + + +async def _aiter_sse( + event_source_mgr: AsyncContextManager[Any], +) -> AsyncIterator[Dict]: + """Iterate over the server-sent events.""" + async with event_source_mgr as event_source: + await _araise_on_error(event_source.response) + async for sse in event_source.aiter_sse(): + event_data = sse.json() + if sse.event == "signal" and event_data.get("data", {}) == "[DONE]": + return + if sse.event == "result": + return + yield sse + + +def _raise_on_error(response: httpx.Response) -> None: + """Raise an error if the response is an error.""" + if httpx.codes.is_error(response.status_code): + error_message = response.read().decode("utf-8") + raise httpx.HTTPStatusError( + f"Error response {response.status_code} " + f"while fetching {response.url}: {error_message}", + request=response.request, + response=response, + ) + + +async def _araise_on_error(response: httpx.Response) -> None: + """Raise an error if the response is an error.""" + if httpx.codes.is_error(response.status_code): + error_message = (await response.aread()).decode("utf-8") + raise httpx.HTTPStatusError( + f"Error response {response.status_code} " + f"while fetching {response.url}: {error_message}", + request=response.request, + response=response, + ) + + +class ChatClovaX(BaseChatModel): + """`NCP ClovaStudio` Chat Completion API. + + following environment variables set or passed in constructor in lower case: + - ``NCP_CLOVASTUDIO_API_KEY`` + - ``NCP_APIGW_API_KEY`` + + Example: + .. code-block:: python + + from langchain_core.messages import HumanMessage + + from langchain_community import ChatClovaX + + model = ChatClovaX() + model.invoke([HumanMessage(content="Come up with 10 names for a song about parrots.")]) + """ # noqa: E501 + + client: httpx.Client = Field(default=None) #: :meta private: + async_client: httpx.AsyncClient = Field(default=None) #: :meta private: + + model_name: str = Field( + default="HCX-003", + validation_alias=AliasChoices("model_name", "model"), + description="NCP ClovaStudio chat model name", + ) + task_id: Optional[str] = Field( + default=None, description="NCP Clova Studio chat model tuning task ID" + ) + service_app: bool = Field( + default=False, + description="false: use testapp, true: use service app on NCP Clova Studio", + ) + + ncp_clovastudio_api_key: Optional[SecretStr] = Field(default=None, alias="api_key") + """Automatically inferred from env are `NCP_CLOVASTUDIO_API_KEY` if not provided.""" + + ncp_apigw_api_key: Optional[SecretStr] = Field(default=None, alias="apigw_api_key") + """Automatically inferred from env are `NCP_APIGW_API_KEY` if not provided.""" + + base_url: str = Field(default=None, alias="base_url") + """ + Automatically inferred from env are `NCP_CLOVASTUDIO_API_BASE_URL` if not provided. + """ + + temperature: Optional[float] = Field(gt=0.0, le=1.0, default=0.5) + top_k: Optional[int] = Field(ge=0, le=128, default=0) + top_p: Optional[float] = Field(ge=0, le=1.0, default=0.8) + repeat_penalty: Optional[float] = Field(gt=0.0, le=10, default=5.0) + max_tokens: Optional[int] = Field(ge=0, le=4096, default=100) + stop_before: Optional[list[str]] = Field(default=None, alias="stop") + include_ai_filters: Optional[bool] = Field(default=False) + seed: Optional[int] = Field(ge=0, le=4294967295, default=0) + + timeout: int = Field(gt=0, default=90) + max_retries: int = Field(ge=1, default=2) + + class Config: + """Configuration for this pydantic object.""" + + allow_population_by_field_name = True + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling the API.""" + defaults = { + "temperature": self.temperature, + "topK": self.top_k, + "topP": self.top_p, + "repeatPenalty": self.repeat_penalty, + "maxTokens": self.max_tokens, + "stopBefore": self.stop_before, + "includeAiFilters": self.include_ai_filters, + "seed": self.seed, + } + filtered = {k: v for k, v in defaults.items() if v is not None} + return filtered + + @property + def _identifying_params(self) -> Dict[str, Any]: + """Get the identifying parameters.""" + self._default_params["model_name"] = self.model_name + return self._default_params + + @property + def lc_secrets(self) -> Dict[str, str]: + return { + "ncp_clovastudio_api_key": "NCP_CLOVASTUDIO_API_KEY", + "ncp_apigw_api_key": "NCP_APIGW_API_KEY", + } + + @property + def _llm_type(self) -> str: + """Return type of chat model.""" + return "chat-naver" + + def _get_ls_params( + self, stop: Optional[List[str]] = None, **kwargs: Any + ) -> LangSmithParams: + """Get the parameters used to invoke the model.""" + params = super()._get_ls_params(stop=stop, **kwargs) + params["ls_provider"] = "naver" + return params + + @property + def _client_params(self) -> Dict[str, Any]: + """Get the parameters used for the client.""" + return self._default_params + + @property + def _api_url(self) -> str: + """GET chat completion api url""" + app_type = "serviceapp" if self.service_app else "testapp" + + if self.task_id: + return ( + f"{self.base_url}/{app_type}/v1/tasks/{self.task_id}/chat-completions" + ) + else: + return f"{self.base_url}/{app_type}/v1/chat-completions/{self.model_name}" + + @model_validator(mode="after") + def validate_model_after(self) -> Self: + if not (self.model_name or self.task_id): + raise ValueError("either model_name or task_id must be assigned a value.") + + if not self.ncp_clovastudio_api_key: + self.ncp_clovastudio_api_key = convert_to_secret_str( + get_from_env("ncp_clovastudio_api_key", "NCP_CLOVASTUDIO_API_KEY") + ) + + if not self.ncp_apigw_api_key: + self.ncp_apigw_api_key = convert_to_secret_str( + get_from_env("ncp_apigw_api_key", "NCP_APIGW_API_KEY") + ) + + if not self.base_url: + self.base_url = get_from_env( + "base_url", "NCP_CLOVASTUDIO_API_BASE_URL", _DEFAULT_BASE_URL + ) + + if not self.client: + self.client = httpx.Client( + base_url=self.base_url, + headers=self.default_headers(), + timeout=self.timeout, + ) + + if not self.async_client: + self.async_client = httpx.AsyncClient( + base_url=self.base_url, + headers=self.default_headers(), + timeout=self.timeout, + ) + + return self + + def default_headers(self) -> Dict[str, Any]: + clovastudio_api_key = ( + self.ncp_clovastudio_api_key.get_secret_value() + if self.ncp_clovastudio_api_key + else None + ) + apigw_api_key = ( + self.ncp_apigw_api_key.get_secret_value() + if self.ncp_apigw_api_key + else None + ) + return { + "Content-Type": "application/json", + "Accept": "application/json", + "X-NCP-CLOVASTUDIO-API-KEY": clovastudio_api_key, + "X-NCP-APIGW-API-KEY": apigw_api_key, + } + + def _create_message_dicts( + self, messages: List[BaseMessage], stop: Optional[List[str]] + ) -> Tuple[List[Dict], Dict[str, Any]]: + params = self._client_params + if stop is not None and "stopBefore" in params: + params["stopBefore"] = stop + + message_dicts = [_convert_message_to_naver_chat_message(m) for m in messages] + return message_dicts, params + + def _completion_with_retry(self, **kwargs: Any) -> Any: + from httpx_sse import ( + ServerSentEvent, + SSEError, + connect_sse, + ) + + if "stream" not in kwargs: + kwargs["stream"] = False + + stream = kwargs["stream"] + if stream: + + def iter_sse() -> Iterator[ServerSentEvent]: + with connect_sse( + self.client, "POST", self._api_url, json=kwargs + ) as event_source: + _raise_on_error(event_source.response) + for sse in event_source.iter_sse(): + event_data = sse.json() + if ( + sse.event == "signal" + and event_data.get("data", {}) == "[DONE]" + ): + return + if sse.event == "result": + return + if sse.event == "error": + raise SSEError(message=sse.data) + yield sse + + return iter_sse() + else: + response = self.client.post(url=self._api_url, json=kwargs) + _raise_on_error(response) + return response.json() + + async def _acompletion_with_retry( + self, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Any: + from httpx_sse import aconnect_sse + + """Use tenacity to retry the async completion call.""" + retry_decorator = _create_retry_decorator(self, run_manager=run_manager) + + @retry_decorator + async def _completion_with_retry(**kwargs: Any) -> Any: + if "stream" not in kwargs: + kwargs["stream"] = False + stream = kwargs["stream"] + if stream: + event_source = aconnect_sse( + self.async_client, "POST", self._api_url, json=kwargs + ) + return _aiter_sse(event_source) + else: + response = await self.async_client.post(url=self._api_url, json=kwargs) + await _araise_on_error(response) + return response.json() + + return await _completion_with_retry(**kwargs) + + def _create_chat_result(self, response: Dict) -> ChatResult: + generations = [] + result = response.get("result", {}) + msg = result.get("message", {}) + message = _convert_naver_chat_message_to_message(msg) + + if isinstance(message, AIMessage): + message.usage_metadata = { + "input_tokens": result.get("inputLength"), + "output_tokens": result.get("outputLength"), + "total_tokens": result.get("inputLength") + result.get("outputLength"), + } + + gen = ChatGeneration( + message=message, + ) + generations.append(gen) + + llm_output = { + "stop_reason": result.get("stopReason"), + "input_length": result.get("inputLength"), + "output_length": result.get("outputLength"), + "seed": result.get("seed"), + "ai_filter": result.get("aiFilter"), + } + return ChatResult(generations=generations, llm_output=llm_output) + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs} + + response = self._completion_with_retry(messages=message_dicts, **params) + + return self._create_chat_result(response) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs, "stream": True} + + default_chunk_class: Type[BaseMessageChunk] = AIMessageChunk + for sse in self._completion_with_retry( + messages=message_dicts, run_manager=run_manager, **params + ): + new_chunk = _convert_chunk_to_message_chunk(sse, default_chunk_class) + default_chunk_class = new_chunk.__class__ + gen_chunk = ChatGenerationChunk(message=new_chunk) + + if run_manager: + run_manager.on_llm_new_token( + token=cast(str, new_chunk.content), chunk=gen_chunk + ) + + yield gen_chunk + + async def _agenerate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs} + + response = await self._acompletion_with_retry( + messages=message_dicts, run_manager=run_manager, **params + ) + + return self._create_chat_result(response) + + async def _astream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncIterator[ChatGenerationChunk]: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs, "stream": True} + + default_chunk_class: Type[BaseMessageChunk] = AIMessageChunk + async for chunk in await self._acompletion_with_retry( + messages=message_dicts, run_manager=run_manager, **params + ): + new_chunk = _convert_chunk_to_message_chunk(chunk, default_chunk_class) + default_chunk_class = new_chunk.__class__ + gen_chunk = ChatGenerationChunk(message=new_chunk) + + if run_manager: + await run_manager.on_llm_new_token( + token=cast(str, new_chunk.content), chunk=gen_chunk + ) + + yield gen_chunk + + +def _create_retry_decorator( + llm: ChatClovaX, + run_manager: Optional[ + Union[AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun] + ] = None, +) -> Callable[[Any], Any]: + """Returns a tenacity retry decorator, preconfigured to handle exceptions""" + + errors = [httpx.RequestError, httpx.StreamError] + return create_base_retry_decorator( + error_types=errors, max_retries=llm.max_retries, run_manager=run_manager + ) diff --git a/libs/community/langchain_community/embeddings/__init__.py b/libs/community/langchain_community/embeddings/__init__.py index 845d437e1ece4..38c7d5a76bc1d 100644 --- a/libs/community/langchain_community/embeddings/__init__.py +++ b/libs/community/langchain_community/embeddings/__init__.py @@ -151,6 +151,9 @@ from langchain_community.embeddings.mosaicml import ( MosaicMLInstructorEmbeddings, ) + from langchain_community.embeddings.naver import ( + ClovaXEmbeddings, + ) from langchain_community.embeddings.nemo import ( NeMoEmbeddings, ) @@ -250,6 +253,7 @@ "BookendEmbeddings", "ClarifaiEmbeddings", "ClovaEmbeddings", + "ClovaXEmbeddings", "CohereEmbeddings", "DashScopeEmbeddings", "DatabricksEmbeddings", @@ -332,6 +336,7 @@ "BookendEmbeddings": "langchain_community.embeddings.bookend", "ClarifaiEmbeddings": "langchain_community.embeddings.clarifai", "ClovaEmbeddings": "langchain_community.embeddings.clova", + "ClovaXEmbeddings": "langchain_community.embeddings.naver", "CohereEmbeddings": "langchain_community.embeddings.cohere", "DashScopeEmbeddings": "langchain_community.embeddings.dashscope", "DatabricksEmbeddings": "langchain_community.embeddings.databricks", diff --git a/libs/community/langchain_community/embeddings/clova.py b/libs/community/langchain_community/embeddings/clova.py index 551dc63526192..fe30cd2af2a4f 100644 --- a/libs/community/langchain_community/embeddings/clova.py +++ b/libs/community/langchain_community/embeddings/clova.py @@ -3,11 +3,17 @@ from typing import Any, Dict, List, Optional, cast import requests +from langchain_core._api.deprecation import deprecated from langchain_core.embeddings import Embeddings from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env from pydantic import BaseModel, ConfigDict, SecretStr, model_validator +@deprecated( + since="0.3.1", + removal="1.0.0", + alternative_import="langchain_community.ClovaXEmbeddings", +) class ClovaEmbeddings(BaseModel, Embeddings): """ Clova's embedding service. diff --git a/libs/community/langchain_community/embeddings/naver.py b/libs/community/langchain_community/embeddings/naver.py new file mode 100644 index 0000000000000..927c2849c093b --- /dev/null +++ b/libs/community/langchain_community/embeddings/naver.py @@ -0,0 +1,190 @@ +import logging +from typing import Any, Dict, List, Optional, Self + +import httpx +from langchain_core.embeddings import Embeddings +from langchain_core.utils import convert_to_secret_str, get_from_env +from pydantic import ( + BaseModel, + Field, + SecretStr, + model_validator, +) + +_DEFAULT_BASE_URL = "https://clovastudio.apigw.ntruss.com" + +logger = logging.getLogger(__name__) + + +def _raise_on_error(response: httpx.Response) -> None: + """Raise an error if the response is an error.""" + if httpx.codes.is_error(response.status_code): + error_message = response.read().decode("utf-8") + raise httpx.HTTPStatusError( + f"Error response {response.status_code} " + f"while fetching {response.url}: {error_message}", + request=response.request, + response=response, + ) + + +async def _araise_on_error(response: httpx.Response) -> None: + """Raise an error if the response is an error.""" + if httpx.codes.is_error(response.status_code): + error_message = (await response.aread()).decode("utf-8") + raise httpx.HTTPStatusError( + f"Error response {response.status_code} " + f"while fetching {response.url}: {error_message}", + request=response.request, + response=response, + ) + + +class ClovaXEmbeddings(BaseModel, Embeddings): + """`NCP ClovaStudio` Embedding API. + + following environment variables set or passed in constructor in lower case: + - ``NCP_CLOVASTUDIO_API_KEY`` + - ``NCP_APIGW_API_KEY`` + - ``NCP_CLOVASTUDIO_APP_ID`` + + Example: + .. code-block:: python + + from langchain_community import ClovaXEmbeddings + + model = ClovaXEmbeddings(model="clir-emb-dolphin") + output = embedding.embed_documents(documents) + """ # noqa: E501 + + client: httpx.Client = Field(default=None) #: :meta private: + async_client: httpx.AsyncClient = Field(default=None) #: :meta private: + + ncp_clovastudio_api_key: Optional[SecretStr] = Field(default=None, alias="api_key") + """Automatically inferred from env are `NCP_CLOVASTUDIO_API_KEY` if not provided.""" + + ncp_apigw_api_key: Optional[SecretStr] = Field(default=None, alias="apigw_api_key") + """Automatically inferred from env are `NCP_APIGW_API_KEY` if not provided.""" + + base_url: str = Field(default=None, alias="base_url") + """ + Automatically inferred from env are `NCP_CLOVASTUDIO_API_BASE_URL` if not provided. + """ + + app_id: Optional[str] = Field(default=None, alias="clovastudio_app_id") + service_app: bool = Field( + default=False, + description="false: use testapp, true: use service app on NCP Clova Studio", + ) + model_name: str = Field( + default="clir-emb-dolphin", + alias="model", + description="NCP ClovaStudio embedding model name", + ) + + timeout: int = Field(gt=0, default=60) + + class Config: + arbitrary_types_allowed = True + + @property + def lc_secrets(self) -> Dict[str, str]: + return { + "ncp_clovastudio_api_key": "NCP_CLOVASTUDIO_API_KEY", + "ncp_apigw_api_key": "NCP_APIGW_API_KEY", + } + + @property + def _api_url(self) -> str: + """GET embedding api url""" + app_type = "serviceapp" if self.service_app else "testapp" + model_name = self.model_name if self.model_name != "bge-m3" else "v2" + return ( + f"{self.base_url}/{app_type}" + f"/v1/api-tools/embedding/{model_name}/{self.app_id}" + ) + + @model_validator(mode="after") + def validate_model_after(self) -> Self: + if not self.ncp_clovastudio_api_key: + self.ncp_clovastudio_api_key = convert_to_secret_str( + get_from_env("ncp_clovastudio_api_key", "NCP_CLOVASTUDIO_API_KEY") + ) + + if not self.ncp_apigw_api_key: + self.ncp_apigw_api_key = convert_to_secret_str( + get_from_env("ncp_apigw_api_key", "NCP_APIGW_API_KEY") + ) + + if not self.base_url: + self.base_url = get_from_env( + "base_url", "NCP_CLOVASTUDIO_API_BASE_URL", _DEFAULT_BASE_URL + ) + + if not self.app_id: + self.app_id = get_from_env("app_id", "NCP_CLOVASTUDIO_APP_ID") + + if not self.client: + self.client = httpx.Client( + base_url=self.base_url, + headers=self.default_headers(), + timeout=self.timeout, + ) + + if not self.async_client: + self.async_client = httpx.AsyncClient( + base_url=self.base_url, + headers=self.default_headers(), + timeout=self.timeout, + ) + + return self + + def default_headers(self) -> Dict[str, Any]: + clovastudio_api_key = ( + self.ncp_clovastudio_api_key.get_secret_value() + if self.ncp_clovastudio_api_key + else None + ) + apigw_api_key = ( + self.ncp_apigw_api_key.get_secret_value() + if self.ncp_apigw_api_key + else None + ) + return { + "Content-Type": "application/json", + "Accept": "application/json", + "X-NCP-CLOVASTUDIO-API-KEY": clovastudio_api_key, + "X-NCP-APIGW-API-KEY": apigw_api_key, + } + + def _embed_text(self, text: str) -> List[float]: + payload = {"text": text} + response = self.client.post(url=self._api_url, json=payload) + _raise_on_error(response) + return response.json()["result"]["embedding"] + + async def _aembed_text(self, text: str) -> List[float]: + payload = {"text": text} + response = await self.async_client.post(url=self._api_url, json=payload) + await _araise_on_error(response) + return response.json()["result"]["embedding"] + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + embeddings = [] + for text in texts: + embeddings.append(self._embed_text(text)) + return embeddings + + def embed_query(self, text: str) -> List[float]: + return self._embed_text(text) + + async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + embeddings = [] + for text in texts: + embedding = await self._aembed_text(text) + embeddings.append(embedding) + return embeddings + + async def aembed_query(self, text: str) -> List[float]: + return await self._aembed_text(text) diff --git a/libs/community/tests/integration_tests/chat_models/test_naver.py b/libs/community/tests/integration_tests/chat_models/test_naver.py new file mode 100644 index 0000000000000..b0f3aef02fa65 --- /dev/null +++ b/libs/community/tests/integration_tests/chat_models/test_naver.py @@ -0,0 +1,62 @@ +"""Test ChatNaver chat model.""" + +from langchain_community.chat_models import ChatClovaX + + +def test_stream() -> None: + """Test streaming tokens from ChatClovaX.""" + llm = ChatClovaX() + + for token in llm.stream("I'm Clova"): + assert isinstance(token.content, str) + + +async def test_astream() -> None: + """Test streaming tokens from ChatClovaX.""" + llm = ChatClovaX() + + async for token in llm.astream("I'm Clova"): + assert isinstance(token.content, str) + + +async def test_abatch() -> None: + """Test streaming tokens from ChatClovaX.""" + llm = ChatClovaX() + + result = await llm.abatch(["I'm Clova", "I'm not Clova"]) + for token in result: + assert isinstance(token.content, str) + + +async def test_abatch_tags() -> None: + """Test batch tokens from ChatClovaX.""" + llm = ChatClovaX() + + result = await llm.abatch(["I'm Clova", "I'm not Clova"], config={"tags": ["foo"]}) + for token in result: + assert isinstance(token.content, str) + + +def test_batch() -> None: + """Test batch tokens from ChatClovaX.""" + llm = ChatClovaX() + + result = llm.batch(["I'm Clova", "I'm not Clova"]) + for token in result: + assert isinstance(token.content, str) + + +async def test_ainvoke() -> None: + """Test invoke tokens from ChatClovaX.""" + llm = ChatClovaX() + + result = await llm.ainvoke("I'm Clova", config={"tags": ["foo"]}) + assert isinstance(result.content, str) + + +def test_invoke() -> None: + """Test invoke tokens from ChatClovaX.""" + llm = ChatClovaX() + + result = llm.invoke("I'm Clova", config=dict(tags=["foo"])) + assert isinstance(result.content, str) diff --git a/libs/community/tests/integration_tests/embeddings/test_naver.py b/libs/community/tests/integration_tests/embeddings/test_naver.py new file mode 100644 index 0000000000000..a2c836c7ac287 --- /dev/null +++ b/libs/community/tests/integration_tests/embeddings/test_naver.py @@ -0,0 +1,37 @@ +"""Test Naver embeddings.""" + +from langchain_community.embeddings import ClovaXEmbeddings + + +def test_embedding_documents() -> None: + """Test cohere embeddings.""" + documents = ["foo bar"] + embedding = ClovaXEmbeddings() + output = embedding.embed_documents(documents) + assert len(output) == 1 + assert len(output[0]) > 0 + + +async def test_aembedding_documents() -> None: + """Test cohere embeddings.""" + documents = ["foo bar"] + embedding = ClovaXEmbeddings() + output = await embedding.aembed_documents(documents) + assert len(output) == 1 + assert len(output[0]) > 0 + + +def test_embedding_query() -> None: + """Test cohere embeddings.""" + document = "foo bar" + embedding = ClovaXEmbeddings() + output = embedding.embed_query(document) + assert len(output) > 0 + + +async def test_aembedding_query() -> None: + """Test cohere embeddings.""" + document = "foo bar" + embedding = ClovaXEmbeddings() + output = await embedding.aembed_query(document) + assert len(output) > 0 diff --git a/libs/community/tests/unit_tests/chat_models/test_imports.py b/libs/community/tests/unit_tests/chat_models/test_imports.py index d8399ed83158f..496cc03ba1bbf 100644 --- a/libs/community/tests/unit_tests/chat_models/test_imports.py +++ b/libs/community/tests/unit_tests/chat_models/test_imports.py @@ -6,6 +6,7 @@ "ChatAnthropic", "ChatAnyscale", "ChatBaichuan", + "ChatClovaX", "ChatCohere", "ChatCoze", "ChatDatabricks", diff --git a/libs/community/tests/unit_tests/chat_models/test_naver.py b/libs/community/tests/unit_tests/chat_models/test_naver.py new file mode 100644 index 0000000000000..a4c48af874e32 --- /dev/null +++ b/libs/community/tests/unit_tests/chat_models/test_naver.py @@ -0,0 +1,196 @@ +"""Test chat model integration.""" + +import json +import os +from typing import Any, AsyncGenerator, Generator, cast +from unittest.mock import patch + +import pytest +from httpx_sse import ServerSentEvent +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, +) +from langchain_core.pydantic_v1 import SecretStr + +from langchain_community.chat_models import ChatClovaX +from langchain_community.chat_models.naver import ( + _convert_message_to_naver_chat_message, + _convert_naver_chat_message_to_message, +) + +os.environ["NCP_CLOVASTUDIO_API_KEY"] = "test_api_key" +os.environ["NCP_APIGW_API_KEY"] = "test_gw_key" + + +def test_initialization_api_key() -> None: + """Test chat model initialization.""" + chat_model = ChatClovaX(api_key="foo", apigw_api_key="bar") # type: ignore[arg-type] + assert ( + cast(SecretStr, chat_model.ncp_clovastudio_api_key).get_secret_value() == "foo" + ) + assert cast(SecretStr, chat_model.ncp_apigw_api_key).get_secret_value() == "bar" + + +def test_initialization_model_name() -> None: + llm = ChatClovaX(model="HCX-DASH-001") # type: ignore[call-arg] + assert llm.model_name == "HCX-DASH-001" + llm = ChatClovaX(model_name="HCX-DASH-001") + assert llm.model_name == "HCX-DASH-001" + + +def test_convert_dict_to_message_human() -> None: + message = {"role": "user", "content": "foo"} + result = _convert_naver_chat_message_to_message(message) + expected_output = HumanMessage(content="foo") + assert result == expected_output + assert _convert_message_to_naver_chat_message(expected_output) == message + + +def test_convert_dict_to_message_ai() -> None: + message = {"role": "assistant", "content": "foo"} + result = _convert_naver_chat_message_to_message(message) + expected_output = AIMessage(content="foo") + assert result == expected_output + assert _convert_message_to_naver_chat_message(expected_output) == message + + +def test_convert_dict_to_message_system() -> None: + message = {"role": "system", "content": "foo"} + result = _convert_naver_chat_message_to_message(message) + expected_output = SystemMessage(content="foo") + assert result == expected_output + assert _convert_message_to_naver_chat_message(expected_output) == message + + +@pytest.fixture +def mock_chat_completion_response() -> dict: + return { + "status": {"code": "20000", "message": "OK"}, + "result": { + "message": { + "role": "assistant", + "content": "Phrases: Record what happened today and prepare " + "for tomorrow. " + "The diary will make your life richer.", + }, + "stopReason": "LENGTH", + "inputLength": 100, + "outputLength": 10, + "aiFilter": [ + {"groupName": "curse", "name": "insult", "score": "1"}, + {"groupName": "curse", "name": "discrimination", "score": "0"}, + { + "groupName": "unsafeContents", + "name": "sexualHarassment", + "score": "2", + }, + ], + }, + } + + +def test_naver_invoke(mock_chat_completion_response: dict) -> None: + llm = ChatClovaX() + completed = False + + def mock_completion_with_retry(*args: Any, **kwargs: Any) -> Any: + nonlocal completed + completed = True + return mock_chat_completion_response + + with patch.object(ChatClovaX, "_completion_with_retry", mock_completion_with_retry): + res = llm.invoke("Let's test it.") + assert ( + res.content + == "Phrases: Record what happened today and prepare for tomorrow. " + "The diary will make your life richer." + ) + assert completed + + +async def test_naver_ainvoke(mock_chat_completion_response: dict) -> None: + llm = ChatClovaX() + completed = False + + async def mock_acompletion_with_retry(*args: Any, **kwargs: Any) -> Any: + nonlocal completed + completed = True + return mock_chat_completion_response + + with patch.object( + ChatClovaX, "_acompletion_with_retry", mock_acompletion_with_retry + ): + res = await llm.ainvoke("Let's test it.") + assert ( + res.content + == "Phrases: Record what happened today and prepare for tomorrow. " + "The diary will make your life richer." + ) + assert completed + + +def _make_completion_response_from_token(token: str) -> ServerSentEvent: + return ServerSentEvent( + event="token", + data=json.dumps( + dict( + index=0, + inputLength=89, + outputLength=1, + message=dict( + content=token, + role="assistant", + ), + ) + ), + ) + + +def mock_chat_stream(*args: Any, **kwargs: Any) -> Generator: + def it() -> Generator: + for token in ["Hello", " how", " can", " I", " help", "?"]: + yield _make_completion_response_from_token(token) + + return it() + + +async def mock_chat_astream(*args: Any, **kwargs: Any) -> AsyncGenerator: + async def it() -> AsyncGenerator: + for token in ["Hello", " how", " can", " I", " help", "?"]: + yield _make_completion_response_from_token(token) + + return it() + + +class MyCustomHandler(BaseCallbackHandler): + last_token: str = "" + + def on_llm_new_token(self, token: str, **kwargs: Any) -> None: + self.last_token = token + + +@patch( + "langchain_community.chat_models.ChatClovaX._completion_with_retry", + new=mock_chat_stream, +) +@pytest.mark.requires("httpx_sse") +def test_stream_with_callback() -> None: + callback = MyCustomHandler() + chat = ChatClovaX(callbacks=[callback]) + for token in chat.stream("Hello"): + assert callback.last_token == token.content + + +@patch( + "langchain_community.chat_models.ChatClovaX._acompletion_with_retry", + new=mock_chat_astream, +) +@pytest.mark.requires("httpx_sse") +async def test_astream_with_callback() -> None: + callback = MyCustomHandler() + chat = ChatClovaX(callbacks=[callback]) + async for token in chat.astream("Hello"): + assert callback.last_token == token.content diff --git a/libs/community/tests/unit_tests/embeddings/test_imports.py b/libs/community/tests/unit_tests/embeddings/test_imports.py index 6298b43464aa5..a6f26ce0c3fa6 100644 --- a/libs/community/tests/unit_tests/embeddings/test_imports.py +++ b/libs/community/tests/unit_tests/embeddings/test_imports.py @@ -7,6 +7,7 @@ "AzureOpenAIEmbeddings", "BaichuanTextEmbeddings", "ClarifaiEmbeddings", + "ClovaXEmbeddings", "CohereEmbeddings", "DatabricksEmbeddings", "ElasticsearchEmbeddings", diff --git a/libs/community/tests/unit_tests/embeddings/test_naver.py b/libs/community/tests/unit_tests/embeddings/test_naver.py new file mode 100644 index 0000000000000..7fd8b28665633 --- /dev/null +++ b/libs/community/tests/unit_tests/embeddings/test_naver.py @@ -0,0 +1,18 @@ +"""Test embedding model integration.""" + +import os +from typing import cast + +from langchain_core.pydantic_v1 import SecretStr + +from langchain_community.embeddings import ClovaXEmbeddings + +os.environ["NCP_CLOVASTUDIO_API_KEY"] = "test_api_key" +os.environ["NCP_APIGW_API_KEY"] = "test_gw_key" +os.environ["NCP_CLOVASTUDIO_APP_ID"] = "test_app_id" + + +def test_initialization_api_key() -> None: + llm = ClovaXEmbeddings(api_key="foo", apigw_api_key="bar") # type: ignore[arg-type] + assert cast(SecretStr, llm.ncp_clovastudio_api_key).get_secret_value() == "foo" + assert cast(SecretStr, llm.ncp_apigw_api_key).get_secret_value() == "bar"