Skip to content

Commit 6086292

Browse files
authored
Centralize logic for loading from LangChainHub, add ability to pin dependencies (#805)
It's generally considered to be a good practice to pin dependencies to prevent surprise breakages when a new version of a dependency is released. This commit adds the ability to pin dependencies when loading from LangChainHub. Centralizing this logic and using urllib fixes an issue identified by some windows users highlighted in this video - https://youtu.be/aJ6IQUh8MLQ?t=537
1 parent b3916f7 commit 6086292

File tree

9 files changed

+201
-68
lines changed

9 files changed

+201
-68
lines changed

docs/modules/agents/examples/load_from_hub.ipynb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,26 @@
6262
"self_ask_with_search.run(\"What is the hometown of the reigning men's U.S. Open champion?\")"
6363
]
6464
},
65+
{
66+
"attachments": {},
67+
"cell_type": "markdown",
68+
"id": "3aede965",
69+
"metadata": {},
70+
"source": [
71+
"# Pinning Dependencies\n",
72+
"\n",
73+
"Specific versions of LangChainHub agents can be pinned with the `lc@<ref>://` syntax."
74+
]
75+
},
6576
{
6677
"cell_type": "code",
6778
"execution_count": null,
6879
"id": "e679f7b6",
6980
"metadata": {},
7081
"outputs": [],
71-
"source": []
82+
"source": [
83+
"self_ask_with_search = initialize_agent(tools, llm, agent_path=\"lc@2826ef9e8acdf88465e1e5fc8a7bf59e0f9d0a85://agents/self-ask-with-search/agent.json\", verbose=True)"
84+
]
7285
}
7386
],
7487
"metadata": {

langchain/agents/loading.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
"""Functionality for loading agents."""
22
import json
3-
import os
4-
import tempfile
53
from pathlib import Path
64
from typing import Any, List, Optional, Union
75

8-
import requests
96
import yaml
107

118
from langchain.agents.agent import Agent
@@ -16,6 +13,7 @@
1613
from langchain.agents.tools import Tool
1714
from langchain.chains.loading import load_chain, load_chain_from_config
1815
from langchain.llms.base import BaseLLM
16+
from langchain.utilities.loading import try_load_from_hub
1917

2018
AGENT_TO_CLASS = {
2119
"zero-shot-react-description": ZeroShotAgent,
@@ -81,29 +79,14 @@ def load_agent_from_config(
8179

8280
def load_agent(path: Union[str, Path], **kwargs: Any) -> Agent:
8381
"""Unified method for loading a agent from LangChainHub or local fs."""
84-
if isinstance(path, str) and path.startswith("lc://agents"):
85-
path = os.path.relpath(path, "lc://agents/")
86-
return _load_from_hub(path, **kwargs)
82+
if hub_result := try_load_from_hub(
83+
path, _load_agent_from_file, "agents", {"json", "yaml"}
84+
):
85+
return hub_result
8786
else:
8887
return _load_agent_from_file(path, **kwargs)
8988

9089

91-
def _load_from_hub(path: str, **kwargs: Any) -> Agent:
92-
"""Load agent from hub."""
93-
suffix = path.split(".")[-1]
94-
if suffix not in {"json", "yaml"}:
95-
raise ValueError("Unsupported file type.")
96-
full_url = URL_BASE + path
97-
r = requests.get(full_url)
98-
if r.status_code != 200:
99-
raise ValueError(f"Could not find file at {full_url}")
100-
with tempfile.TemporaryDirectory() as tmpdirname:
101-
file = tmpdirname + "/agent." + suffix
102-
with open(file, "wb") as f:
103-
f.write(r.content)
104-
return _load_agent_from_file(file, **kwargs)
105-
106-
10790
def _load_agent_from_file(file: Union[str, Path], **kwargs: Any) -> Agent:
10891
"""Load agent from file."""
10992
# Convert file to Path object.

langchain/chains/loading.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
"""Functionality for loading chains."""
22
import json
3-
import os
4-
import tempfile
53
from pathlib import Path
64
from typing import Any, Union
75

8-
import requests
96
import yaml
107

118
from langchain.chains.api.base import APIChain
@@ -27,6 +24,7 @@
2724
from langchain.chains.vector_db_qa.base import VectorDBQA
2825
from langchain.llms.loading import load_llm, load_llm_from_config
2926
from langchain.prompts.loading import load_prompt, load_prompt_from_config
27+
from langchain.utilities.loading import try_load_from_hub
3028

3129
URL_BASE = "https://raw.githubusercontent.com/hwchase17/langchain-hub/master/chains/"
3230

@@ -441,9 +439,10 @@ def load_chain_from_config(config: dict, **kwargs: Any) -> Chain:
441439

442440
def load_chain(path: Union[str, Path], **kwargs: Any) -> Chain:
443441
"""Unified method for loading a chain from LangChainHub or local fs."""
444-
if isinstance(path, str) and path.startswith("lc://chains"):
445-
path = os.path.relpath(path, "lc://chains/")
446-
return _load_from_hub(path, **kwargs)
442+
if hub_result := try_load_from_hub(
443+
path, _load_chain_from_file, "chains", {"json", "yaml"}
444+
):
445+
return hub_result
447446
else:
448447
return _load_chain_from_file(path, **kwargs)
449448

@@ -466,19 +465,3 @@ def _load_chain_from_file(file: Union[str, Path], **kwargs: Any) -> Chain:
466465
raise ValueError("File type must be json or yaml")
467466
# Load the chain from the config now.
468467
return load_chain_from_config(config, **kwargs)
469-
470-
471-
def _load_from_hub(path: str, **kwargs: Any) -> Chain:
472-
"""Load chain from hub."""
473-
suffix = path.split(".")[-1]
474-
if suffix not in {"json", "yaml"}:
475-
raise ValueError("Unsupported file type.")
476-
full_url = URL_BASE + path
477-
r = requests.get(full_url)
478-
if r.status_code != 200:
479-
raise ValueError(f"Could not find file at {full_url}")
480-
with tempfile.TemporaryDirectory() as tmpdirname:
481-
file = tmpdirname + "/chain." + suffix
482-
with open(file, "wb") as f:
483-
f.write(r.content)
484-
return _load_chain_from_file(file, **kwargs)

langchain/prompts/loading.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"""Load prompts from disk."""
22
import importlib
33
import json
4-
import os
5-
import tempfile
64
from pathlib import Path
75
from typing import Union
86

9-
import requests
107
import yaml
118

129
from langchain.prompts.base import BasePromptTemplate, RegexParser
1310
from langchain.prompts.few_shot import FewShotPromptTemplate
1411
from langchain.prompts.prompt import PromptTemplate
12+
from langchain.utilities.loading import try_load_from_hub
1513

1614
URL_BASE = "https://raw.githubusercontent.com/hwchase17/langchain-hub/master/prompts/"
1715

@@ -114,9 +112,10 @@ def _load_prompt(config: dict) -> PromptTemplate:
114112

115113
def load_prompt(path: Union[str, Path]) -> BasePromptTemplate:
116114
"""Unified method for loading a prompt from LangChainHub or local fs."""
117-
if isinstance(path, str) and path.startswith("lc://prompts"):
118-
path = os.path.relpath(path, "lc://prompts/")
119-
return _load_from_hub(path)
115+
if hub_result := try_load_from_hub(
116+
path, _load_prompt_from_file, "prompts", {"py", "json", "yaml"}
117+
):
118+
return hub_result
120119
else:
121120
return _load_prompt_from_file(path)
122121

@@ -151,19 +150,3 @@ def _load_prompt_from_file(file: Union[str, Path]) -> BasePromptTemplate:
151150
raise ValueError(f"Got unsupported file type {file_path.suffix}")
152151
# Load the prompt from the config now.
153152
return load_prompt_from_config(config)
154-
155-
156-
def _load_from_hub(path: str) -> BasePromptTemplate:
157-
"""Load prompt from hub."""
158-
suffix = path.split(".")[-1]
159-
if suffix not in {"py", "json", "yaml"}:
160-
raise ValueError("Unsupported file type.")
161-
full_url = URL_BASE + path
162-
r = requests.get(full_url)
163-
if r.status_code != 200:
164-
raise ValueError(f"Could not find file at {full_url}")
165-
with tempfile.TemporaryDirectory() as tmpdirname:
166-
file = tmpdirname + "/prompt." + suffix
167-
with open(file, "wb") as f:
168-
f.write(r.content)
169-
return _load_prompt_from_file(file)

langchain/utilities/loading.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Utilities for loading configurations from langchian-hub."""
2+
3+
import os
4+
import re
5+
import tempfile
6+
from pathlib import Path
7+
from typing import Any, Callable, Optional, Set, TypeVar, Union
8+
from urllib.parse import urljoin
9+
10+
import requests
11+
12+
DEFAULT_REF = os.environ.get("LANGCHAIN_HUB_DEFAULT_REF", "master")
13+
URL_BASE = os.environ.get(
14+
"LANGCHAIN_HUB_URL_BASE",
15+
"https://raw.githubusercontent.com/hwchase17/langchain-hub/{ref}/",
16+
)
17+
HUB_PATH_RE = re.compile(r"lc(?P<ref>@[^:]+)?://(?P<path>.*)")
18+
19+
20+
T = TypeVar("T")
21+
22+
23+
def try_load_from_hub(
24+
path: Union[str, Path],
25+
loader: Callable[[str], T],
26+
valid_prefix: str,
27+
valid_suffixes: Set[str],
28+
**kwargs: Any,
29+
) -> Optional[T]:
30+
"""Load configuration from hub. Returns None if path is not a hub path."""
31+
if not isinstance(path, str) or not (match := HUB_PATH_RE.match(path)):
32+
return None
33+
ref, remote_path_str = match.groups()
34+
ref = ref[1:] if ref else DEFAULT_REF
35+
remote_path = Path(remote_path_str)
36+
if remote_path.parts[0] != valid_prefix:
37+
return None
38+
if remote_path.suffix[1:] not in valid_suffixes:
39+
raise ValueError("Unsupported file type.")
40+
41+
full_url = urljoin(URL_BASE.format(ref=ref), str(remote_path))
42+
r = requests.get(full_url, timeout=5)
43+
if r.status_code != 200:
44+
raise ValueError(f"Could not find file at {full_url}")
45+
with tempfile.TemporaryDirectory() as tmpdirname:
46+
file = Path(tmpdirname) / remote_path.name
47+
with open(file, "wb") as f:
48+
f.write(r.content)
49+
return loader(str(file), **kwargs)

poetry.lock

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pytest-dotenv = "^0.5.2"
5757
duckdb-engine = "^0.6.6"
5858
pytest-watcher = "^0.2.6"
5959
freezegun = "^1.2.2"
60+
responses = "^0.22.0"
6061

6162
[tool.poetry.group.lint.dependencies]
6263
flake8-docstrings = "^1.6.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests utilities module."""

0 commit comments

Comments
 (0)