From 1a8364a25d03e927192da10841bad0c12bf2445c Mon Sep 17 00:00:00 2001 From: Dmitry Labazkin Date: Tue, 29 Oct 2024 09:10:20 +0300 Subject: [PATCH 1/2] docs: use monthly downloads counter --- libs/gigachat/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/gigachat/README.md b/libs/gigachat/README.md index 59c2a8a..e8841fd 100644 --- a/libs/gigachat/README.md +++ b/libs/gigachat/README.md @@ -5,7 +5,7 @@ This is a library integration with [GigaChat](https://giga.chat/). [![GitHub Release](https://img.shields.io/github/v/release/ai-forever/langchain-gigachat?style=flat-square)](https://github.com/ai-forever/langchain-gigachat/releases) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ai-forever/langchain-gigachat/check_diffs.yml?style=flat-square)](https://github.com/ai-forever/langchain-gigachat/actions/workflows/check_diffs.yml) [![GitHub License](https://img.shields.io/github/license/ai-forever/langchain-gigachat?style=flat-square)](https://opensource.org/license/MIT) -[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/ai-forever/langchain-gigachat/total?style=flat-square)](https://pypistats.org/packages/langchain-gigachat) +[![GitHub Downloads (all assets, all releases)](https://img.shields.io/pypi/dm/langchain-gigachat?style=flat-square?style=flat-square)](https://pypistats.org/packages/langchain-gigachat) [![GitHub Repo stars](https://img.shields.io/github/stars/ai-forever/langchain-gigachat?style=flat-square)](https://star-history.com/#ai-forever/langchain-gigachat) [![GitHub Open Issues](https://img.shields.io/github/issues-raw/ai-forever/langchain-gigachat)](https://github.com/ai-forever/langchain-gigachat/issues) From 1d57eb37a69790b439bc19c437934d6c4d2c0ab3 Mon Sep 17 00:00:00 2001 From: Konstantin Krestnikov Date: Tue, 26 Nov 2024 15:32:51 +0300 Subject: [PATCH 2/2] Dev (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add authorization methods to README for GigaChat usage (#1) * ci: add workflow for dev branch (#2) * docs: use monthly downloads counter (#3) * docs: Readme update * feat: support loading prompts from github repos (#8) * docs: use monthly downloads counter * feat: support loading prompts from github repos --------- Co-authored-by: Dmitry Labazkin * Support few-shots from class description (#10) * docs: use monthly downloads counter * few_shot_examples можно задавать через pydantic схему * feat: add pydantic schema support for few_shot_examples --------- Co-authored-by: Dmitry Labazkin Co-authored-by: NIK-TIGER-BILL * chore: minor version up (#14) --------- Co-authored-by: Dmitry Labazkin Co-authored-by: NIK-TIGER-BILL --- .github/workflows/check_diffs.yml | 4 +- libs/gigachat/README.md | 42 +++++++++++- .../chat_models/gigachat.py | 35 +++++++++- .../langchain_gigachat/tools/load_prompt.py | 67 +++++++++++++++++++ .../utils/function_calling.py | 5 ++ libs/gigachat/poetry.lock | 41 ++++++++++-- libs/gigachat/pyproject.toml | 6 +- .../tests/unit_tests/test_gigachat.py | 43 +++++++++++- libs/gigachat/tests/unit_tests/test_utils.py | 31 +++++++++ 9 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 libs/gigachat/langchain_gigachat/tools/load_prompt.py create mode 100644 libs/gigachat/tests/unit_tests/test_utils.py diff --git a/.github/workflows/check_diffs.yml b/.github/workflows/check_diffs.yml index 3516f2b..9423c5d 100644 --- a/.github/workflows/check_diffs.yml +++ b/.github/workflows/check_diffs.yml @@ -3,7 +3,9 @@ name: CI on: push: - branches: [master] + branches: + - master + - dev pull_request: # If another push to the same PR or branch happens while this workflow is still running, diff --git a/libs/gigachat/README.md b/libs/gigachat/README.md index e8841fd..e99f876 100644 --- a/libs/gigachat/README.md +++ b/libs/gigachat/README.md @@ -10,7 +10,45 @@ This is a library integration with [GigaChat](https://giga.chat/). [![GitHub Open Issues](https://img.shields.io/github/issues-raw/ai-forever/langchain-gigachat)](https://github.com/ai-forever/langchain-gigachat/issues) ## Installation - ```bash pip install -U langchain-gigachat -``` \ No newline at end of file +``` + +## Quickstart +Follow these simple steps to get up and running quickly. + +### Installation +To install the package use following command: +```shell +pip install -U langchain-gigachat +``` + +### Initialization + +To initialize chat model: +```python +from langchain_gigachat.chat_models import GigaChat + +giga = GigaChat(credentials="YOUR_AUTHORIZATION_KEY", verify_ssl_certs=False) +``` + +To initialize embeddings: + +```python +from langchain_gigachat.embeddings import GigaChatEmbeddings + +embedding = GigaChatEmbeddings( + credentials="YOUR_AUTHORIZATION_KEY", + verify_ssl_certs=False +) +``` + +### Usage + +Use the GigaChat object to generate responses: + +```python +print(giga.invoke("Hello, world!")) +``` + +Now you can use the GigaChat object with LangChain's standard primitives to create LLM-applications. \ No newline at end of file diff --git a/libs/gigachat/langchain_gigachat/chat_models/gigachat.py b/libs/gigachat/langchain_gigachat/chat_models/gigachat.py index 130d342..4855898 100644 --- a/libs/gigachat/langchain_gigachat/chat_models/gigachat.py +++ b/libs/gigachat/langchain_gigachat/chat_models/gigachat.py @@ -314,14 +314,43 @@ def trim_content_to_stop_sequence( class GigaChat(_BaseGigaChat, BaseChatModel): """`GigaChat` large language models API. - To use, you should pass login and password to access GigaChat API or use token. + To use, provide credentials via token, login and password, + or mTLS for secure access to the GigaChat API. - Example: + Example Usage: .. code-block:: python from langchain_community.chat_models import GigaChat - giga = GigaChat(credentials=..., scope=..., verify_ssl_certs=False) + # Authorization with Token + # (obtainable in the personal cabinet under Authorization Data): + giga = GigaChat(credentials="YOUR_TOKEN") + + # Personal Space: + giga = GigaChat(credentials="YOUR_TOKEN", scope="GIGACHAT_API_PERS") + + # Corporate Space: + giga = GigaChat(credentials="YOUR_TOKEN", scope="GIGACHAT_API_CORP") + + # Authorization with Login and Password: + giga = GigaChat( + base_url="https://gigachat.devices.sberbank.ru/api/v1", + user="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + + # Mutual Authentication via TLS (mTLS): + giga = GigaChat( + base_url="https://gigachat.devices.sberbank.ru/api/v1", + ca_bundle_file="certs/ca.pem", # chain_pem.txt + cert_file="certs/tls.pem", # published_pem.txt + key_file="certs/tls.key", + key_file_password="YOUR_KEY_PASSWORD", + ) + + # Authorization with Temporary Token: + giga = GigaChat(access_token="YOUR_TEMPORARY_TOKEN") + """ def _build_payload(self, messages: List[BaseMessage], **kwargs: Any) -> gm.Chat: diff --git a/libs/gigachat/langchain_gigachat/tools/load_prompt.py b/libs/gigachat/langchain_gigachat/tools/load_prompt.py new file mode 100644 index 0000000..e4bca8f --- /dev/null +++ b/libs/gigachat/langchain_gigachat/tools/load_prompt.py @@ -0,0 +1,67 @@ +"""Utilities for loading templates from gigachain +github-based hub or other extenal sources.""" + +import os +import re +import tempfile +from pathlib import Path, PurePosixPath +from typing import Any, Callable, Optional, Set, TypeVar, Union +from urllib.parse import urljoin + +import requests +from langchain_core.prompts.base import BasePromptTemplate +from langchain_core.prompts.loading import _load_prompt_from_file + +DEFAULT_REF = os.environ.get("GIGACHAIN_HUB_DEFAULT_REF", "master") +URL_BASE = os.environ.get( + "GIGACHAIN_HUB_DEFAULT_REF", + "https://raw.githubusercontent.com/ai-forever/gigachain/{ref}/hub/", +) +HUB_PATH_RE = re.compile(r"lc(?P@[^:]+)?://(?P.*)") + +T = TypeVar("T") + + +def _load_from_giga_hub( + path: Union[str, Path], + loader: Callable[[str], T], + valid_prefix: str, + valid_suffixes: Set[str], + **kwargs: Any, +) -> Optional[T]: + """Load configuration from hub. Returns None if path is not a hub path.""" + if not isinstance(path, str) or not (match := HUB_PATH_RE.match(path)): + return None + ref, remote_path_str = match.groups() + ref = ref[1:] if ref else DEFAULT_REF + remote_path = Path(remote_path_str) + if remote_path.parts[0] != valid_prefix: + return None + if remote_path.suffix[1:] not in valid_suffixes: + raise ValueError(f"Unsupported file type, must be one of {valid_suffixes}.") + + # Using Path with URLs is not recommended, because on Windows + # the backslash is used as the path separator, which can cause issues + # when working with URLs that use forward slashes as the path separator. + # Instead, use PurePosixPath to ensure that forward slashes are used as the + # path separator, regardless of the operating system. + full_url = urljoin(URL_BASE.format(ref=ref), PurePosixPath(remote_path).__str__()) + + r = requests.get(full_url, timeout=5) + if r.status_code != 200: + raise ValueError(f"Could not find file at {full_url}") + with tempfile.TemporaryDirectory() as tmpdirname: + file = Path(tmpdirname) / remote_path.name + with open(file, "wb") as f: + f.write(r.content) + return loader(str(file), **kwargs) + + +def load_from_giga_hub(path: Union[str, Path]) -> BasePromptTemplate: + """Unified method for loading a prompt from GigaChain repo or local fs.""" + if hub_result := _load_from_giga_hub( + path, _load_prompt_from_file, "prompts", {"py", "json", "yaml"} + ): + return hub_result + else: + raise ValueError("Prompt not found in GigaChain Hub.") diff --git a/libs/gigachat/langchain_gigachat/utils/function_calling.py b/libs/gigachat/langchain_gigachat/utils/function_calling.py index b8dda8b..6915454 100644 --- a/libs/gigachat/langchain_gigachat/utils/function_calling.py +++ b/libs/gigachat/langchain_gigachat/utils/function_calling.py @@ -365,6 +365,11 @@ def convert_pydantic_to_gigachat_function( "Incorrect function or tool description. Description is required." ) + if few_shot_examples is None and hasattr(model, "few_shot_examples"): + few_shot_examples_attr = getattr(model, "few_shot_examples") + if inspect.isfunction(few_shot_examples_attr): + few_shot_examples = few_shot_examples_attr() + return GigaFunctionDescription( name=name or title, description=description, diff --git a/libs/gigachat/poetry.lock b/libs/gigachat/poetry.lock index 3aeebea..2af8062 100644 --- a/libs/gigachat/poetry.lock +++ b/libs/gigachat/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -300,13 +300,13 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "gigachat" -version = "0.1.35" +version = "0.1.36" description = "GigaChat. Python-library for GigaChain and LangChain" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "gigachat-0.1.35-py3-none-any.whl", hash = "sha256:4e142b1f10807a3655d7c54740103571aca073f2fc91aad2eefbff378121f5a1"}, - {file = "gigachat-0.1.35.tar.gz", hash = "sha256:508de76d99659c96bb5345d33242095c7b358c453eaa645c5379b06691cfb454"}, + {file = "gigachat-0.1.36-py3-none-any.whl", hash = "sha256:7e9e3192e8d33e9c63beac533a31290d1a2e7b1b3c57ea0b5dc26da47551db5d"}, + {file = "gigachat-0.1.36.tar.gz", hash = "sha256:6c10ae23647946854ef98d67899e2ef239a6f675e4e1406ff08f8d56fc2160d6"}, ] [package.dependencies] @@ -962,6 +962,23 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-mock" +version = "1.12.1" +description = "Mock out responses from the requests package" +optional = false +python-versions = ">=3.5" +files = [ + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, +] + +[package.dependencies] +requests = ">=2.22,<3" + +[package.extras] +fixture = ["fixtures"] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -1040,6 +1057,20 @@ files = [ {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, + {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -1091,4 +1122,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "b9830f2612cf42645cb3bbf53c89ffbd83eea475e5db866ca3087e973b5becf3" +content-hash = "ae025205756012ce341fd3aecad1a22024336eb23af3e39c5c4ba8cb34d17476" diff --git a/libs/gigachat/pyproject.toml b/libs/gigachat/pyproject.toml index 8c51594..c97ebd9 100644 --- a/libs/gigachat/pyproject.toml +++ b/libs/gigachat/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langchain-gigachat" -version = "0.3.0" +version = "0.3.1" description = "An integration package connecting GigaChat and LangChain" authors = [] readme = "README.md" @@ -13,7 +13,8 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9,<4.0" langchain-core = "^0.3" -gigachat = "^0.1.35" +gigachat = "^0.1.36" +types-requests = "^2.32" [tool.poetry.group.dev] optional = true @@ -41,6 +42,7 @@ pytest = "^8.3.3" pytest-cov = "^5.0.0" pytest-asyncio = "^0.24.0" pytest-mock = "^3.14.0" +requests_mock = "^1.12.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/libs/gigachat/tests/unit_tests/test_gigachat.py b/libs/gigachat/tests/unit_tests/test_gigachat.py index b56c178..204eab4 100644 --- a/libs/gigachat/tests/unit_tests/test_gigachat.py +++ b/libs/gigachat/tests/unit_tests/test_gigachat.py @@ -31,7 +31,7 @@ _convert_dict_to_message, _convert_message_to_dict, ) -from langchain_gigachat.tools.giga_tool import giga_tool +from langchain_gigachat.tools.giga_tool import FewShotExamples, giga_tool from tests.unit_tests.stubs import FakeAsyncCallbackHandler, FakeCallbackHandler @@ -334,3 +334,44 @@ def test_gigachat_bind_gigatool() -> None: "required": ["status", "message"], "type": "object", } + + +class SomeResult(BaseModel): + """My desc""" + + @staticmethod + def few_shot_examples() -> FewShotExamples: + return [ + { + "request": "request example", + "params": {"is_valid": 1, "description": "correct message"}, + } + ] + + value: int = Field(description="some value") + description: str = Field(description="some descriptin") + + +def test_structured_output() -> None: + llm = GigaChat().with_structured_output(SomeResult) + assert llm.steps[0].kwargs["function_call"] == {"name": "SomeResult"} # type: ignore[attr-defined] + assert llm.steps[0].kwargs["tools"][0]["function"] == { # type: ignore[attr-defined] + "name": "SomeResult", + "description": "My desc", + "parameters": { + "description": "My desc", + "properties": { + "value": {"description": "some value", "type": "integer"}, + "description": {"description": "some descriptin", "type": "string"}, + }, + "required": ["value", "description"], + "type": "object", + }, + "return_parameters": None, + "few_shot_examples": [ + { + "request": "request example", + "params": {"is_valid": 1, "description": "correct message"}, + } + ], + } diff --git a/libs/gigachat/tests/unit_tests/test_utils.py b/libs/gigachat/tests/unit_tests/test_utils.py new file mode 100644 index 0000000..4f27cae --- /dev/null +++ b/libs/gigachat/tests/unit_tests/test_utils.py @@ -0,0 +1,31 @@ +from typing import Generator + +import pytest +import requests_mock +from langchain_core.prompts.prompt import PromptTemplate + +from langchain_gigachat.tools.load_prompt import load_from_giga_hub + + +@pytest.fixture +def mock_requests_get() -> Generator: + with requests_mock.Mocker() as mocker: + mocker.get( + "https://raw.githubusercontent.com/ai-forever/gigachain/master/hub/prompts/entertainment/meditation.yaml", + text=( + "input_variables: [background, topic]\n" + "output_parser: null\n" + "template: 'Create mediation for {topic} with {background}'\n" + "template_format: f-string\n" + "_type: prompt" + ), + ) + yield mocker + + +def test__load_from_giga_hub(mock_requests_get: Generator) -> None: + template = load_from_giga_hub("lc://prompts/entertainment/meditation.yaml") + assert isinstance(template, PromptTemplate) + assert template.template == "Create mediation for {topic} with {background}" + assert "background" in template.input_variables + assert "topic" in template.input_variables