langchain-dev-utils 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ from .has_tool_calling import has_tool_calling
2
+ from .content import (
3
+ convert_reasoning_content_for_ai_message,
4
+ convert_reasoning_content_for_chunk_iterator,
5
+ aconvert_reasoning_content_for_ai_message,
6
+ aconvert_reasoning_content_for_chunk_iterator,
7
+ )
8
+ from .embbedings import load_embeddings, register_embeddings_provider
9
+ from .chat_model import load_chat_model, register_model_provider
10
+
11
+ __all__ = [
12
+ "has_tool_calling",
13
+ "convert_reasoning_content_for_ai_message",
14
+ "convert_reasoning_content_for_chunk_iterator",
15
+ "aconvert_reasoning_content_for_ai_message",
16
+ "aconvert_reasoning_content_for_chunk_iterator",
17
+ "load_embeddings",
18
+ "register_embeddings_provider",
19
+ "load_chat_model",
20
+ "register_model_provider",
21
+ ]
22
+
23
+
24
+ __version__ = "0.1.0"
@@ -0,0 +1,136 @@
1
+ import os
2
+ from typing import Any, Optional, Union, cast
3
+
4
+ from langchain.chat_models.base import (
5
+ BaseChatModel,
6
+ _SUPPORTED_PROVIDERS,
7
+ init_chat_model,
8
+ _init_chat_model_helper,
9
+ )
10
+
11
+
12
+ _MODEL_PROVIDERS_DICT = {}
13
+
14
+
15
+ def _parse_model(model: str, model_provider: Optional[str]) -> tuple[str, str]:
16
+ """Parse model string and provider.
17
+
18
+ Args:
19
+ model: Model name string, potentially including provider prefix
20
+ model_provider: Optional provider name
21
+
22
+ Returns:
23
+ Tuple of (model_name, provider_name)
24
+
25
+ Raises:
26
+ ValueError: If unable to infer model provider
27
+ """
28
+ if (
29
+ not model_provider
30
+ and ":" in model
31
+ and model.split(":")[0] in _MODEL_PROVIDERS_DICT
32
+ ):
33
+ model_provider = model.split(":")[0]
34
+ model = ":".join(model.split(":")[1:])
35
+ if not model_provider:
36
+ msg = (
37
+ f"Unable to infer model provider for {model=}, please specify "
38
+ f"model_provider directly."
39
+ )
40
+ raise ValueError(msg)
41
+ model_provider = model_provider.replace("-", "_").lower()
42
+ return model, model_provider
43
+
44
+
45
+ def _load_chat_model_helper(
46
+ model: str,
47
+ model_provider: Optional[str] = None,
48
+ **kwargs: Any,
49
+ ) -> BaseChatModel:
50
+ """Helper function to load chat model.
51
+
52
+ Args:
53
+ model: Model name
54
+ model_provider: Optional provider name
55
+ **kwargs: Additional arguments for model initialization
56
+
57
+ Returns:
58
+ BaseChatModel: Initialized chat model instance
59
+ """
60
+ model, model_provider = _parse_model(model, model_provider)
61
+ if model_provider in _MODEL_PROVIDERS_DICT.keys():
62
+ chat_model = _MODEL_PROVIDERS_DICT[model_provider]["chat_model"]
63
+ if isinstance(chat_model, str):
64
+ if not (api_key := kwargs.get("api_key")):
65
+ api_key = os.getenv(f"{model_provider.upper()}_API_KEY")
66
+ if not api_key:
67
+ raise ValueError(
68
+ f"API key for {model_provider} not found. Please set it in the environment."
69
+ )
70
+ kwargs["api_key"] = api_key
71
+ base_url = _MODEL_PROVIDERS_DICT[model_provider]["base_url"]
72
+ return init_chat_model(
73
+ model=model,
74
+ model_provider=chat_model,
75
+ base_url=base_url,
76
+ **kwargs,
77
+ )
78
+ else:
79
+ return chat_model(model=model, **kwargs)
80
+
81
+ return _init_chat_model_helper(model, model_provider=model_provider, **kwargs)
82
+
83
+
84
+ def register_model_provider(
85
+ provider_name: str,
86
+ chat_model: Union[type[BaseChatModel], str],
87
+ base_url: Optional[str] = None,
88
+ ):
89
+ """Register a new model provider.
90
+
91
+ Args:
92
+ provider_name: Name of the provider to register
93
+ chat_model: Either a BaseChatModel class or a string identifier for a supported provider
94
+ base_url: Optional base URL for API endpoints (required when chat_model is a string)
95
+
96
+ Raises:
97
+ ValueError: If base_url is not provided when chat_model is a string,
98
+ or if chat_model string is not in supported providers
99
+ """
100
+ if isinstance(chat_model, str):
101
+ if base_url is None:
102
+ raise ValueError("base_url must be provided when chat_model is a string")
103
+
104
+ if chat_model not in _SUPPORTED_PROVIDERS:
105
+ raise ValueError(
106
+ f"when chat_model is a string, the value must be one of {_SUPPORTED_PROVIDERS}"
107
+ )
108
+
109
+ _MODEL_PROVIDERS_DICT.update(
110
+ {provider_name: {"chat_model": chat_model, "base_url": base_url}}
111
+ )
112
+ else:
113
+ _MODEL_PROVIDERS_DICT.update({provider_name: {"chat_model": chat_model}})
114
+
115
+
116
+ def load_chat_model(
117
+ model: Optional[str] = None,
118
+ *,
119
+ model_provider: Optional[str] = None,
120
+ **kwargs: Any,
121
+ ) -> BaseChatModel:
122
+ """Load a chat model.
123
+
124
+ Args:
125
+ model: Model name
126
+ model_provider: Optional provider name
127
+ **kwargs: Additional arguments for model initialization
128
+
129
+ Returns:
130
+ BaseChatModel: Initialized chat model instance
131
+ """
132
+ return _load_chat_model_helper(
133
+ cast(str, model),
134
+ model_provider=model_provider,
135
+ **kwargs,
136
+ )
@@ -0,0 +1,121 @@
1
+ from typing import AsyncIterator, Iterator, Tuple
2
+
3
+ from langchain_core.messages import (
4
+ AIMessage,
5
+ AIMessageChunk,
6
+ BaseMessageChunk,
7
+ )
8
+
9
+
10
+ def convert_reasoning_content_for_ai_message(
11
+ model_response: AIMessage,
12
+ think_tag: Tuple[str, str] = ("", ""),
13
+ ) -> AIMessage:
14
+ """Convert reasoning content in AI message to visible content.
15
+
16
+ Args:
17
+ model_response: AI message response from model
18
+ think_tag: Tuple of (opening_tag, closing_tag) to wrap reasoning content
19
+
20
+ Returns:
21
+ AIMessage: Modified AI message with reasoning content in visible content
22
+ """
23
+ if "reasoning_content" in model_response.additional_kwargs:
24
+ model_response.content = f"{think_tag[0]}{model_response.additional_kwargs['reasoning_content']}{think_tag[1]}"
25
+ return model_response
26
+
27
+
28
+ def convert_reasoning_content_for_chunk_iterator(
29
+ model_response: Iterator[BaseMessageChunk],
30
+ think_tag: Tuple[str, str] = ("", ""),
31
+ ) -> Iterator[BaseMessageChunk]:
32
+ """Convert reasoning content for streaming response chunks.
33
+
34
+ Args:
35
+ model_response: Iterator of message chunks from streaming response
36
+ think_tag: Tuple of (opening_tag, closing_tag) to wrap reasoning content
37
+
38
+ Yields:
39
+ BaseMessageChunk: Modified message chunks with reasoning content
40
+ """
41
+ isfirst = True
42
+ isend = True
43
+
44
+ for chunk in model_response:
45
+ if (
46
+ isinstance(chunk, AIMessageChunk)
47
+ and "reasoning_content" in chunk.additional_kwargs
48
+ ):
49
+ if isfirst:
50
+ chunk.content = (
51
+ f"{think_tag[0]}{chunk.additional_kwargs['reasoning_content']}"
52
+ )
53
+ isfirst = False
54
+ else:
55
+ chunk.content = chunk.additional_kwargs["reasoning_content"]
56
+ elif (
57
+ isinstance(chunk, AIMessageChunk)
58
+ and "reasoning_content" not in chunk.additional_kwargs
59
+ and chunk.content
60
+ and isend
61
+ ):
62
+ chunk.content = f"{think_tag[1]}{chunk.content}"
63
+ isend = False
64
+ yield chunk
65
+
66
+
67
+ async def aconvert_reasoning_content_for_ai_message(
68
+ model_response: AIMessage,
69
+ think_tag: Tuple[str, str] = ("", ""),
70
+ ) -> AIMessage:
71
+ """Async convert reasoning content in AI message to visible content.
72
+
73
+ Args:
74
+ model_response: AI message response from model
75
+ think_tag: Tuple of (opening_tag, closing_tag) to wrap reasoning content
76
+
77
+ Returns:
78
+ AIMessage: Modified AI message with reasoning content in visible content
79
+ """
80
+ if "reasoning_content" in model_response.additional_kwargs:
81
+ model_response.content = f"{think_tag[0]}{model_response.additional_kwargs['reasoning_content']}{think_tag[1]}"
82
+ return model_response
83
+
84
+
85
+ async def aconvert_reasoning_content_for_chunk_iterator(
86
+ amodel_response: AsyncIterator[BaseMessageChunk],
87
+ think_tag: Tuple[str, str] = ("", ""),
88
+ ) -> AsyncIterator[BaseMessageChunk]:
89
+ """Async convert reasoning content for streaming response chunks.
90
+
91
+ Args:
92
+ amodel_response: Async iterator of message chunks from streaming response
93
+ think_tag: Tuple of (opening_tag, closing_tag) to wrap reasoning content
94
+
95
+ Yields:
96
+ BaseMessageChunk: Modified message chunks with reasoning content
97
+ """
98
+ isfirst = True
99
+ isend = True
100
+
101
+ async for chunk in amodel_response:
102
+ if (
103
+ isinstance(chunk, AIMessageChunk)
104
+ and "reasoning_content" in chunk.additional_kwargs
105
+ ):
106
+ if isfirst:
107
+ chunk.content = (
108
+ f"{think_tag[0]}{chunk.additional_kwargs['reasoning_content']}"
109
+ )
110
+ isfirst = False
111
+ else:
112
+ chunk.content = chunk.additional_kwargs["reasoning_content"]
113
+ elif (
114
+ isinstance(chunk, AIMessageChunk)
115
+ and "reasoning_content" not in chunk.additional_kwargs
116
+ and chunk.content
117
+ and isend
118
+ ):
119
+ chunk.content = f"{think_tag[1]}{chunk.content}"
120
+ isend = False
121
+ yield chunk
@@ -0,0 +1,121 @@
1
+ import os
2
+ from typing import Any, Optional, Union
3
+ from langchain.embeddings.base import init_embeddings, _SUPPORTED_PROVIDERS, Embeddings
4
+ from langchain_core.runnables import Runnable
5
+
6
+
7
+ _EMBEDDINGS_PROVIDERS_DICT = {}
8
+
9
+
10
+ def _parse_model_string(model_name: str) -> tuple[str, str]:
11
+ """Parse model string into provider and model name.
12
+
13
+ Args:
14
+ model_name: Model name string in format 'provider:model-name'
15
+
16
+ Returns:
17
+ Tuple of (provider, model) parsed from the model_name
18
+
19
+ Raises:
20
+ ValueError: If model name format is invalid or model name is empty
21
+ """
22
+ if ":" not in model_name:
23
+ msg = (
24
+ f"Invalid model format '{model_name}'.\n"
25
+ f"Model name must be in format 'provider:model-name'\n"
26
+ )
27
+ raise ValueError(msg)
28
+
29
+ provider, model = model_name.split(":", 1)
30
+ provider = provider.lower().strip()
31
+ model = model.strip()
32
+ if not model:
33
+ msg = "Model name cannot be empty"
34
+ raise ValueError(msg)
35
+ return provider, model
36
+
37
+
38
+ def register_embeddings_provider(
39
+ provider_name: str,
40
+ embeddings_model: Union[type[Embeddings], str],
41
+ base_url: Optional[str] = None,
42
+ ):
43
+ """Register an embeddings provider.
44
+
45
+ Args:
46
+ provider_name: Name of the provider to register
47
+ embeddings_model: Either an Embeddings class or a string identifier for a supported provider
48
+ base_url: Optional base URL for API endpoints (required when embeddings_model is a string)
49
+
50
+ Raises:
51
+ ValueError: If base_url is not provided when embeddings_model is a string
52
+ """
53
+ if isinstance(embeddings_model, str):
54
+ if base_url is None:
55
+ raise ValueError(
56
+ "base_url must be provided when embeddings_model is a string"
57
+ )
58
+
59
+ if embeddings_model not in _SUPPORTED_PROVIDERS:
60
+ raise ValueError(
61
+ f"when embeddings_model is a string, the value must be one of {_SUPPORTED_PROVIDERS}"
62
+ )
63
+
64
+ _EMBEDDINGS_PROVIDERS_DICT.update(
65
+ {
66
+ provider_name: {
67
+ "embeddings_model": embeddings_model,
68
+ "base_url": base_url,
69
+ }
70
+ }
71
+ )
72
+ else:
73
+ _EMBEDDINGS_PROVIDERS_DICT.update(
74
+ {provider_name: {"embeddings_model": embeddings_model}}
75
+ )
76
+
77
+
78
+ def load_embeddings(
79
+ model: str,
80
+ *,
81
+ provider: Optional[str] = None,
82
+ **kwargs: Any,
83
+ ) -> Union[Embeddings, Runnable[Any, list[float]]]:
84
+ """Load embeddings model.
85
+
86
+ Args:
87
+ model: Model name in format 'provider:model-name' if provider not specified separately
88
+ provider: Optional provider name (if not included in model parameter)
89
+ **kwargs: Additional arguments for model initialization
90
+
91
+ Returns:
92
+ Union[Embeddings, Runnable[Any, list[float]]]: Initialized embeddings model instance
93
+
94
+ Raises:
95
+ ValueError: If provider is not registered or API key is not found
96
+ """
97
+ if provider is None:
98
+ provider, model = _parse_model_string(model)
99
+ if provider not in _EMBEDDINGS_PROVIDERS_DICT:
100
+ raise ValueError(f"Provider {provider} not registered")
101
+
102
+ embeddings = _EMBEDDINGS_PROVIDERS_DICT[provider]["embeddings_model"]
103
+ if isinstance(embeddings, str):
104
+ if not (api_key := kwargs.get("api_key")):
105
+ api_key = os.getenv(f"{provider.upper()}_API_KEY")
106
+ if not api_key:
107
+ raise ValueError(
108
+ f"API key for {provider} not found. Please set it in the environment."
109
+ )
110
+ kwargs["api_key"] = api_key
111
+ if embeddings == "openai":
112
+ kwargs["check_embedding_ctx_length"] = False
113
+
114
+ return init_embeddings(
115
+ model=model,
116
+ provider=embeddings,
117
+ base_url=_EMBEDDINGS_PROVIDERS_DICT[provider]["base_url"],
118
+ **kwargs,
119
+ )
120
+ else:
121
+ return embeddings(model=model, **kwargs)
@@ -0,0 +1,19 @@
1
+ from langchain_core.messages import AIMessage, AnyMessage
2
+
3
+
4
+ def has_tool_calling(message: AnyMessage):
5
+ """Check if a message contains tool calls.
6
+
7
+ Args:
8
+ message: Any message type to check for tool calls
9
+
10
+ Returns:
11
+ bool: True if message is an AIMessage with tool calls, False otherwise
12
+ """
13
+ if (
14
+ isinstance(message, AIMessage)
15
+ and hasattr(message, "tool_calls")
16
+ and len(message.tool_calls) > 0
17
+ ):
18
+ return True
19
+ return False
File without changes
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-dev-utils
3
+ Version: 0.1.0
4
+ Summary: A practical utility library for LangChain and LangGraph development
5
+ Project-URL: Source Code, https://github.com/TBice123123/langchain-dev-utils
6
+ Project-URL: repository, https://github.com/TBice123123/langchain-dev-utils
7
+ Author-email: tiebingice <tiebingice123@outlook.com>
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: langchain>=0.3.27
10
+ Requires-Dist: langgraph>=0.6.6
11
+ Description-Content-Type: text/markdown
12
+
13
+ # LangChain Dev Utils
14
+
15
+ [中文文档](https://github.com/TBice123123/langchain-dev-utils/blob/master/README_cn.md)
16
+
17
+ This toolkit is designed to provide encapsulated utility tools for developers using LangChain and LangGraph to develop large language model applications, helping developers work more efficiently.
18
+
19
+ ## Installation and Usage
20
+
21
+ 1. Using pip
22
+
23
+ ```bash
24
+ pip install -U langchain-dev-utils
25
+ ```
26
+
27
+ 2. Using poetry
28
+
29
+ ```bash
30
+ poetry add langchain-dev-utils
31
+ ```
32
+
33
+ 3. Using uv
34
+
35
+ ```bash
36
+ uv add langchain-dev-utils
37
+ ```
38
+
39
+ ## Function Modules
40
+
41
+ ### 1. Extended Model Loading Functionality
42
+
43
+ While the official `init_chat_model` function is very useful, it has limited support for model providers. This toolkit provides extended model loading functionality that allows registration and use of more model providers.
44
+
45
+ #### Core Functions
46
+
47
+ - `register_model_provider`: Register a model provider
48
+ - `load_chat_model`: Load a chat model
49
+
50
+ #### `register_model_provider` Parameter Description
51
+
52
+ - `provider_name`: Provider name, requires a custom name
53
+ - `chat_model`: ChatModel class or string. If it's a string, it must be a provider supported by the official `init_chat_model` (e.g., `openai`, `anthropic`). In this case, the `init_chat_model` function will be called
54
+ - `base_url`: Optional base URL. Recommended when `chat_model` is a string
55
+
56
+ #### Usage Example
57
+
58
+ ```python
59
+ from langchain_dev_utils.chat_model import register_model_provider, load_chat_model
60
+ from langchain_qwq import ChatQwen
61
+ from dotenv import load_dotenv
62
+
63
+ load_dotenv()
64
+
65
+ # Register custom model providers
66
+ register_model_provider("dashscope", ChatQwen)
67
+ register_model_provider("openrouter", "openai", base_url="https://openrouter.ai/api/v1")
68
+
69
+ # Load models
70
+ model = load_chat_model(model="dashscope:qwen-flash")
71
+ print(model.invoke("Hello!"))
72
+
73
+ model = load_chat_model(model="openrouter:moonshotai/kimi-k2-0905")
74
+ print(model.invoke("Hello!"))
75
+ ```
76
+
77
+ **Note**: Since the underlying implementation of the function is a global dictionary, **all model providers must be registered at application startup**. Modifications should not be made at runtime, otherwise multi-threading concurrency synchronization issues may occur.
78
+
79
+ ### 2. Reasoning Content Processing Functionality
80
+
81
+ Provides utility functions for processing model reasoning content, supporting both synchronous and asynchronous operations.
82
+
83
+ #### Core Functions
84
+
85
+ - `convert_reasoning_content_for_ai_message`: Convert reasoning content for a single AI message
86
+ - `convert_reasoning_content_for_chunk_iterator`: Convert reasoning content for streaming response message chunk iterator
87
+ - `aconvert_reasoning_content_for_ai_message`: Asynchronously convert reasoning content for a single AI message
88
+ - `aconvert_reasoning_content_for_chunk_iterator`: Asynchronously convert reasoning content for streaming response message chunk iterator
89
+
90
+ #### Usage Example
91
+
92
+ ```python
93
+ # Synchronously process reasoning content
94
+ from langchain_dev_utils.content import convert_reasoning_content_for_ai_message
95
+
96
+ response = model.invoke("Please solve this math problem")
97
+ converted_response = convert_reasoning_content_for_ai_message(response, think_tag=("", ""))
98
+
99
+ # Stream processing reasoning content
100
+ from langchain_dev_utils.content import convert_reasoning_content_for_chunk_iterator
101
+
102
+ for chunk in convert_reasoning_content_for_chunk_iterator(model.stream("Please solve this math problem"), think_tag=("", "")):
103
+ print(chunk.content, end="", flush=True)
104
+ ```
105
+
106
+ ### 3. Embeddings Model Loading Functionality
107
+
108
+ Provides extended embeddings model loading functionality, similar to the model loading functionality.
109
+
110
+ #### Core Functions
111
+
112
+ - `register_embeddings_provider`: Register an embeddings model provider
113
+ - `load_embeddings`: Load an embeddings model
114
+
115
+ #### Usage Example
116
+
117
+ ```python
118
+ from langchain_dev_utils.embbedings import register_embeddings_provider, load_embeddings
119
+
120
+ # Register embeddings model provider
121
+ register_embeddings_provider("openai", "openai", base_url="https://api.openai.com/v1")
122
+
123
+ # Load embeddings model
124
+ embeddings = load_embeddings("openai:text-embedding-ada-002")
125
+ ```
126
+
127
+ ### 4. Tool Calling Detection Functionality
128
+
129
+ Provides a simple function to detect whether a message contains tool calls.
130
+
131
+ #### Core Functions
132
+
133
+ - `has_tool_calling`: Detect whether a message contains tool calls
134
+
135
+ #### Usage Example
136
+
137
+ ```python
138
+ from langchain_dev_utils.has_tool_calling import has_tool_calling
139
+
140
+ if has_tool_calling(message):
141
+ # Handle tool calling logic
142
+ pass
143
+ ```
144
+
145
+ ## Test
146
+
147
+ All the current tool functions in this project have been tested, and you can also clone this project for testing.
148
+
149
+ ```bash
150
+ git clone https://github.com/TBice123123/langchain-dev-utils.git
151
+ ```
152
+
153
+ ```bash
154
+ cd langchain-dev-utils
155
+ ```
156
+
157
+ ```bash
158
+ uv sync --group test
159
+ ```
160
+
161
+ ```bash
162
+ uv run pytest .
163
+ ```
@@ -0,0 +1,9 @@
1
+ langchain_dev_utils/__init__.py,sha256=v9MXwLR7iSJcZNV9rl7HfbqkMztYBfCoabT-hNiGdAE,779
2
+ langchain_dev_utils/chat_model.py,sha256=HeihWUOJS1iRcjMTshbVVWeUcdjuHmfjG5aqL85SWh4,4402
3
+ langchain_dev_utils/content.py,sha256=2FdyiFiqtEmCmIJ5n9EtxF-Z9cTj5cT7DPPxkU1CPiQ,4274
4
+ langchain_dev_utils/embbedings.py,sha256=frq6WxYcPRRVTvfTHBqqu6xLmM2_03emmyDrkZxoDkE,4138
5
+ langchain_dev_utils/has_tool_calling.py,sha256=w3JP7uc5khmAjBTH63rGXUWh6jrMX1ikg9FmEt8wdOc,524
6
+ langchain_dev_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ langchain_dev_utils-0.1.0.dist-info/METADATA,sha256=yy1zkuW2SEI2cpfCuXEyNsIYA4m1oxyMc3dGZEQmf9w,5225
8
+ langchain_dev_utils-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ langchain_dev_utils-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any