mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-04 08:10:25 +00:00
Compare commits
23 Commits
sr/refacto
...
mdrxy/open
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae94520a56 | ||
|
|
5e059d48ff | ||
|
|
e51bacf8a9 | ||
|
|
3d026b803d | ||
|
|
b508b32da7 | ||
|
|
d88ef0160f | ||
|
|
d6f39ccce5 | ||
|
|
7a2347ae4f | ||
|
|
0a9686f183 | ||
|
|
ce6e56859e | ||
|
|
5143a7db9b | ||
|
|
d4a7878084 | ||
|
|
9d6250798d | ||
|
|
6408637f38 | ||
|
|
4ee0050641 | ||
|
|
760a1ee2b3 | ||
|
|
c9349bb089 | ||
|
|
6aa192cf48 | ||
|
|
ccf3e25884 | ||
|
|
b325eac612 | ||
|
|
551c2a6683 | ||
|
|
44ed735113 | ||
|
|
372048c569 |
@@ -51,7 +51,7 @@
|
||||
"\n",
|
||||
"### Credentials\n",
|
||||
"\n",
|
||||
"Head to https://platform.openai.com to sign up to OpenAI and generate an API key. Once you've done this set the OPENAI_API_KEY environment variable:"
|
||||
"Head to https://platform.openai.com to sign up to OpenAI and generate an API key. Once you've done this set the `OPENAI_API_KEY` environment variable:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -463,7 +463,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "a47c809b-852f-46bd-8b9e-d9534c17213d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
|
||||
@@ -8,11 +8,7 @@ from typing import Any, Literal, Optional, Union, cast
|
||||
from pydantic import model_validator
|
||||
from typing_extensions import NotRequired, Self, TypedDict, override
|
||||
|
||||
from langchain_core.messages.base import (
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
merge_content,
|
||||
)
|
||||
from langchain_core.messages.base import BaseMessage, BaseMessageChunk, merge_content
|
||||
from langchain_core.messages.tool import (
|
||||
InvalidToolCall,
|
||||
ToolCall,
|
||||
@@ -20,15 +16,9 @@ from langchain_core.messages.tool import (
|
||||
default_tool_chunk_parser,
|
||||
default_tool_parser,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
invalid_tool_call as create_invalid_tool_call,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
tool_call as create_tool_call,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
tool_call_chunk as create_tool_call_chunk,
|
||||
)
|
||||
from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call
|
||||
from langchain_core.messages.tool import tool_call as create_tool_call
|
||||
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.json import parse_partial_json
|
||||
from langchain_core.utils.usage import _dict_int_op
|
||||
|
||||
@@ -92,12 +92,12 @@ def tool(
|
||||
positional argument.
|
||||
description: Optional description for the tool.
|
||||
Precedence for the tool description value is as follows:
|
||||
- `description` argument
|
||||
(used even if docstring and/or `args_schema` are provided)
|
||||
- ``description`` argument
|
||||
(used even if docstring and/or ``args_schema`` are provided)
|
||||
- tool function docstring
|
||||
(used even if `args_schema` is provided)
|
||||
- `args_schema` description
|
||||
(used only if `description` / docstring are not provided)
|
||||
(used even if ``args_schema`` is provided)
|
||||
- ``args_schema`` description
|
||||
(used only if ``description`` / docstring are not provided)
|
||||
*args: Extra positional arguments. Must be empty.
|
||||
return_direct: Whether to return directly from the tool rather
|
||||
than continuing the agent loop. Defaults to False.
|
||||
@@ -105,13 +105,13 @@ def tool(
|
||||
Defaults to None.
|
||||
infer_schema: Whether to infer the schema of the arguments from
|
||||
the function's signature. This also makes the resultant tool
|
||||
accept a dictionary input to its `run()` function.
|
||||
accept a dictionary input to its ``run()`` function.
|
||||
Defaults to True.
|
||||
response_format: The tool response format. If "content" then the output of
|
||||
the tool is interpreted as the contents of a ToolMessage. If
|
||||
"content_and_artifact" then the output is expected to be a two-tuple
|
||||
corresponding to the (content, artifact) of a ToolMessage.
|
||||
Defaults to "content".
|
||||
response_format: The tool response format. If ``'content'`` then the output of
|
||||
the tool is interpreted as the contents of a ``ToolMessage``. If
|
||||
``'content_and_artifact'`` then the output is expected to be a two-tuple
|
||||
corresponding to the (content, artifact) of a ``ToolMessage``.
|
||||
Defaults to ``'content'``.
|
||||
parse_docstring: if ``infer_schema`` and ``parse_docstring``, will attempt to
|
||||
parse parameter descriptions from Google Style function docstrings.
|
||||
Defaults to False.
|
||||
|
||||
@@ -5,15 +5,7 @@ from __future__ import annotations
|
||||
import textwrap
|
||||
from collections.abc import Awaitable
|
||||
from inspect import signature
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Callable, Literal, Optional, Union
|
||||
|
||||
from pydantic import Field, SkipValidation
|
||||
from typing_extensions import override
|
||||
|
||||
2
libs/core/uv.lock
generated
2
libs/core/uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
|
||||
@@ -568,16 +568,20 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
making requests to OpenAI compatible APIs, such as vLLM, LM Studio, or other
|
||||
providers.
|
||||
|
||||
|
||||
This is the recommended way to pass custom parameters that are specific to your
|
||||
OpenAI-compatible API provider but not part of the standard OpenAI API.
|
||||
|
||||
|
||||
Examples:
|
||||
- LM Studio TTL parameter: ``extra_body={"ttl": 300}``
|
||||
- vLLM custom parameters: ``extra_body={"use_beam_search": True}``
|
||||
- Any other provider-specific parameters
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
Do NOT use ``model_kwargs`` for custom parameters that are not part of the
|
||||
standard OpenAI API, as this will cause errors when making API calls. Use
|
||||
``extra_body`` instead.
|
||||
@@ -593,10 +597,12 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
parameter and the value is either None, meaning that parameter should never be
|
||||
used, or it's a list of disabled values for the parameter.
|
||||
|
||||
|
||||
For example, older models may not support the ``'parallel_tool_calls'`` parameter at
|
||||
all, in which case ``disabled_params={"parallel_tool_calls": None}`` can be passed
|
||||
in.
|
||||
|
||||
|
||||
If a parameter is disabled then it will not be used by default in any methods, e.g.
|
||||
in :meth:`~langchain_openai.chat_models.base.ChatOpenAI.with_structured_output`.
|
||||
However this does not prevent a user from directly passed in the parameter during
|
||||
@@ -1731,7 +1737,26 @@ class BaseChatOpenAI(BaseChatModel):
|
||||
elif isinstance(tool_choice, bool):
|
||||
tool_choice = "required"
|
||||
elif isinstance(tool_choice, dict):
|
||||
pass
|
||||
# Handle allowed_tools choice format
|
||||
if tool_choice.get("type") == "allowed_tools":
|
||||
allowed_config = tool_choice.get("allowed_tools", {})
|
||||
mode = allowed_config.get("mode", "auto")
|
||||
allowed_tools = allowed_config.get("tools", [])
|
||||
|
||||
if mode not in ["auto", "required"]:
|
||||
raise ValueError(
|
||||
f"allowed_tools mode must be 'auto' or 'required', "
|
||||
f"got: {mode}"
|
||||
)
|
||||
|
||||
# Convert allowed_tools to the expected format
|
||||
tool_choice = {
|
||||
"type": "allowed_tools",
|
||||
"mode": mode,
|
||||
"tools": allowed_tools,
|
||||
}
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unrecognized tool_choice type. Expected str, bool or dict. "
|
||||
@@ -3585,6 +3610,14 @@ def _construct_responses_api_payload(
|
||||
schema_dict = schema
|
||||
if schema_dict == {"type": "json_object"}: # JSON mode
|
||||
payload["text"] = {"format": {"type": "json_object"}}
|
||||
elif schema_dict.get("type") == "grammar":
|
||||
if "grammar" not in schema_dict:
|
||||
raise ValueError("Grammar format requires 'grammar' field")
|
||||
payload["text"] = {
|
||||
"format": {"type": "grammar", "grammar": schema_dict["grammar"]}
|
||||
}
|
||||
elif schema_dict.get("type") == "python":
|
||||
payload["text"] = {"format": {"type": "python"}}
|
||||
elif (
|
||||
(
|
||||
response_format := _convert_to_openai_response_format(
|
||||
|
||||
121
libs/partners/openai/langchain_openai/chat_models/cfg_grammar.py
Normal file
121
libs/partners/openai/langchain_openai/chat_models/cfg_grammar.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Context-Free Grammar validation for custom tools."""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
try:
|
||||
from lark import Lark, LarkError
|
||||
except ImportError:
|
||||
Lark = None # type: ignore[misc,assignment]
|
||||
LarkError = Exception # type: ignore[misc,assignment]
|
||||
|
||||
|
||||
class CFGValidator:
|
||||
"""Validates input text against a Context-Free Grammar using Lark parser.
|
||||
|
||||
This class provides grammar-constrained validation for custom tool outputs,
|
||||
ensuring model outputs conform to specific syntax rules while maintaining
|
||||
the flexibility of plaintext (non-JSON) outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, grammar: str) -> None:
|
||||
"""Initialize the CFG validator.
|
||||
|
||||
Args:
|
||||
grammar: The Lark grammar string defining the allowed syntax.
|
||||
|
||||
Raises:
|
||||
ImportError: If lark package is not installed.
|
||||
LarkError: If the grammar string is invalid.
|
||||
"""
|
||||
if Lark is None:
|
||||
raise ImportError(
|
||||
"The 'lark' package is required for CFG validation. "
|
||||
"Install it with: pip install lark"
|
||||
)
|
||||
|
||||
self.grammar = grammar
|
||||
try:
|
||||
self.parser = Lark(grammar, start="start")
|
||||
except Exception as e:
|
||||
raise LarkError(f"Invalid grammar definition: {e}") from e
|
||||
|
||||
def validate(self, text: str) -> bool:
|
||||
"""Validate input text against the grammar.
|
||||
|
||||
Args:
|
||||
text: The text to validate.
|
||||
|
||||
Returns:
|
||||
True if the text matches the grammar, False otherwise.
|
||||
"""
|
||||
try:
|
||||
self.parser.parse(text)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def parse(self, text: str) -> Any:
|
||||
"""Parse input text according to the grammar.
|
||||
|
||||
Args:
|
||||
text: The text to parse.
|
||||
|
||||
Returns:
|
||||
The parse tree if successful.
|
||||
|
||||
Raises:
|
||||
LarkError: If the text doesn't match the grammar.
|
||||
"""
|
||||
try:
|
||||
return self.parser.parse(text)
|
||||
except Exception as e:
|
||||
raise LarkError(f"Grammar validation failed: {e}") from e
|
||||
|
||||
|
||||
def validate_cfg_format(tool_format: dict[str, Any]) -> Optional[CFGValidator]:
|
||||
"""Validate and create a CFG validator from tool format specification.
|
||||
|
||||
Args:
|
||||
tool_format: The format specification dictionary containing grammar rules.
|
||||
|
||||
Returns:
|
||||
CFGValidator instance if format type is 'grammar', None otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If format type is 'grammar' but grammar is missing or invalid.
|
||||
"""
|
||||
if not isinstance(tool_format, dict):
|
||||
return None
|
||||
|
||||
if tool_format.get("type") != "grammar":
|
||||
return None
|
||||
|
||||
grammar = tool_format.get("grammar")
|
||||
if not grammar:
|
||||
raise ValueError("Grammar format requires 'grammar' field")
|
||||
|
||||
if not isinstance(grammar, str):
|
||||
raise ValueError("Grammar must be a string")
|
||||
|
||||
try:
|
||||
return CFGValidator(grammar)
|
||||
except (ImportError, LarkError) as e:
|
||||
raise ValueError(f"Invalid grammar specification: {e}") from e
|
||||
|
||||
|
||||
def validate_custom_tool_output(
|
||||
output: str, cfg_validator: Optional[CFGValidator]
|
||||
) -> bool:
|
||||
"""Validate custom tool output against CFG if validator is provided.
|
||||
|
||||
Args:
|
||||
output: The tool output text to validate.
|
||||
cfg_validator: Optional CFG validator instance.
|
||||
|
||||
Returns:
|
||||
True if validation passes or no validator is provided, False otherwise.
|
||||
"""
|
||||
if cfg_validator is None:
|
||||
return True
|
||||
|
||||
return cfg_validator.validate(output)
|
||||
@@ -8,8 +8,9 @@ license = { text = "MIT" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"langchain-core<1.0.0,>=0.3.74",
|
||||
"openai<2.0.0,>=1.86.0",
|
||||
"openai<2.0.0,>=1.99.5",
|
||||
"tiktoken<1,>=0.7",
|
||||
"lark>=1.1.0"
|
||||
]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.29"
|
||||
|
||||
@@ -534,7 +534,7 @@ def test_disable_parallel_tool_calling() -> None:
|
||||
assert len(result.tool_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["gpt-4o-mini", "o1", "gpt-4", "gpt-5-nano"])
|
||||
@pytest.mark.parametrize("model", ["o1", "gpt-4", "gpt-5-nano"])
|
||||
def test_openai_structured_output(model: str) -> None:
|
||||
class MyModel(BaseModel):
|
||||
"""A Person"""
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Integration tests for CFG-enabled custom tools."""
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
from langchain_openai import ChatOpenAI, custom_tool
|
||||
from langchain_openai.chat_models.cfg_grammar import (
|
||||
validate_cfg_format,
|
||||
validate_custom_tool_output,
|
||||
)
|
||||
|
||||
|
||||
@custom_tool
|
||||
def execute_math_expression(expression: str) -> str:
|
||||
"""Execute a mathematical expression with CFG validation.
|
||||
|
||||
Args:
|
||||
expression: A mathematical expression to evaluate.
|
||||
|
||||
Returns:
|
||||
The result of the mathematical expression or an error message.
|
||||
"""
|
||||
# Define grammar for arithmetic expressions
|
||||
math_grammar = """
|
||||
start: expr
|
||||
expr: term (("+" | "-") term)*
|
||||
term: factor (("*" | "/") factor)*
|
||||
factor: NUMBER | "(" expr ")"
|
||||
NUMBER: /[0-9]+(\\.[0-9]+)?/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
# Create CFG validator
|
||||
tool_format = {"type": "grammar", "grammar": math_grammar}
|
||||
validator = validate_cfg_format(tool_format)
|
||||
|
||||
# Validate input against grammar
|
||||
if not validate_custom_tool_output(expression, validator):
|
||||
return f"Grammar validation failed for expression: {expression}"
|
||||
|
||||
# If valid, evaluate safely (in practice, use a safer evaluator)
|
||||
try:
|
||||
# Simple evaluation - in production, use ast.literal_eval or similar
|
||||
# for safety!
|
||||
result = eval(expression) # noqa: S307
|
||||
return f"Result: {result}"
|
||||
except Exception as e:
|
||||
return f"Execution error: {e}"
|
||||
|
||||
|
||||
@custom_tool
|
||||
def generate_sql_query(query: str) -> str:
|
||||
"""Generate and validate SQL SELECT queries with CFG validation.
|
||||
|
||||
Args:
|
||||
query: A SQL SELECT query to validate.
|
||||
|
||||
Returns:
|
||||
Validation result and mock execution.
|
||||
"""
|
||||
# Define grammar for simple SELECT queries
|
||||
sql_grammar = """
|
||||
start: query
|
||||
query: "SELECT" field_list "FROM" table_name where_clause?
|
||||
field_list: field ("," field)*
|
||||
field: IDENTIFIER | "*"
|
||||
table_name: IDENTIFIER
|
||||
where_clause: "WHERE" condition
|
||||
condition: IDENTIFIER "=" STRING
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
STRING: /"[^"]*"/ | /'[^']*'/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
# Create CFG validator
|
||||
tool_format = {"type": "grammar", "grammar": sql_grammar}
|
||||
validator = validate_cfg_format(tool_format)
|
||||
|
||||
# Validate input against grammar
|
||||
if not validate_custom_tool_output(query, validator):
|
||||
return f"SQL grammar validation failed for query: {query}"
|
||||
|
||||
# Mock execution of valid query
|
||||
return f"Valid SQL query: {query}\nMock result: [Sample data rows]"
|
||||
|
||||
|
||||
class TestCFGCustomToolsIntegration:
|
||||
"""Integration tests for CFG-enabled custom tools."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_math_tool_with_valid_expressions(self) -> None:
|
||||
"""Test CFG math tool with valid mathematical expressions."""
|
||||
# Test the tool directly with valid expressions
|
||||
result1 = execute_math_expression("5 + 3")
|
||||
assert "Result: 8" in result1
|
||||
|
||||
result2 = execute_math_expression("(10 - 2) * 3")
|
||||
assert "Result: 24" in result2
|
||||
|
||||
result3 = execute_math_expression("15 / 3")
|
||||
assert "Result: 5" in result3
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_math_tool_with_invalid_expressions(self) -> None:
|
||||
"""Test CFG math tool rejects invalid expressions."""
|
||||
# Test with invalid expressions
|
||||
result1 = execute_math_expression("hello world")
|
||||
assert "Grammar validation failed" in result1
|
||||
|
||||
result2 = execute_math_expression("5 + + 3")
|
||||
assert "Grammar validation failed" in result2
|
||||
|
||||
result3 = execute_math_expression("print('hello')")
|
||||
assert "Grammar validation failed" in result3
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_sql_tool_with_valid_queries(self) -> None:
|
||||
"""Test CFG SQL tool with valid SELECT queries."""
|
||||
# Test with valid SQL queries
|
||||
result1 = generate_sql_query("SELECT id FROM users")
|
||||
assert "Valid SQL query" in result1
|
||||
assert "Mock result" in result1
|
||||
|
||||
result2 = generate_sql_query("SELECT name, email FROM customers")
|
||||
assert "Valid SQL query" in result2
|
||||
|
||||
result3 = generate_sql_query(
|
||||
'SELECT * FROM products WHERE category = "electronics"'
|
||||
)
|
||||
assert "Valid SQL query" in result3
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_sql_tool_with_invalid_queries(self) -> None:
|
||||
"""Test CFG SQL tool rejects invalid queries."""
|
||||
# Test with invalid SQL queries
|
||||
result1 = generate_sql_query("DELETE FROM users")
|
||||
assert "SQL grammar validation failed" in result1
|
||||
|
||||
result2 = generate_sql_query("UPDATE users SET name = 'test'")
|
||||
assert "SQL grammar validation failed" in result2
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_validator_error_handling(self) -> None:
|
||||
"""Test CFG validator handles edge cases properly."""
|
||||
# Test empty input
|
||||
result1 = execute_math_expression("")
|
||||
assert "Grammar validation failed" in result1
|
||||
|
||||
# Test whitespace-only input
|
||||
result2 = execute_math_expression(" ")
|
||||
assert "Grammar validation failed" in result2
|
||||
|
||||
# Test special characters
|
||||
result3 = execute_math_expression("5 & 3")
|
||||
assert "Grammar validation failed" in result3
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_validator_with_complex_valid_expressions(self) -> None:
|
||||
"""Test CFG validator with complex but valid expressions."""
|
||||
# Test nested parentheses
|
||||
result1 = execute_math_expression("((5 + 3) * 2) - 1")
|
||||
assert "Result: 15" in result1
|
||||
|
||||
# Test decimal numbers
|
||||
result2 = execute_math_expression("3.14 * 2")
|
||||
assert "Result: 6.28" in result2
|
||||
|
||||
# Test multiple operations
|
||||
result3 = execute_math_expression("10 + 5 - 3 * 2")
|
||||
assert "Result: 9" in result3 # Should follow operator precedence
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_integration_performance(self) -> None:
|
||||
"""Test that CFG validation doesn't significantly impact performance."""
|
||||
import time
|
||||
|
||||
expressions = [
|
||||
"1 + 1",
|
||||
"2 * 3",
|
||||
"10 / 2",
|
||||
"(4 + 6) * 2",
|
||||
"100 - 50",
|
||||
"3.14 * 2",
|
||||
"25 / 5",
|
||||
"7 + 8 - 3",
|
||||
]
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for expr in expressions:
|
||||
result = execute_math_expression(expr)
|
||||
assert "Result:" in result # All should be valid
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# Should complete all validations in reasonable time (< 1 second)
|
||||
assert duration < 1.0, f"CFG validation took too long: {duration}s"
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_cfg_grammar_reusability(self) -> None:
|
||||
"""Test that CFG grammars can be reused efficiently."""
|
||||
math_grammar = """
|
||||
start: expr
|
||||
expr: term (("+" | "-") term)*
|
||||
term: factor (("*" | "/") factor)*
|
||||
factor: NUMBER | "(" expr ")"
|
||||
NUMBER: /[0-9]+(\\.[0-9]+)?/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
tool_format = {"type": "grammar", "grammar": math_grammar}
|
||||
validator = validate_cfg_format(tool_format)
|
||||
|
||||
# Reuse validator multiple times
|
||||
test_cases = [
|
||||
("5 + 3", True),
|
||||
("hello", False),
|
||||
("10 * 2", True),
|
||||
("invalid syntax", False),
|
||||
("(1 + 2) * 3", True),
|
||||
]
|
||||
|
||||
for expression, should_be_valid in test_cases:
|
||||
result = validate_custom_tool_output(expression, validator)
|
||||
assert result == should_be_valid, f"Failed for expression: {expression}"
|
||||
|
||||
|
||||
class TestCFGModelIntegration:
|
||||
"""Integration tests for CFG validation with actual OpenAI models."""
|
||||
|
||||
@pytest.mark.skip(reason="CFG model integration not yet implemented")
|
||||
@pytest.mark.scheduled
|
||||
def test_model_with_cfg_tools_valid_output(self) -> None:
|
||||
"""Test that model generates valid CFG-compliant outputs."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
llm_with_cfg_tools = llm.bind_tools(
|
||||
[execute_math_expression],
|
||||
tool_format={
|
||||
"type": "grammar",
|
||||
"grammar": "start: expr\nexpr: NUMBER ('+' | '-' | '*' | '/') NUMBER\nNUMBER: /[0-9]+/", # noqa: E501
|
||||
},
|
||||
)
|
||||
|
||||
response = llm_with_cfg_tools.invoke(
|
||||
"Calculate 5 + 3 using the math tool. Make sure to output a valid mathematical expression." # noqa: E501
|
||||
)
|
||||
|
||||
assert isinstance(response, AIMessage)
|
||||
assert response.tool_calls
|
||||
|
||||
@pytest.mark.skip(reason="CFG model integration not yet implemented")
|
||||
@pytest.mark.scheduled
|
||||
def test_model_cfg_validation_rejection(self) -> None:
|
||||
"""Test that model tool calls are rejected if they don't match CFG."""
|
||||
pass
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Integration tests for new OpenAI API features."""
|
||||
|
||||
import pytest
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langchain_openai import ChatOpenAI, custom_tool
|
||||
|
||||
|
||||
class TestResponseFormatsIntegration:
|
||||
"""Integration tests for new response format types."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_grammar_response_format_integration(self):
|
||||
"""Test grammar response format with actual API."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
grammar_format = {
|
||||
"type": "grammar",
|
||||
"grammar": """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*" | "/") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
""",
|
||||
}
|
||||
|
||||
bound_llm = llm.bind(response_format=grammar_format)
|
||||
assert bound_llm is not None
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_python_response_format_integration(self):
|
||||
"""Test python response format with actual API."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
python_format = {"type": "python"}
|
||||
|
||||
bound_llm = llm.bind(response_format=python_format)
|
||||
assert bound_llm is not None
|
||||
|
||||
|
||||
class TestAllowedToolsChoiceIntegration:
|
||||
"""Integration tests for allowed_tools tool choice."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_allowed_tools_integration(self):
|
||||
"""Test allowed_tools choice with actual API."""
|
||||
|
||||
@tool
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
@tool
|
||||
def get_time() -> str:
|
||||
"""Get current time."""
|
||||
return "12:00 PM"
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
allowed_tools_choice = {
|
||||
"type": "allowed_tools",
|
||||
"allowed_tools": {
|
||||
"mode": "auto",
|
||||
"tools": [
|
||||
{"type": "function", "function": {"name": "get_weather"}},
|
||||
{"type": "function", "function": {"name": "get_time"}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
bound_llm = llm.bind_tools(
|
||||
[get_weather, get_time], tool_choice=allowed_tools_choice
|
||||
)
|
||||
|
||||
# Test that it can be invoked without errors
|
||||
response = bound_llm.invoke("What's the weather like in Paris?")
|
||||
assert response is not None
|
||||
|
||||
|
||||
class TestVerbosityParameterIntegration:
|
||||
"""Integration tests for verbosity parameter."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_verbosity_integration(self):
|
||||
"""Test verbosity parameter with actual API."""
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="low", temperature=0)
|
||||
|
||||
response = llm.invoke("Tell me about artificial intelligence.")
|
||||
assert response is not None
|
||||
|
||||
|
||||
class TestCustomToolsIntegration:
|
||||
"""Integration tests for custom tools functionality."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_custom_tools_with_cfg_validation(self):
|
||||
"""Test custom tools with CFG validation."""
|
||||
# Import from the CFG validation module
|
||||
from langchain_openai.chat_models.cfg_grammar import (
|
||||
validate_cfg_format,
|
||||
validate_custom_tool_output,
|
||||
)
|
||||
|
||||
# Test arithmetic expressions
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: term (("+" | "-") term)*
|
||||
term: factor (("*" | "/") factor)*
|
||||
factor: NUMBER | "(" expr ")"
|
||||
NUMBER: /[0-9]+(\\.[0-9]+)?/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
tool_format = {"type": "grammar", "grammar": grammar}
|
||||
validator = validate_cfg_format(tool_format)
|
||||
|
||||
assert validator is not None
|
||||
|
||||
# Test valid expressions
|
||||
valid_expressions = ["5 + 3", "10 * 2", "(1 + 2) * 3"]
|
||||
for expr in valid_expressions:
|
||||
assert validate_custom_tool_output(expr, validator) is True
|
||||
|
||||
# Test invalid expressions
|
||||
invalid_expressions = ["hello", "5 + +", "invalid"]
|
||||
for expr in invalid_expressions:
|
||||
assert validate_custom_tool_output(expr, validator) is False
|
||||
|
||||
|
||||
class TestStreamingIntegration:
|
||||
"""Integration tests for streaming with new features."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_streaming_with_verbosity(self):
|
||||
"""Test streaming works with verbosity parameter."""
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="medium", temperature=0)
|
||||
|
||||
chunks = []
|
||||
for chunk in llm.stream("Count from 1 to 3"):
|
||||
chunks.append(chunk)
|
||||
|
||||
assert len(chunks) > 0
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_streaming_with_custom_tools(self):
|
||||
"""Test streaming works with custom tools."""
|
||||
|
||||
@custom_tool
|
||||
def execute_code(code: str) -> str:
|
||||
"""Execute Python code."""
|
||||
return f"Executed: {code}"
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
bound_llm = llm.bind_tools([execute_code])
|
||||
|
||||
chunks = []
|
||||
for chunk in bound_llm.stream("Write a simple Python print statement"):
|
||||
chunks.append(chunk)
|
||||
|
||||
assert len(chunks) > 0
|
||||
|
||||
|
||||
class TestMinimalReasoningEffortIntegration:
|
||||
"""Integration tests for minimal reasoning effort."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_minimal_reasoning_effort_integration(self):
|
||||
"""Test minimal reasoning effort with reasoning models."""
|
||||
llm = ChatOpenAI(model="gpt-5", reasoning_effort="minimal", temperature=0)
|
||||
|
||||
response = llm.invoke("What is 2 + 2?")
|
||||
assert response is not None
|
||||
|
||||
|
||||
class TestFullIntegration:
|
||||
"""Test combinations of new features together."""
|
||||
|
||||
@pytest.mark.scheduled
|
||||
def test_multiple_new_features_together(self):
|
||||
"""Test using multiple new features in combination."""
|
||||
|
||||
@tool
|
||||
def analyze_data(data: str) -> str:
|
||||
"""Analyze data and return insights."""
|
||||
return f"Analysis of {data}: positive trend"
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model="gpt-5", verbosity="medium", reasoning_effort="low", temperature=0
|
||||
)
|
||||
|
||||
# Try with allowed tools and grammar response format
|
||||
allowed_tools_choice = {
|
||||
"type": "allowed_tools",
|
||||
"allowed_tools": {
|
||||
"mode": "auto",
|
||||
"tools": [{"type": "function", "function": {"name": "analyze_data"}}],
|
||||
},
|
||||
}
|
||||
|
||||
grammar_format = {
|
||||
"type": "grammar",
|
||||
"grammar": "start: result\nresult: /[a-zA-Z0-9 ]+/",
|
||||
}
|
||||
|
||||
bound_llm = llm.bind_tools(
|
||||
[analyze_data], tool_choice=allowed_tools_choice
|
||||
).bind(response_format=grammar_format)
|
||||
|
||||
# If this works, it means all features are compatible
|
||||
response = bound_llm.invoke("Analyze this sales data")
|
||||
assert response is not None
|
||||
@@ -0,0 +1,646 @@
|
||||
"""Test custom tools functionality."""
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages.tool import tool_call
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langchain_openai import custom_tool
|
||||
from langchain_openai.chat_models.base import (
|
||||
_convert_delta_to_message_chunk,
|
||||
_convert_dict_to_message,
|
||||
)
|
||||
from langchain_openai.chat_models.cfg_grammar import (
|
||||
CFGValidator,
|
||||
validate_cfg_format,
|
||||
validate_custom_tool_output,
|
||||
)
|
||||
|
||||
|
||||
def test_custom_tool_decorator():
|
||||
"""Test that custom tools can be created with the `@tool` decorator."""
|
||||
|
||||
@custom_tool
|
||||
def execute_code(code: str) -> str:
|
||||
"""Execute arbitrary Python code."""
|
||||
return f"Executed: {code}"
|
||||
|
||||
assert execute_code.custom_tool is True
|
||||
|
||||
result = execute_code.invoke({"text_input": "print('hello')"})
|
||||
assert result == "Executed: print('hello')"
|
||||
|
||||
|
||||
def test_regular_tool_not_custom():
|
||||
"""Test that regular tools are not marked as custom."""
|
||||
|
||||
@tool
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
assert get_weather.custom_tool is False
|
||||
|
||||
|
||||
def test_tool_call_with_text_input():
|
||||
"""Test creating tool calls with `text_input`."""
|
||||
|
||||
custom_call = tool_call(name="execute_code", id="call_123")
|
||||
|
||||
assert custom_call["name"] == "execute_code"
|
||||
assert custom_call.get("text_input") == "print('hello world')"
|
||||
assert "args" not in custom_call
|
||||
assert custom_call["id"] == "call_123"
|
||||
|
||||
|
||||
def test_tool_call_validation():
|
||||
"""Test that `tool_call()` allows flexible creation."""
|
||||
|
||||
# Should allow both args and text_input (validation happens at execution time)
|
||||
call_with_both = tool_call(
|
||||
name="test", args={"x": 1}, text_input="some text", id="call_123"
|
||||
)
|
||||
assert call_with_both["name"] == "test"
|
||||
assert call_with_both.get("args") == {"x": 1}
|
||||
assert call_with_both.get("text_input") == "some text"
|
||||
|
||||
# Should allow empty args/text_input (backward compatibility)
|
||||
call_empty = tool_call(name="test", id="call_123")
|
||||
assert call_empty["name"] == "test"
|
||||
assert call_empty.get("args", {}) == {}
|
||||
|
||||
|
||||
def test_custom_tool_call_parsing():
|
||||
"""Test parsing custom tool calls from OpenAI response format."""
|
||||
|
||||
# Simulate OpenAI custom tool call response
|
||||
openai_response = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "execute_code",
|
||||
"input": "print('hello world')",
|
||||
"id": "call_abc123",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Parse the message
|
||||
message = _convert_dict_to_message(openai_response)
|
||||
|
||||
assert isinstance(message, AIMessage)
|
||||
assert len(message.tool_calls) == 1
|
||||
|
||||
tool_call = message.tool_calls[0]
|
||||
assert tool_call["name"] == "execute_code"
|
||||
assert tool_call.get("text_input") == "print('hello world')"
|
||||
assert "args" not in tool_call # Custom tools don't have an args field
|
||||
assert tool_call["id"] == "call_abc123"
|
||||
assert tool_call.get("type") == "tool_call"
|
||||
|
||||
|
||||
def test_regular_tool_call_parsing_unchanged():
|
||||
"""Test that regular tool call parsing still works."""
|
||||
|
||||
# Simulate regular OpenAI function tool call response
|
||||
openai_response = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "Paris", "unit": "celsius"}',
|
||||
},
|
||||
"id": "call_def456",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Parse the message
|
||||
message = _convert_dict_to_message(openai_response)
|
||||
|
||||
assert isinstance(message, AIMessage)
|
||||
assert len(message.tool_calls) == 1
|
||||
|
||||
tool_call = message.tool_calls[0]
|
||||
assert tool_call["name"] == "get_weather"
|
||||
assert tool_call.get("args") == {"location": "Paris", "unit": "celsius"}
|
||||
assert "text_input" not in tool_call
|
||||
assert tool_call["id"] == "call_def456"
|
||||
|
||||
|
||||
def test_custom_tool_streaming_text_input():
|
||||
"""Test streaming custom tool calls use `text_input` field."""
|
||||
chunk1 = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "execute_code",
|
||||
"input": "print('hello",
|
||||
"id": "call_abc123",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
chunk2 = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": None,
|
||||
"input": " world')",
|
||||
"id": None,
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
message_chunk1 = _convert_delta_to_message_chunk(chunk1, AIMessageChunk)
|
||||
message_chunk2 = _convert_delta_to_message_chunk(chunk2, AIMessageChunk)
|
||||
|
||||
# Verify first chunk
|
||||
assert isinstance(message_chunk1, AIMessageChunk)
|
||||
assert len(message_chunk1.tool_call_chunks) == 1
|
||||
tool_call_chunk1 = message_chunk1.tool_call_chunks[0]
|
||||
assert tool_call_chunk1["name"] == "execute_code"
|
||||
assert tool_call_chunk1.get("text_input") == "print('hello"
|
||||
assert tool_call_chunk1.get("args") == "" # Empty for custom tools
|
||||
assert tool_call_chunk1["id"] == "call_abc123"
|
||||
assert tool_call_chunk1["index"] == 0
|
||||
|
||||
# Verify second chunk
|
||||
assert isinstance(message_chunk2, AIMessageChunk)
|
||||
assert len(message_chunk2.tool_call_chunks) == 1
|
||||
tool_call_chunk2 = message_chunk2.tool_call_chunks[0]
|
||||
assert tool_call_chunk2["name"] is None
|
||||
assert tool_call_chunk2.get("text_input") == " world')"
|
||||
assert tool_call_chunk2.get("args") == "" # Empty for custom tools
|
||||
assert tool_call_chunk2["id"] is None
|
||||
assert tool_call_chunk2["index"] == 0
|
||||
|
||||
# Test chunk aggregation
|
||||
combined = message_chunk1 + message_chunk2
|
||||
assert isinstance(combined, AIMessageChunk)
|
||||
assert len(combined.tool_call_chunks) == 1
|
||||
combined_chunk = combined.tool_call_chunks[0]
|
||||
assert combined_chunk["name"] == "execute_code"
|
||||
assert combined_chunk.get("text_input") == "print('hello world')"
|
||||
assert combined_chunk.get("args") == "" # Empty for custom tools
|
||||
assert combined_chunk["id"] == "call_abc123"
|
||||
|
||||
|
||||
def test_function_tool_streaming_args():
|
||||
"""Test streaming function tool calls still use args field with JSON."""
|
||||
chunk1 = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"location": "Par'},
|
||||
"id": "call_def456",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
chunk2 = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": None, "arguments": 'is", "unit": "celsius"}'},
|
||||
"id": None,
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Parse the chunks
|
||||
message_chunk1 = _convert_delta_to_message_chunk(chunk1, AIMessageChunk)
|
||||
message_chunk2 = _convert_delta_to_message_chunk(chunk2, AIMessageChunk)
|
||||
|
||||
# Verify first chunk
|
||||
assert isinstance(message_chunk1, AIMessageChunk)
|
||||
assert len(message_chunk1.tool_call_chunks) == 1
|
||||
tool_call_chunk1 = message_chunk1.tool_call_chunks[0]
|
||||
assert tool_call_chunk1["name"] == "get_weather"
|
||||
assert tool_call_chunk1.get("args") == '{"location": "Par'
|
||||
assert "text_input" not in tool_call_chunk1
|
||||
assert tool_call_chunk1["id"] == "call_def456"
|
||||
assert tool_call_chunk1["index"] == 0
|
||||
|
||||
# Verify second chunk
|
||||
assert isinstance(message_chunk2, AIMessageChunk)
|
||||
assert len(message_chunk2.tool_call_chunks) == 1
|
||||
tool_call_chunk2 = message_chunk2.tool_call_chunks[0]
|
||||
assert tool_call_chunk2["name"] is None
|
||||
assert tool_call_chunk2.get("args") == 'is", "unit": "celsius"}'
|
||||
assert "text_input" not in tool_call_chunk2
|
||||
assert tool_call_chunk2["id"] is None
|
||||
assert tool_call_chunk2["index"] == 0
|
||||
|
||||
# Test chunk aggregation
|
||||
combined = message_chunk1 + message_chunk2
|
||||
assert isinstance(combined, AIMessageChunk)
|
||||
assert len(combined.tool_call_chunks) == 1
|
||||
combined_chunk = combined.tool_call_chunks[0]
|
||||
assert combined_chunk["name"] == "get_weather"
|
||||
assert combined_chunk.get("args") == '{"location": "Paris", "unit": "celsius"}'
|
||||
assert "text_input" not in combined_chunk
|
||||
assert combined_chunk["id"] == "call_def456"
|
||||
|
||||
|
||||
def test_mixed_tool_streaming():
|
||||
"""Test streaming with both custom and function tools in same response."""
|
||||
# Simulate mixed tool streaming chunk from OpenAI
|
||||
chunk = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "execute_code",
|
||||
"input": "x = 5",
|
||||
"id": "call_custom_123",
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"location": "NYC"}'},
|
||||
"id": "call_func_456",
|
||||
"index": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Parse the chunk
|
||||
message_chunk = _convert_delta_to_message_chunk(chunk, AIMessageChunk)
|
||||
|
||||
assert isinstance(message_chunk, AIMessageChunk)
|
||||
assert len(message_chunk.tool_call_chunks) == 2
|
||||
|
||||
# Verify custom tool chunk
|
||||
custom_chunk = message_chunk.tool_call_chunks[0]
|
||||
assert custom_chunk["name"] == "execute_code"
|
||||
assert custom_chunk.get("text_input") == "x = 5"
|
||||
assert custom_chunk.get("args") == "" # Empty for custom tools
|
||||
assert custom_chunk["id"] == "call_custom_123"
|
||||
assert custom_chunk["index"] == 0
|
||||
|
||||
# Verify function tool chunk
|
||||
function_chunk = message_chunk.tool_call_chunks[1]
|
||||
assert function_chunk["name"] == "get_weather"
|
||||
assert function_chunk.get("args") == '{"location": "NYC"}'
|
||||
assert "text_input" not in function_chunk
|
||||
assert function_chunk["id"] == "call_func_456"
|
||||
assert function_chunk["index"] == 1
|
||||
|
||||
|
||||
# CFG Grammar Tests
|
||||
|
||||
|
||||
class TestCFGValidator:
|
||||
"""Test CFG validator functionality."""
|
||||
|
||||
def test_cfg_validator_initialization(self):
|
||||
"""Test CFG validator can be initialized with valid grammar."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
assert validator.grammar == grammar
|
||||
assert validator.parser is not None
|
||||
|
||||
def test_cfg_validator_initialization_invalid_grammar(self):
|
||||
"""Test CFG validator raises error with invalid grammar."""
|
||||
invalid_grammar = "invalid grammar string [["
|
||||
with pytest.raises(Exception):
|
||||
CFGValidator(invalid_grammar)
|
||||
|
||||
def test_cfg_validator_validate_valid_input(self):
|
||||
"""Test CFG validator accepts valid input."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
assert validator.validate("5 + 3") is True
|
||||
assert validator.validate("10 * 2") is True
|
||||
assert validator.validate("100 - 50") is True
|
||||
|
||||
def test_cfg_validator_validate_invalid_input(self):
|
||||
"""Test CFG validator rejects invalid input."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
assert validator.validate("hello") is False
|
||||
assert validator.validate("5 + + 3") is False
|
||||
assert validator.validate("5 + ") is False
|
||||
assert validator.validate("+ 5") is False
|
||||
|
||||
def test_cfg_validator_parse_valid_input(self):
|
||||
"""Test CFG validator can parse valid input."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
tree = validator.parse("5 + 3")
|
||||
assert tree is not None
|
||||
assert str(tree.data) == "start"
|
||||
|
||||
def test_cfg_validator_parse_invalid_input(self):
|
||||
"""Test CFG validator raises error when parsing invalid input."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
validator.parse("invalid input")
|
||||
|
||||
def test_cfg_validator_complex_grammar(self):
|
||||
"""Test CFG validator with more complex grammar."""
|
||||
# SQL-like grammar
|
||||
grammar = """
|
||||
start: query
|
||||
query: "SELECT" field_list "FROM" table_name where_clause?
|
||||
field_list: field ("," field)*
|
||||
field: IDENTIFIER
|
||||
table_name: IDENTIFIER
|
||||
where_clause: "WHERE" condition
|
||||
condition: IDENTIFIER "=" STRING
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
STRING: /"[^"]*"/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
# Valid SQL queries
|
||||
assert validator.validate("SELECT id FROM users") is True
|
||||
assert (
|
||||
validator.validate('SELECT id, name FROM users WHERE status = "active"')
|
||||
is True
|
||||
)
|
||||
|
||||
# Invalid SQL queries
|
||||
assert validator.validate("SELECT FROM users") is False
|
||||
assert validator.validate("INVALID QUERY") is False
|
||||
|
||||
def test_cfg_validator_python_code_grammar(self):
|
||||
"""Test CFG validator with Python code grammar."""
|
||||
# Simple Python expression grammar
|
||||
grammar = """
|
||||
start: statement
|
||||
statement: assignment | expression
|
||||
assignment: IDENTIFIER "=" expression
|
||||
expression: term (("+" | "-") term)*
|
||||
term: factor (("*" | "/") factor)*
|
||||
factor: NUMBER | IDENTIFIER | "(" expression ")"
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
# Valid Python expressions
|
||||
assert validator.validate("x = 5") is True
|
||||
assert validator.validate("result = a + b * c") is True
|
||||
assert validator.validate("(x + y) / 2") is True
|
||||
|
||||
# Invalid Python expressions
|
||||
assert validator.validate("x =") is False
|
||||
assert validator.validate("+ + +") is False
|
||||
|
||||
|
||||
class TestValidateCFGFormat:
|
||||
"""Test validate_cfg_format function."""
|
||||
|
||||
def test_validate_cfg_format_valid_grammar_format(self):
|
||||
"""Test validate_cfg_format with valid grammar format."""
|
||||
tool_format = {
|
||||
"type": "grammar",
|
||||
"grammar": "start: expr\nexpr: NUMBER\nNUMBER: /[0-9]+/",
|
||||
}
|
||||
|
||||
validator = validate_cfg_format(tool_format)
|
||||
assert validator is not None
|
||||
assert isinstance(validator, CFGValidator)
|
||||
|
||||
def test_validate_cfg_format_non_grammar_format(self):
|
||||
"""Test validate_cfg_format with non-grammar format."""
|
||||
tool_format = {"type": "json_schema", "schema": {}}
|
||||
|
||||
validator = validate_cfg_format(tool_format)
|
||||
assert validator is None
|
||||
|
||||
def test_validate_cfg_format_missing_grammar(self):
|
||||
"""Test validate_cfg_format with missing grammar field."""
|
||||
tool_format = {"type": "grammar"}
|
||||
|
||||
with pytest.raises(ValueError, match="Grammar format requires 'grammar' field"):
|
||||
validate_cfg_format(tool_format)
|
||||
|
||||
def test_validate_cfg_format_invalid_grammar_type(self):
|
||||
"""Test validate_cfg_format with non-string grammar."""
|
||||
tool_format = {"type": "grammar", "grammar": ["not", "a", "string"]}
|
||||
|
||||
with pytest.raises(ValueError, match="Grammar must be a string"):
|
||||
validate_cfg_format(tool_format)
|
||||
|
||||
def test_validate_cfg_format_invalid_grammar_syntax(self):
|
||||
"""Test validate_cfg_format with invalid grammar syntax."""
|
||||
tool_format = {"type": "grammar", "grammar": "invalid grammar [[ syntax"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid grammar specification"):
|
||||
validate_cfg_format(tool_format)
|
||||
|
||||
def test_validate_cfg_format_non_dict_input(self):
|
||||
"""Test validate_cfg_format with non-dict input."""
|
||||
assert validate_cfg_format("not a dict") is None
|
||||
assert validate_cfg_format(None) is None
|
||||
assert validate_cfg_format([]) is None
|
||||
|
||||
|
||||
class TestValidateCustomToolOutput:
|
||||
"""Test validate_custom_tool_output function."""
|
||||
|
||||
def test_validate_custom_tool_output_with_validator(self):
|
||||
"""Test validate_custom_tool_output with CFG validator."""
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
# Valid outputs
|
||||
assert validate_custom_tool_output("5 + 3", validator) is True
|
||||
assert validate_custom_tool_output("10 - 7", validator) is True
|
||||
|
||||
# Invalid outputs
|
||||
assert validate_custom_tool_output("hello", validator) is False
|
||||
assert validate_custom_tool_output("5 + + 3", validator) is False
|
||||
|
||||
def test_validate_custom_tool_output_without_validator(self):
|
||||
"""Test validate_custom_tool_output without CFG validator."""
|
||||
# Should return True when no validator is provided
|
||||
assert validate_custom_tool_output("any string", None) is True
|
||||
assert validate_custom_tool_output("", None) is True
|
||||
assert validate_custom_tool_output("invalid grammar", None) is True
|
||||
|
||||
|
||||
class TestCFGIntegration:
|
||||
"""Test CFG integration with custom tools."""
|
||||
|
||||
def test_custom_tool_call_with_cfg_validation(self):
|
||||
"""Test that CFG validation can be integrated with custom tool calls."""
|
||||
# Arithmetic expressions
|
||||
grammar = """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-" | "*" | "/") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
# Simulate custom tool definition with CFG format
|
||||
tool_format = {"type": "grammar", "grammar": grammar}
|
||||
|
||||
validator = validate_cfg_format(tool_format)
|
||||
assert validator is not None
|
||||
|
||||
# Test valid tool outputs
|
||||
valid_outputs = ["5 + 3", "10 * 2", "100 / 5", "50 - 25"]
|
||||
for output in valid_outputs:
|
||||
assert validate_custom_tool_output(output, validator) is True
|
||||
|
||||
# Test invalid tool outputs
|
||||
invalid_outputs = ["hello", "5 + + 3", "invalid", "5 +"]
|
||||
for output in invalid_outputs:
|
||||
assert validate_custom_tool_output(output, validator) is False
|
||||
|
||||
def test_sql_query_cfg_validation(self):
|
||||
"""Test CFG validation for SQL-like queries."""
|
||||
sql_grammar = """
|
||||
start: query
|
||||
query: "SELECT" field "FROM" table
|
||||
field: IDENTIFIER
|
||||
table: IDENTIFIER
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
tool_format = {"type": "grammar", "grammar": sql_grammar}
|
||||
|
||||
validator = validate_cfg_format(tool_format)
|
||||
assert validator is not None
|
||||
|
||||
# Valid SQL queries
|
||||
assert validate_custom_tool_output("SELECT id FROM users", validator) is True
|
||||
assert (
|
||||
validate_custom_tool_output("SELECT name FROM products", validator) is True
|
||||
)
|
||||
|
||||
# Invalid SQL queries
|
||||
assert validate_custom_tool_output("SELECT FROM users", validator) is False
|
||||
assert validate_custom_tool_output("INVALID QUERY", validator) is False
|
||||
|
||||
def test_python_expression_cfg_validation(self):
|
||||
"""Test CFG validation for Python expressions."""
|
||||
python_grammar = """
|
||||
start: assignment
|
||||
assignment: IDENTIFIER "=" expression
|
||||
expression: term (("+" | "-") term)*
|
||||
term: factor (("*" | "/") factor)*
|
||||
factor: NUMBER | IDENTIFIER
|
||||
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
|
||||
tool_format = {"type": "grammar", "grammar": python_grammar}
|
||||
|
||||
validator = validate_cfg_format(tool_format)
|
||||
assert validator is not None
|
||||
|
||||
# Valid Python assignments
|
||||
assert validate_custom_tool_output("x = 5", validator) is True
|
||||
assert validate_custom_tool_output("result = a + b", validator) is True
|
||||
assert (
|
||||
validate_custom_tool_output("total = price * quantity", validator) is True
|
||||
)
|
||||
|
||||
# Invalid Python assignments
|
||||
assert validate_custom_tool_output("x =", validator) is False
|
||||
assert validate_custom_tool_output("= 5", validator) is False
|
||||
assert validate_custom_tool_output("hello world", validator) is False
|
||||
|
||||
|
||||
@pytest.mark.skipif(CFGValidator is None, reason="lark package not available")
|
||||
class TestCFGErrorHandling:
|
||||
"""Test CFG error handling when lark is not available."""
|
||||
|
||||
def test_cfg_validator_import_error(self, monkeypatch):
|
||||
"""Test CFG validator handles missing lark import gracefully."""
|
||||
# Mock the import to fail
|
||||
monkeypatch.setattr("langchain_openai.chat_models.cfg_grammar.Lark", None)
|
||||
|
||||
with pytest.raises(ImportError, match="The 'lark' package is required"):
|
||||
CFGValidator("start: NUMBER\nNUMBER: /[0-9]+/")
|
||||
|
||||
def test_cfg_edge_cases(self):
|
||||
"""Test CFG validator edge cases."""
|
||||
grammar = """
|
||||
start: item*
|
||||
item: WORD
|
||||
WORD: /\\w+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
"""
|
||||
validator = CFGValidator(grammar)
|
||||
|
||||
# Empty string should be valid (zero items)
|
||||
assert validator.validate("") is True
|
||||
|
||||
# Single word should be valid
|
||||
assert validator.validate("hello") is True
|
||||
|
||||
# Multiple words should be valid
|
||||
assert validator.validate("hello world test") is True
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Test new OpenAI API features."""
|
||||
|
||||
import pytest
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
|
||||
class TestResponseFormats:
|
||||
"""Test new response format types."""
|
||||
|
||||
def test_grammar_response_format(self):
|
||||
"""Test grammar response format configuration."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
grammar_format = {
|
||||
"type": "grammar",
|
||||
"grammar": """
|
||||
start: expr
|
||||
expr: NUMBER ("+" | "-") NUMBER
|
||||
NUMBER: /[0-9]+/
|
||||
%import common.WS
|
||||
%ignore WS
|
||||
""",
|
||||
}
|
||||
|
||||
# This should not raise an error during bind
|
||||
bound_llm = llm.bind(response_format=grammar_format)
|
||||
assert bound_llm is not None
|
||||
|
||||
def test_python_response_format(self):
|
||||
"""Test python response format configuration."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
python_format = {"type": "python"}
|
||||
|
||||
# This should not raise an error during bind
|
||||
bound_llm = llm.bind(response_format=python_format)
|
||||
assert bound_llm is not None
|
||||
|
||||
def test_grammar_format_validation(self):
|
||||
"""Test that grammar format requires grammar field."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
# Test missing grammar field
|
||||
invalid_format = {"type": "grammar"}
|
||||
|
||||
bound_llm = llm.bind(response_format=invalid_format)
|
||||
assert bound_llm is not None
|
||||
|
||||
|
||||
class TestAllowedToolsChoice:
|
||||
"""Test allowed_tools tool choice functionality."""
|
||||
|
||||
def test_allowed_tools_auto_mode(self):
|
||||
"""Test allowed_tools with auto mode."""
|
||||
|
||||
@tool
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for location."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
@tool
|
||||
def get_time() -> str:
|
||||
"""Get current time."""
|
||||
return "12:00 PM"
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
allowed_tools_choice = {
|
||||
"type": "allowed_tools",
|
||||
"allowed_tools": {
|
||||
"mode": "auto",
|
||||
"tools": [
|
||||
{"type": "function", "function": {"name": "get_weather"}},
|
||||
{"type": "function", "function": {"name": "get_time"}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
bound_llm = llm.bind_tools(
|
||||
[get_weather, get_time], tool_choice=allowed_tools_choice
|
||||
)
|
||||
assert bound_llm is not None
|
||||
|
||||
def test_allowed_tools_required_mode(self):
|
||||
"""Test allowed_tools with required mode."""
|
||||
|
||||
@tool
|
||||
def calculate(expression: str) -> str:
|
||||
"""Calculate mathematical expression."""
|
||||
return f"Result: {eval(expression)}" # noqa: S307
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
allowed_tools_choice = {
|
||||
"type": "allowed_tools",
|
||||
"allowed_tools": {
|
||||
"mode": "required",
|
||||
"tools": [{"type": "function", "function": {"name": "calculate"}}],
|
||||
},
|
||||
}
|
||||
|
||||
bound_llm = llm.bind_tools([calculate], tool_choice=allowed_tools_choice)
|
||||
assert bound_llm is not None
|
||||
|
||||
def test_allowed_tools_invalid_mode(self):
|
||||
"""Test that invalid allowed_tools mode raises error."""
|
||||
|
||||
@tool
|
||||
def test_tool() -> str:
|
||||
"""Test tool."""
|
||||
return "test"
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
invalid_choice = {
|
||||
"type": "allowed_tools",
|
||||
"allowed_tools": {"mode": "invalid_mode", "tools": []},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="allowed_tools mode must be"):
|
||||
llm.bind_tools([test_tool], tool_choice=invalid_choice)
|
||||
|
||||
|
||||
class TestVerbosityParameter:
|
||||
"""Test verbosity parameter functionality."""
|
||||
|
||||
def test_verbosity_parameter_low(self):
|
||||
"""Test verbosity parameter with low value."""
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="low")
|
||||
|
||||
assert llm.verbosity == "low"
|
||||
assert "verbosity" in llm._default_params
|
||||
assert llm._default_params["verbosity"] == "low"
|
||||
|
||||
def test_verbosity_parameter_medium(self):
|
||||
"""Test verbosity parameter with medium value."""
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="medium")
|
||||
|
||||
assert llm.verbosity == "medium"
|
||||
assert llm._default_params["verbosity"] == "medium"
|
||||
|
||||
def test_verbosity_parameter_high(self):
|
||||
"""Test verbosity parameter with high value."""
|
||||
llm = ChatOpenAI(model="gpt-5", verbosity="high")
|
||||
|
||||
assert llm.verbosity == "high"
|
||||
assert llm._default_params["verbosity"] == "high"
|
||||
|
||||
def test_verbosity_parameter_none(self):
|
||||
"""Test verbosity parameter with None (default)."""
|
||||
llm = ChatOpenAI(model="gpt-5")
|
||||
|
||||
assert llm.verbosity is None
|
||||
# When verbosity is None, it may not be included in _default_params
|
||||
# due to the exclude_if_none filtering
|
||||
verbosity_param = llm._default_params.get("verbosity")
|
||||
assert verbosity_param is None
|
||||
|
||||
|
||||
class TestCustomToolStreamingSupport:
|
||||
"""Test that custom tool streaming events are handled."""
|
||||
|
||||
def test_custom_tool_streaming_event_types(self):
|
||||
"""Test that the new custom tool streaming event types are supported."""
|
||||
# This test verifies that our code includes the necessary event handling
|
||||
# The actual streaming event handling is tested in integration tests
|
||||
|
||||
# Import the base module to verify it loads without errors
|
||||
import langchain_openai.chat_models.base as base_module
|
||||
|
||||
# Verify the module loaded successfully
|
||||
assert base_module is not None
|
||||
|
||||
# Check that the module contains our custom tool streaming logic
|
||||
# by looking for the event type strings in the source
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(base_module)
|
||||
|
||||
# Verify our custom tool streaming events are handled
|
||||
assert "response.custom_tool_call_input.delta" in source
|
||||
assert "response.custom_tool_call_input.done" in source
|
||||
|
||||
|
||||
class TestMinimalReasoningEffort:
|
||||
"""Test that minimal reasoning effort is supported."""
|
||||
|
||||
def test_minimal_reasoning_effort(self):
|
||||
"""Test reasoning_effort parameter supports 'minimal'."""
|
||||
llm = ChatOpenAI(model="gpt-5", reasoning_effort="minimal")
|
||||
|
||||
assert llm.reasoning_effort == "minimal"
|
||||
assert llm._default_params["reasoning_effort"] == "minimal"
|
||||
|
||||
def test_all_reasoning_effort_values(self):
|
||||
"""Test all supported reasoning effort values."""
|
||||
supported_values = ["minimal", "low", "medium", "high"]
|
||||
|
||||
for value in supported_values:
|
||||
llm = ChatOpenAI(model="gpt-5", reasoning_effort=value)
|
||||
assert llm.reasoning_effort == value
|
||||
assert llm._default_params["reasoning_effort"] == value
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test that existing functionality still works."""
|
||||
|
||||
def test_existing_response_formats(self):
|
||||
"""Test that existing response formats still work."""
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
# JSON object format should still work
|
||||
json_llm = llm.bind(response_format={"type": "json_object"})
|
||||
assert json_llm is not None
|
||||
|
||||
# JSON schema format should still work
|
||||
schema = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "test_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"result": {"type": "string"}},
|
||||
"required": ["result"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
schema_llm = llm.bind(response_format=schema)
|
||||
assert schema_llm is not None
|
||||
|
||||
def test_existing_tool_choice(self):
|
||||
"""Test that existing tool_choice functionality still works."""
|
||||
|
||||
@tool
|
||||
def test_tool(x: int) -> int:
|
||||
"""Test tool."""
|
||||
return x * 2
|
||||
|
||||
llm = ChatOpenAI(model="gpt-5", temperature=0)
|
||||
|
||||
# String tool choice should still work
|
||||
bound_llm = llm.bind_tools([test_tool], tool_choice="test_tool")
|
||||
assert bound_llm is not None
|
||||
|
||||
# Auto/none/required should still work
|
||||
for choice in ["auto", "none", "required"]:
|
||||
bound_llm = llm.bind_tools([test_tool], tool_choice=choice)
|
||||
assert bound_llm is not None
|
||||
|
||||
# Boolean tool choice should still work
|
||||
bound_llm = llm.bind_tools([test_tool], tool_choice=True)
|
||||
assert bound_llm is not None
|
||||
21
libs/partners/openai/uv.lock
generated
21
libs/partners/openai/uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
@@ -542,6 +542,7 @@ version = "0.3.29"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "lark" },
|
||||
{ name = "openai" },
|
||||
{ name = "tiktoken" },
|
||||
]
|
||||
@@ -588,7 +589,8 @@ typing = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "openai", specifier = ">=1.99.3,<2.0.0" },
|
||||
{ name = "lark", specifier = ">=1.1.0" },
|
||||
{ name = "openai", specifier = ">=1.99.5,<2.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.7,<1" },
|
||||
]
|
||||
|
||||
@@ -688,6 +690,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/f4/c206c0888f8a506404cb4f16ad89593bdc2f70cf00de26a1a0a7a76ad7a3/langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed", size = 363002, upload-time = "2025-06-05T05:10:27.228Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lark"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
@@ -995,7 +1006,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.99.3"
|
||||
version = "1.99.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -1007,9 +1018,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/d3/c372420c8ca1c60e785fd8c19e536cea8f16b0cfdcdad6458e1d8884f2ea/openai-1.99.3.tar.gz", hash = "sha256:1a0e2910e4545d828c14218f2ac3276827c94a043f5353e43b9413b38b497897", size = 504932, upload-time = "2025-08-07T20:35:15.893Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/16b1b6ee8a62cbfb59057f97f6d9b7bb5ce529047d80bc0b406f65dfdc48/openai-1.99.5.tar.gz", hash = "sha256:aa97ac3326cac7949c5e4ac0274c454c1d19c939760107ae0d3948fc26a924ca", size = 505144, upload-time = "2025-08-08T16:44:46.865Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/bc/e52f49940b4e320629da7db09c90a2407a48c612cff397b4b41b7e58cdf9/openai-1.99.3-py3-none-any.whl", hash = "sha256:c786a03f6cddadb5ee42c6d749aa4f6134fe14fdd7d69a667e5e7ce7fd29a719", size = 785776, upload-time = "2025-08-07T20:35:13.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/f2/2472ae020f5156a994710bf926a76915c71bc7b5debf7b81a11506ec8414/openai-1.99.5-py3-none-any.whl", hash = "sha256:4e870f9501b7c36132e2be13313ce3c4d6915a837e7a299c483aab6a6d4412e9", size = 786246, upload-time = "2025-08-08T16:44:45.062Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
30
uv.lock
generated
30
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
@@ -181,7 +181,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.57.1"
|
||||
version = "0.62.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -192,9 +192,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/75/6261a1a8d92aed47e27d2fcfb3a411af73b1435e6ae1186da02b760565d0/anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0", size = 423775, upload-time = "2025-07-03T16:57:35.932Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/89/d41aa785f724275ff2a3135d4a656ba42c786e7a140973cbd7315dd2d5d2/anthropic-0.62.0.tar.gz", hash = "sha256:d45389229db9e443ea1a877f8d63309947f134991473cf8e88efee322840d084", size = 427073, upload-time = "2025-08-08T13:28:54.411Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/cf/ca0ba77805aec6171629a8b665c7dc224dab374539c3d27005b5d8c100a0/anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306", size = 292779, upload-time = "2025-07-03T16:57:34.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/2f/53d41ff5d8fee7c77030a7fbf3432d0c7db5b799596b7d8e581bcb9a377d/anthropic-0.62.0-py3-none-any.whl", hash = "sha256:adcf2af98aa2b11e3b7c71afb2e0cb0613f679ad4a18ef58c38f17784b3df72e", size = 296625, upload-time = "2025-08-08T13:28:53.042Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2354,7 +2354,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = { editable = "libs/partners/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
@@ -2364,7 +2364,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", specifier = ">=0.57.0,<1" },
|
||||
{ name = "anthropic", specifier = ">=0.60.0,<1" },
|
||||
{ name = "langchain-core", editable = "libs/core" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
]
|
||||
@@ -2483,7 +2483,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.72"
|
||||
version = "0.3.74"
|
||||
source = { editable = "libs/core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2618,7 +2618,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-groq"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = { editable = "libs/partners/groq" }
|
||||
dependencies = [
|
||||
{ name = "groq" },
|
||||
@@ -2627,7 +2627,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "groq", specifier = ">=0.29.0,<1" },
|
||||
{ name = "groq", specifier = ">=0.30.0,<1" },
|
||||
{ name = "langchain-core", editable = "libs/core" },
|
||||
]
|
||||
|
||||
@@ -2819,10 +2819,11 @@ typing = []
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = { editable = "libs/partners/openai" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "lark" },
|
||||
{ name = "openai" },
|
||||
{ name = "tiktoken" },
|
||||
]
|
||||
@@ -2830,7 +2831,8 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", editable = "libs/core" },
|
||||
{ name = "openai", specifier = ">=1.86.0,<2.0.0" },
|
||||
{ name = "lark", specifier = ">=1.1.0" },
|
||||
{ name = "openai", specifier = ">=1.99.5,<2.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.7,<1" },
|
||||
]
|
||||
|
||||
@@ -3971,7 +3973,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.88.0"
|
||||
version = "1.99.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -3983,9 +3985,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/ea/bbeef604d1fe0f7e9111745bb8a81362973a95713b28855beb9a9832ab12/openai-1.88.0.tar.gz", hash = "sha256:122d35e42998255cf1fc84560f6ee49a844e65c054cd05d3e42fda506b832bb1", size = 470963, upload-time = "2025-06-17T05:04:45.856Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/16b1b6ee8a62cbfb59057f97f6d9b7bb5ce529047d80bc0b406f65dfdc48/openai-1.99.5.tar.gz", hash = "sha256:aa97ac3326cac7949c5e4ac0274c454c1d19c939760107ae0d3948fc26a924ca", size = 505144, upload-time = "2025-08-08T16:44:46.865Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/03/ef68d77a38dd383cbed7fc898857d394d5a8b0520a35f054e7fe05dc3ac1/openai-1.88.0-py3-none-any.whl", hash = "sha256:7edd7826b3b83f5846562a6f310f040c79576278bf8e3687b30ba05bb5dff978", size = 734293, upload-time = "2025-06-17T05:04:43.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/f2/2472ae020f5156a994710bf926a76915c71bc7b5debf7b81a11506ec8414/openai-1.99.5-py3-none-any.whl", hash = "sha256:4e870f9501b7c36132e2be13313ce3c4d6915a837e7a299c483aab6a6d4412e9", size = 786246, upload-time = "2025-08-08T16:44:45.062Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user