Compare commits

..

2 Commits

Author SHA1 Message Date
Eugene Yurtsev
568e3107da x 2026-03-06 14:20:43 -05:00
Eugene Yurtsev
f68e25c41d fix(langchain): suppress mypy errors in langchain_classic for deprecated/removed openai APIs and fallback imports 2026-03-06 14:17:16 -05:00
10 changed files with 20 additions and 78 deletions

View File

@@ -41,7 +41,9 @@ class MRKLOutputParser(AgentOutputParser):
OutputParserException: If the output could not be parsed.
"""
includes_answer = FINAL_ANSWER_ACTION in text
regex = r"Action\s*\d*\s*:[\s]*(.*?)Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
regex = (
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
)
action_match = re.search(regex, text, re.DOTALL)
if action_match and includes_answer:
if text.find(FINAL_ANSWER_ACTION) < text.find(action_match.group(0)):

View File

@@ -21,7 +21,7 @@ from typing_extensions import Self, override
if TYPE_CHECKING:
import openai
from openai.types.beta.threads import ThreadMessage
from openai.types.beta.threads import ThreadMessage # type: ignore[attr-defined]
from openai.types.beta.threads.required_action_function_tool_call import (
RequiredActionFunctionToolCall,
)
@@ -276,10 +276,10 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]):
OpenAIAssistantRunnable configured to run using the created assistant.
"""
client = client or _get_openai_client()
assistant = client.beta.assistants.create(
assistant = client.beta.assistants.create( # type: ignore[deprecated]
name=name,
instructions=instructions,
tools=[_get_assistants_tool(tool) for tool in tools],
tools=[_get_assistants_tool(tool) for tool in tools], # type: ignore[misc]
model=model,
)
return cls(assistant_id=assistant.id, client=client, **kwargs)
@@ -409,10 +409,10 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]):
"""
async_client = async_client or _get_openai_async_client()
openai_tools = [_get_assistants_tool(tool) for tool in tools]
assistant = await async_client.beta.assistants.create(
assistant = await async_client.beta.assistants.create( # type: ignore[deprecated]
name=name,
instructions=instructions,
tools=openai_tools,
tools=openai_tools, # type: ignore[arg-type]
model=model,
)
return cls(assistant_id=assistant.id, async_client=async_client, **kwargs)
@@ -617,7 +617,7 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]):
if version_gte_1_14
else isinstance(
content,
openai.types.beta.threads.MessageContentText,
openai.types.beta.threads.MessageContentText, # type: ignore[attr-defined]
)
)
for content in answer
@@ -771,7 +771,7 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]):
if version_gte_1_14
else isinstance(
content,
openai.types.beta.threads.MessageContentText,
openai.types.beta.threads.MessageContentText, # type: ignore[attr-defined]
)
)
for content in answer

View File

@@ -52,7 +52,9 @@ class ReActSingleInputOutputParser(AgentOutputParser):
@override
def parse(self, text: str) -> AgentAction | AgentFinish:
includes_answer = FINAL_ANSWER_ACTION in text
regex = r"Action\s*\d*\s*:[\s]*(.*?)Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
regex = (
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
)
action_match = re.search(regex, text, re.DOTALL)
if action_match:
if includes_answer:

View File

@@ -69,7 +69,7 @@ class OpenAIModerationChain(Chain):
except ValueError:
values["openai_pre_1_0"] = True
if values["openai_pre_1_0"]:
values["client"] = openai.Moderation
values["client"] = openai.Moderation # type: ignore[attr-defined]
else:
values["client"] = openai.OpenAI(api_key=openai_api_key)
values["async_client"] = openai.AsyncOpenAI(api_key=openai_api_key)

View File

@@ -60,7 +60,7 @@ def _embedding_factory() -> Embeddings:
from langchain_openai import OpenAIEmbeddings
except ImportError:
try:
from langchain_community.embeddings.openai import (
from langchain_community.embeddings.openai import ( # type: ignore[no-redef]
OpenAIEmbeddings,
)
except ImportError as e:
@@ -121,7 +121,7 @@ class _EmbeddingDistanceChainMixin(Chain):
pass
try:
from langchain_community.embeddings.openai import (
from langchain_community.embeddings.openai import ( # type: ignore[no-redef]
OpenAIEmbeddings,
)

View File

@@ -152,7 +152,7 @@ def load_evaluator(
from langchain_openai import ChatOpenAI
except ImportError:
try:
from langchain_community.chat_models.openai import (
from langchain_community.chat_models.openai import ( # type: ignore[no-redef]
ChatOpenAI,
)
except ImportError as e:

View File

@@ -20,10 +20,10 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
version = "1.0.2"
version = "1.0.1"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langchain-core>=1.2.17,<2.0.0",
"langchain-core>=1.2.5,<2.0.0",
"langchain-text-splitters>=1.1.0,<2.0.0",
"langsmith>=0.1.17,<1.0.0",
"pydantic>=2.7.4,<3.0.0",

View File

@@ -1,6 +1,3 @@
import signal
import sys
import pytest
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.exceptions import OutputParserException
@@ -46,32 +43,3 @@ Action: search Final Answer:
Action Input: what is the temperature in SF?"""
with pytest.raises(OutputParserException):
parser.invoke(_input)
def _timeout_handler(_signum: int, _frame: object) -> None:
msg = "ReDoS: regex took too long"
raise TimeoutError(msg)
@pytest.mark.skipif(
sys.platform == "win32", reason="SIGALRM is not available on Windows"
)
def test_react_single_input_no_redos() -> None:
"""Regression test for ReDoS caused by catastrophic backtracking."""
parser = ReActSingleInputOutputParser()
malicious = "Action: " + " \t" * 1000 + "Action "
old = signal.signal(signal.SIGALRM, _timeout_handler)
signal.alarm(2)
try:
try:
parser.parse(malicious)
except OutputParserException:
pass
except TimeoutError:
pytest.fail(
"ReDoS detected: ReActSingleInputOutputParser.parse() "
"hung on crafted input"
)
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old)

View File

@@ -1,6 +1,3 @@
import signal
import sys
import pytest
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.exceptions import OutputParserException
@@ -82,30 +79,3 @@ def test_final_answer_after_parsable_action() -> None:
"Parsing LLM output produced both a final answer and a parse-able action"
in exception_info.value.args[0]
)
def _timeout_handler(_signum: int, _frame: object) -> None:
msg = "ReDoS: regex took too long"
raise TimeoutError(msg)
@pytest.mark.skipif(
sys.platform == "win32", reason="SIGALRM is not available on Windows"
)
def test_mrkl_output_parser_no_redos() -> None:
"""Regression test for ReDoS caused by catastrophic backtracking."""
malicious = "Action: " + " \t" * 1000 + "Action "
old = signal.signal(signal.SIGALRM, _timeout_handler)
signal.alarm(2)
try:
try:
mrkl_output_parser.parse(malicious)
except OutputParserException:
pass
except TimeoutError:
pytest.fail(
"ReDoS detected: MRKLOutputParser.parse() hung on crafted input"
)
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old)

View File

@@ -2379,7 +2379,7 @@ wheels = [
[[package]]
name = "langchain-classic"
version = "1.0.2"
version = "1.0.1"
source = { editable = "." }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11'" },