참고소스 수정본
This commit is contained in:
0
참고/instructor-main/tests/llm/test_genai/__init__.py
Normal file
0
참고/instructor-main/tests/llm/test_genai/__init__.py
Normal file
40
참고/instructor-main/tests/llm/test_genai/conftest.py
Normal file
40
참고/instructor-main/tests/llm/test_genai/conftest.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# conftest.py
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import instructor
|
||||
|
||||
if not os.getenv("GOOGLE_API_KEY"):
|
||||
pytest.skip(
|
||||
"GOOGLE_API_KEY environment variable not set",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
if not os.getenv("GOOGLE_GENAI_MODEL"):
|
||||
pytest.skip(
|
||||
"GOOGLE_GENAI_MODEL environment variable not set",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
try:
|
||||
from google.genai import Client
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
pytest.skip("google-genai package is not installed", allow_module_level=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client():
|
||||
yield Client()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def aclient():
|
||||
yield Client()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def genai_client():
|
||||
# Use the recommended model for sync client, let the test set the mode
|
||||
return instructor.from_provider(
|
||||
os.getenv("GOOGLE_GENAI_MODEL", "google/gemini-pro"),
|
||||
)
|
||||
129
참고/instructor-main/tests/llm/test_genai/test_decimal.py
Normal file
129
참고/instructor-main/tests/llm/test_genai/test_decimal.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, field_validator
|
||||
import instructor
|
||||
from .util import models, modes
|
||||
|
||||
|
||||
class Receipt(BaseModel):
|
||||
item: str
|
||||
quantity: int
|
||||
price: Decimal
|
||||
total: Decimal
|
||||
|
||||
@field_validator("price", "total", mode="before")
|
||||
@classmethod
|
||||
def parse_decimals(cls, v):
|
||||
if isinstance(v, (str, float, int)):
|
||||
return Decimal(str(v))
|
||||
return v
|
||||
|
||||
|
||||
class Invoice(BaseModel):
|
||||
receipts: list[Receipt]
|
||||
grand_total: Decimal
|
||||
|
||||
@field_validator("grand_total", mode="before")
|
||||
@classmethod
|
||||
def parse_grand_total(cls, v):
|
||||
if isinstance(v, (str, float, int)):
|
||||
return Decimal(str(v))
|
||||
return v
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_decimal_extraction(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I bought 2 apples for $1.50 each and 3 bananas for $0.75 each. Calculate the total.",
|
||||
},
|
||||
],
|
||||
response_model=Invoice,
|
||||
)
|
||||
assert isinstance(response, Invoice)
|
||||
assert len(response.receipts) == 2
|
||||
|
||||
# Check apple receipt
|
||||
apple_receipt = next(
|
||||
(r for r in response.receipts if "apple" in r.item.lower()), None
|
||||
)
|
||||
assert apple_receipt is not None
|
||||
assert apple_receipt.quantity == 2
|
||||
assert isinstance(apple_receipt.price, Decimal)
|
||||
assert isinstance(apple_receipt.total, Decimal)
|
||||
|
||||
# Check banana receipt
|
||||
banana_receipt = next(
|
||||
(r for r in response.receipts if "banana" in r.item.lower()), None
|
||||
)
|
||||
assert banana_receipt is not None
|
||||
assert banana_receipt.quantity == 3
|
||||
assert isinstance(banana_receipt.price, Decimal)
|
||||
assert isinstance(banana_receipt.total, Decimal)
|
||||
|
||||
# Check grand total
|
||||
assert isinstance(response.grand_total, Decimal)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
async def test_decimal_extraction_async(aclient, model, mode):
|
||||
aclient = instructor.from_provider(f"google/{model}", mode=mode, async_client=True)
|
||||
response = await aclient.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I bought 1 coffee for $4.25 and 1 muffin for $2.75. What's the total?",
|
||||
},
|
||||
],
|
||||
response_model=Invoice,
|
||||
)
|
||||
assert isinstance(response, Invoice)
|
||||
assert len(response.receipts) == 2
|
||||
|
||||
# Check that all decimal fields are proper Decimal instances
|
||||
for receipt in response.receipts:
|
||||
assert isinstance(receipt.price, Decimal)
|
||||
assert isinstance(receipt.total, Decimal)
|
||||
|
||||
assert isinstance(response.grand_total, Decimal)
|
||||
|
||||
|
||||
class SimpleProduct(BaseModel):
|
||||
name: str
|
||||
price: Decimal
|
||||
|
||||
@field_validator("price", mode="before")
|
||||
@classmethod
|
||||
def parse_price(cls, v):
|
||||
if isinstance(v, (str, float, int)):
|
||||
return Decimal(str(v))
|
||||
return v
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_simple_decimal_extraction(client, model, mode):
|
||||
"""Test simple decimal extraction to ensure schema conversion works"""
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The laptop costs $999.99",
|
||||
},
|
||||
],
|
||||
response_model=SimpleProduct,
|
||||
)
|
||||
assert isinstance(response, SimpleProduct)
|
||||
assert response.name.lower() == "laptop"
|
||||
assert isinstance(response.price, Decimal)
|
||||
assert response.price == Decimal("999.99")
|
||||
177
참고/instructor-main/tests/llm/test_genai/test_format.py
Normal file
177
참고/instructor-main/tests/llm/test_genai/test_format.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
import instructor
|
||||
from .util import models, modes
|
||||
from itertools import product
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
class Users(BaseModel):
|
||||
users: list[User]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_simple_string_message(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=["Ivan is 28 years old"], # type: ignore
|
||||
response_model=Users,
|
||||
)
|
||||
assert isinstance(response, Users)
|
||||
assert len(response.users) > 0
|
||||
assert response.users[0].name == "Ivan"
|
||||
assert response.users[0].age == 28
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_system_prompt(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Ivan is 28 years old",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Make sure that the response is a list of users",
|
||||
},
|
||||
],
|
||||
response_model=Users,
|
||||
)
|
||||
assert isinstance(response, Users)
|
||||
assert len(response.users) > 0
|
||||
assert response.users[0].name == "Ivan"
|
||||
assert response.users[0].age == 28
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_system_kwarg(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
system="Ivan is 28 years old",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Make sure that the response is a list of users",
|
||||
},
|
||||
],
|
||||
response_model=Users,
|
||||
)
|
||||
assert isinstance(response, Users)
|
||||
assert len(response.users) > 0
|
||||
assert response.users[0].name == "Ivan"
|
||||
assert response.users[0].age == 28
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_system_kwarg_genai(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
system="Ivan is 28 years old",
|
||||
messages=[
|
||||
genai.types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
genai.types.Part.from_text(
|
||||
text="Make sure that the response is a list of users"
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
response_model=Users,
|
||||
)
|
||||
assert isinstance(response, Users)
|
||||
assert len(response.users) > 0
|
||||
assert response.users[0].name == "Ivan"
|
||||
assert response.users[0].age == 28
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_system_prompt_list(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
"Ivan is",
|
||||
" 28 years old",
|
||||
],
|
||||
}, # type: ignore
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Make sure that the response is a list of users",
|
||||
},
|
||||
],
|
||||
response_model=Users,
|
||||
)
|
||||
assert isinstance(response, Users)
|
||||
assert len(response.users) > 0
|
||||
assert response.users[0].name == "Ivan"
|
||||
assert response.users[0].age == 28
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", models)
|
||||
@pytest.mark.parametrize("mode", modes)
|
||||
def test_format_genai_typed(client, model, mode):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
response_model=User,
|
||||
messages=[
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_text(text="Extract {{name}} is {{age}} years old")
|
||||
],
|
||||
), # type: ignore
|
||||
],
|
||||
context={"name": "Jason", "age": 25},
|
||||
)
|
||||
assert isinstance(response, User)
|
||||
assert response.name == "Jason"
|
||||
assert response.age == 25
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model, mode, is_list", product(models, modes, [True, False]))
|
||||
def test_format_string(client, model: str, mode: instructor.Mode, is_list: bool):
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode, async_client=False)
|
||||
|
||||
content = (
|
||||
["Extract {{name}} is {{age}} years old."]
|
||||
if is_list
|
||||
else "Extract {{name}} is {{age}} years old."
|
||||
)
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
}
|
||||
],
|
||||
response_model=User,
|
||||
context={"name": "Jason", "age": 25},
|
||||
)
|
||||
|
||||
assert isinstance(resp, User)
|
||||
assert resp.name == "Jason"
|
||||
assert resp.age == 25
|
||||
232
참고/instructor-main/tests/llm/test_genai/test_invalid_schema.py
Normal file
232
참고/instructor-main/tests/llm/test_genai/test_invalid_schema.py
Normal file
@@ -0,0 +1,232 @@
|
||||
import os
|
||||
import pytest
|
||||
from typing import Optional, Union
|
||||
|
||||
import instructor
|
||||
from pydantic import BaseModel
|
||||
from .util import models, modes
|
||||
from itertools import product
|
||||
from instructor.providers.gemini.utils import map_to_gemini_function_schema
|
||||
|
||||
MODEL = os.getenv("GOOGLE_GENAI_MODEL", "google/gemini-pro")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode,model", product(modes, models))
|
||||
def test_nested(mode, model):
|
||||
"""Test that nested schemas are supported."""
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode)
|
||||
|
||||
class Address(BaseModel):
|
||||
street: str
|
||||
city: str
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
address: Optional[Address] = None
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "John loves to go gardenning with his friends",
|
||||
}
|
||||
],
|
||||
response_model=Person,
|
||||
)
|
||||
|
||||
assert resp.name == "John" # type: ignore
|
||||
assert resp.address is None # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode,model", product(modes, models))
|
||||
def test_union(mode, model):
|
||||
"""Test that union types are now supported with Gemini (issue #1964)."""
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode)
|
||||
|
||||
class UserData(BaseModel):
|
||||
name: str
|
||||
id_value: Union[str, int]
|
||||
|
||||
# Union types are now supported by Google GenAI SDK
|
||||
# See: https://github.com/googleapis/python-genai/issues/447
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "User name is Alice with ID 12345"}],
|
||||
response_model=UserData,
|
||||
)
|
||||
|
||||
assert response.name == "Alice"
|
||||
# The ID could be returned as either str or int
|
||||
assert response.id_value in ["12345", 12345]
|
||||
|
||||
|
||||
def test_optional_types_allowed():
|
||||
"""Test that Optional types are correctly mapped and don't throw errors."""
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: Optional[int] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
schema = User.model_json_schema()
|
||||
# Should not raise an error
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
assert result["properties"]["age"]["nullable"] is True
|
||||
assert result["properties"]["email"]["nullable"] is True
|
||||
assert result["required"] == ["name"]
|
||||
|
||||
|
||||
def test_union_types_allowed_schema():
|
||||
"""Test that Union types are now allowed in schema mapping (issue #1964)."""
|
||||
|
||||
class UserWithUnion(BaseModel):
|
||||
name: str
|
||||
value: Union[int, str]
|
||||
|
||||
schema = UserWithUnion.model_json_schema()
|
||||
|
||||
# Union types are now supported - should not raise
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
# The anyOf structure should be preserved
|
||||
assert "properties" in result
|
||||
assert "value" in result["properties"]
|
||||
assert "anyOf" in result["properties"]["value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode", [instructor.Mode.GENAI_STRUCTURED_OUTPUTS, instructor.Mode.GENAI_TOOLS]
|
||||
)
|
||||
def test_genai_api_call_with_different_types(mode):
|
||||
"""Test actual API call with genai SDK using different types."""
|
||||
|
||||
class UserProfile(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: Optional[str] = None
|
||||
is_premium: bool
|
||||
score: float
|
||||
|
||||
client = instructor.from_provider(MODEL, mode=mode)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Create a user profile for John Doe, 25 years old, premium user with score 85.5",
|
||||
}
|
||||
],
|
||||
response_model=UserProfile,
|
||||
)
|
||||
|
||||
assert isinstance(response, UserProfile)
|
||||
assert response.name == "John Doe"
|
||||
assert response.email is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode", [instructor.Mode.GENAI_STRUCTURED_OUTPUTS, instructor.Mode.GENAI_TOOLS]
|
||||
)
|
||||
def test_genai_api_call_with_nested_models(mode):
|
||||
"""Test API call with nested models (multiple users)."""
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
department: Optional[str] = None
|
||||
|
||||
class UserList(BaseModel):
|
||||
users: list[User]
|
||||
|
||||
client = instructor.from_provider(MODEL, mode=mode)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Create a list of 3 employees: Alice (30, Engineering), Bob (25, Marketing), Charlie (35)",
|
||||
}
|
||||
],
|
||||
response_model=UserList,
|
||||
)
|
||||
|
||||
assert isinstance(response, UserList)
|
||||
assert len(response.users) == 3
|
||||
assert {user.name for user in response.users} == {"Alice", "Bob", "Charlie"}
|
||||
assert {user.age for user in response.users} == {25, 30, 35}
|
||||
assert {user.department for user in response.users} == {
|
||||
None,
|
||||
"Engineering",
|
||||
"Marketing",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"mode", [instructor.Mode.GENAI_STRUCTURED_OUTPUTS, instructor.Mode.GENAI_TOOLS]
|
||||
)
|
||||
async def test_genai_api_call_with_different_types_async(mode):
|
||||
"""Test actual async API call with genai SDK using different types."""
|
||||
|
||||
class UserProfile(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: Optional[str] = None
|
||||
is_premium: bool
|
||||
score: float
|
||||
|
||||
client = instructor.from_provider(MODEL, mode=mode, async_client=True)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Create a user profile for John Doe, 25 years old, premium user with score 85.5",
|
||||
}
|
||||
],
|
||||
response_model=UserProfile,
|
||||
)
|
||||
|
||||
assert isinstance(response, UserProfile)
|
||||
assert response.name == "John Doe"
|
||||
assert response.email is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"mode", [instructor.Mode.GENAI_STRUCTURED_OUTPUTS, instructor.Mode.GENAI_TOOLS]
|
||||
)
|
||||
async def test_genai_api_call_with_nested_models_async(mode):
|
||||
"""Test async API call with nested models (multiple users)."""
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
department: Optional[str] = None
|
||||
|
||||
class UserList(BaseModel):
|
||||
users: list[User]
|
||||
|
||||
client = instructor.from_provider(MODEL, mode=mode, async_client=True)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Create a list of 3 employees: Alice (30, Engineering), Bob (25, Marketing), Charlie (35)",
|
||||
}
|
||||
],
|
||||
response_model=UserList,
|
||||
)
|
||||
|
||||
assert isinstance(response, UserList)
|
||||
assert len(response.users) == 3
|
||||
assert {user.name for user in response.users} == {"Alice", "Bob", "Charlie"}
|
||||
assert {user.age for user in response.users} == {25, 30, 35}
|
||||
assert {user.department for user in response.users} == {
|
||||
None,
|
||||
"Engineering",
|
||||
"Marketing",
|
||||
}
|
||||
36
참고/instructor-main/tests/llm/test_genai/test_reask.py
Normal file
36
참고/instructor-main/tests/llm/test_genai/test_reask.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
import pytest
|
||||
from pydantic import BaseModel, field_validator
|
||||
import instructor
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [instructor.Mode.GENAI_TOOLS])
|
||||
def test_genai_tools_validation_retry_preserves_model_content(mode):
|
||||
"""Ensure GENAI_TOOLS validation retries are wired end-to-end."""
|
||||
from instructor.core.exceptions import InstructorRetryException
|
||||
|
||||
model = os.getenv("GOOGLE_GENAI_MODEL", "gemini-2.0-flash")
|
||||
|
||||
class AlwaysInvalid(BaseModel):
|
||||
value: int
|
||||
|
||||
@field_validator("value")
|
||||
@classmethod
|
||||
def always_fail(cls, v: int) -> int: # noqa: ARG003
|
||||
raise ValueError("force retry for reask validation coverage")
|
||||
|
||||
client = instructor.from_provider(f"google/{model}", mode=mode)
|
||||
with pytest.raises(InstructorRetryException) as exc_info:
|
||||
client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Return any integer value",
|
||||
}
|
||||
],
|
||||
response_model=AlwaysInvalid,
|
||||
max_retries=2,
|
||||
)
|
||||
|
||||
assert exc_info.value.n_attempts == 2
|
||||
@@ -0,0 +1,347 @@
|
||||
"""Test schema conversion functions for Gemini."""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from instructor.providers.gemini.utils import (
|
||||
map_to_gemini_function_schema,
|
||||
verify_no_unions,
|
||||
)
|
||||
|
||||
|
||||
class Priority(Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
is_active: bool
|
||||
|
||||
|
||||
class OptionalModel(BaseModel):
|
||||
name: str
|
||||
age: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class EnumModel(BaseModel):
|
||||
name: str
|
||||
priority: Priority
|
||||
|
||||
|
||||
class NestedModel(BaseModel):
|
||||
name: str
|
||||
items: list[str]
|
||||
details: SimpleModel
|
||||
|
||||
|
||||
def test_simple_schema_conversion():
|
||||
"""Test conversion strips extra pydantic fields like 'title'."""
|
||||
schema = SimpleModel.model_json_schema()
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
# Input has 'title' fields that should be stripped out
|
||||
assert "title" in schema
|
||||
assert "title" in schema["properties"]["name"]
|
||||
|
||||
# Output should be clean without title fields
|
||||
expected = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"is_active": {"type": "boolean"},
|
||||
},
|
||||
"required": ["name", "age", "is_active"],
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_optional_schema_conversion():
|
||||
"""Test conversion transforms anyOf[T, null] to nullable fields."""
|
||||
schema = OptionalModel.model_json_schema()
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
# Input should have anyOf with null type for optional fields
|
||||
assert schema["properties"]["age"]["anyOf"] == [
|
||||
{"type": "integer"},
|
||||
{"type": "null"},
|
||||
]
|
||||
assert schema["properties"]["description"]["anyOf"] == [
|
||||
{"type": "string"},
|
||||
{"type": "null"},
|
||||
]
|
||||
|
||||
# Output should convert to nullable: true
|
||||
expected = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer", "nullable": True},
|
||||
"description": {"type": "string", "nullable": True},
|
||||
},
|
||||
"required": ["name"],
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_enum_schema_conversion():
|
||||
"""Test conversion resolves $refs and adds format: enum."""
|
||||
schema = EnumModel.model_json_schema()
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
# Input should have $ref and $defs
|
||||
assert schema["properties"]["priority"]["$ref"] == "#/$defs/Priority"
|
||||
assert "$defs" in schema
|
||||
assert schema["$defs"]["Priority"]["enum"] == ["low", "medium", "high"]
|
||||
|
||||
# Output should resolve the ref and add format: enum
|
||||
expected = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"format": "enum",
|
||||
},
|
||||
},
|
||||
"required": ["name", "priority"],
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_nested_schema_conversion():
|
||||
"""Test conversion of schema with nested objects."""
|
||||
schema = NestedModel.model_json_schema()
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
expected = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"items": {"type": "array", "items": {"type": "string"}},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"is_active": {"type": "boolean"},
|
||||
},
|
||||
"required": ["name", "age", "is_active"],
|
||||
},
|
||||
},
|
||||
"required": ["name", "items", "details"],
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_verify_no_unions_valid():
|
||||
"""Test verify_no_unions with valid schemas."""
|
||||
# Simple schema should pass
|
||||
simple_schema = SimpleModel.model_json_schema()
|
||||
assert verify_no_unions(simple_schema) is True
|
||||
|
||||
# Optional schema should pass (Optional[T] is Union[T, None])
|
||||
optional_schema = OptionalModel.model_json_schema()
|
||||
assert verify_no_unions(optional_schema) is True
|
||||
|
||||
|
||||
def test_verify_no_unions_invalid():
|
||||
"""Test verify_no_unions with union schemas (now allowed)."""
|
||||
# Create a schema with a true union (not just Optional)
|
||||
invalid_schema = {
|
||||
"type": "object",
|
||||
"properties": {"value": {"anyOf": [{"type": "string"}, {"type": "integer"}]}},
|
||||
}
|
||||
assert verify_no_unions(invalid_schema) is True
|
||||
|
||||
|
||||
def test_schema_without_refs():
|
||||
"""Test schema conversion without $refs."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}, "count": {"type": "integer"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
expected = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}, "count": {"type": "integer"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_schema_with_description():
|
||||
"""Test schema conversion preserves descriptions."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"description": "A test object",
|
||||
"properties": {"name": {"type": "string", "description": "The name field"}},
|
||||
}
|
||||
|
||||
result = map_to_gemini_function_schema(schema)
|
||||
|
||||
expected = {
|
||||
"type": "object",
|
||||
"description": "A test object",
|
||||
"properties": {"name": {"type": "string", "description": "The name field"}},
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_union_type_raises_error():
|
||||
"""Test that union types are allowed in schema conversion."""
|
||||
# Create a model with a true union type (not Optional or Decimal)
|
||||
union_schema = {
|
||||
"type": "object",
|
||||
"properties": {"value": {"anyOf": [{"type": "string"}, {"type": "integer"}]}},
|
||||
}
|
||||
|
||||
result = map_to_gemini_function_schema(union_schema)
|
||||
assert result["properties"]["value"]["anyOf"] == [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
]
|
||||
|
||||
|
||||
def test_verify_no_unions_allows_optional():
|
||||
"""Test that verify_no_unions allows Optional types."""
|
||||
# Schema with Optional field (Union with null)
|
||||
optional_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"anyOf": [{"type": "integer"}, {"type": "null"}]},
|
||||
},
|
||||
}
|
||||
|
||||
assert verify_no_unions(optional_schema) is True
|
||||
|
||||
|
||||
def test_verify_no_unions_allows_decimal():
|
||||
"""Test that verify_no_unions allows Decimal types (string | number)."""
|
||||
# Schema with Decimal field (Union of string and number)
|
||||
decimal_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {"anyOf": [{"type": "number"}, {"type": "string"}]},
|
||||
"price": {
|
||||
"anyOf": [{"type": "string"}, {"type": "number"}]
|
||||
}, # Order shouldn't matter
|
||||
},
|
||||
}
|
||||
|
||||
assert verify_no_unions(decimal_schema) is True
|
||||
|
||||
|
||||
def test_verify_no_unions_rejects_other_unions():
|
||||
"""Test that verify_no_unions allows non-Optional unions."""
|
||||
# Schema with unsupported union type (string | integer)
|
||||
union_schema = {
|
||||
"type": "object",
|
||||
"properties": {"value": {"anyOf": [{"type": "string"}, {"type": "integer"}]}},
|
||||
}
|
||||
|
||||
assert verify_no_unions(union_schema) is True
|
||||
|
||||
|
||||
def test_verify_no_unions_rejects_complex_unions():
|
||||
"""Test that verify_no_unions allows complex union types."""
|
||||
# Schema with more than 2 types in union
|
||||
complex_union_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}, {"type": "boolean"}]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
assert verify_no_unions(complex_union_schema) is True
|
||||
|
||||
|
||||
def test_verify_no_unions_nested_schemas():
|
||||
"""Test that verify_no_unions allows unions in nested schemas."""
|
||||
# Schema with nested object containing Decimal and Optional fields
|
||||
nested_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"receipt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"anyOf": [{"type": "number"}, {"type": "string"}]
|
||||
}, # Decimal - should pass
|
||||
"notes": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}]
|
||||
}, # Optional - should pass
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
assert verify_no_unions(nested_schema) is True
|
||||
|
||||
# Schema with nested object containing unsupported union
|
||||
bad_nested_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"receipt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"anyOf": [{"type": "number"}, {"type": "string"}]
|
||||
}, # Decimal - should pass
|
||||
"status": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
}, # Bad union - should fail
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
assert verify_no_unions(bad_nested_schema) is True
|
||||
|
||||
|
||||
def test_decimal_schema_conversion_succeeds():
|
||||
"""Test that Decimal types (string | number) are successfully converted."""
|
||||
# Schema representing a Receipt with Decimal total field
|
||||
decimal_schema = {
|
||||
"type": "object",
|
||||
"title": "Receipt",
|
||||
"properties": {
|
||||
"total": {
|
||||
"anyOf": [{"type": "number"}, {"type": "string"}],
|
||||
"title": "Total",
|
||||
}
|
||||
},
|
||||
"required": ["total"],
|
||||
}
|
||||
|
||||
# This should not raise an error now
|
||||
result = map_to_gemini_function_schema(decimal_schema)
|
||||
|
||||
# The conversion should succeed and preserve the anyOf structure
|
||||
assert result["type"] == "object"
|
||||
assert result["properties"]["total"]["anyOf"] == [
|
||||
{"type": "number"},
|
||||
{"type": "string"},
|
||||
]
|
||||
assert result["required"] == ["total"]
|
||||
# Title should be stripped out
|
||||
assert "title" not in result
|
||||
assert "title" not in result["properties"]["total"]
|
||||
359
참고/instructor-main/tests/llm/test_genai/test_utils.py
Normal file
359
참고/instructor-main/tests/llm/test_genai/test_utils.py
Normal file
@@ -0,0 +1,359 @@
|
||||
from instructor.providers.gemini.utils import update_genai_kwargs
|
||||
|
||||
|
||||
def test_update_genai_kwargs_basic():
|
||||
"""Test basic parameter mapping from OpenAI to Gemini format."""
|
||||
kwargs = {
|
||||
"generation_config": {
|
||||
"max_tokens": 100,
|
||||
"temperature": 0.7,
|
||||
"n": 2,
|
||||
"top_p": 0.9,
|
||||
"stop": ["END"],
|
||||
"seed": 42,
|
||||
"presence_penalty": 0.1,
|
||||
"frequency_penalty": 0.2,
|
||||
}
|
||||
}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that OpenAI parameters were mapped to Gemini equivalents
|
||||
assert result["max_output_tokens"] == 100
|
||||
assert result["temperature"] == 0.7
|
||||
assert result["candidate_count"] == 2
|
||||
assert result["top_p"] == 0.9
|
||||
assert result["stop_sequences"] == ["END"]
|
||||
assert result["seed"] == 42
|
||||
assert result["presence_penalty"] == 0.1
|
||||
assert result["frequency_penalty"] == 0.2
|
||||
|
||||
|
||||
def test_update_genai_kwargs_safety_settings():
|
||||
"""Test that safety settings are properly configured."""
|
||||
from google.genai.types import HarmCategory, HarmBlockThreshold
|
||||
|
||||
# Exclude JAILBREAK category as it's only for Vertex AI, not google.genai
|
||||
excluded_categories = {HarmCategory.HARM_CATEGORY_UNSPECIFIED}
|
||||
if hasattr(HarmCategory, "HARM_CATEGORY_JAILBREAK"):
|
||||
excluded_categories.add(HarmCategory.HARM_CATEGORY_JAILBREAK)
|
||||
|
||||
supported_categories = [
|
||||
c
|
||||
for c in HarmCategory
|
||||
if c not in excluded_categories
|
||||
and not c.name.startswith("HARM_CATEGORY_IMAGE_")
|
||||
]
|
||||
|
||||
kwargs = {}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that safety_settings is configured as a list
|
||||
assert "safety_settings" in result
|
||||
assert isinstance(result["safety_settings"], list)
|
||||
|
||||
# Should have one entry for each supported HarmCategory
|
||||
assert len(result["safety_settings"]) == len(supported_categories)
|
||||
|
||||
# Each entry should be a dict with category and threshold
|
||||
for setting in result["safety_settings"]:
|
||||
assert isinstance(setting, dict)
|
||||
assert "category" in setting
|
||||
assert "threshold" in setting
|
||||
assert setting["threshold"] == HarmBlockThreshold.OFF # Default
|
||||
|
||||
|
||||
def test_update_genai_kwargs_with_custom_safety_settings():
|
||||
"""Test that custom safety settings are properly handled."""
|
||||
from google.genai.types import HarmCategory, HarmBlockThreshold
|
||||
|
||||
# Exclude JAILBREAK category as it's only for Vertex AI, not google.genai
|
||||
excluded_categories = {HarmCategory.HARM_CATEGORY_UNSPECIFIED}
|
||||
if hasattr(HarmCategory, "HARM_CATEGORY_JAILBREAK"):
|
||||
excluded_categories.add(HarmCategory.HARM_CATEGORY_JAILBREAK)
|
||||
|
||||
supported_categories = [
|
||||
c
|
||||
for c in HarmCategory
|
||||
if c not in excluded_categories
|
||||
and not c.name.startswith("HARM_CATEGORY_IMAGE_")
|
||||
]
|
||||
|
||||
# Test with one category that exists in safety_settings
|
||||
custom_safety = {
|
||||
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
}
|
||||
kwargs = {"safety_settings": custom_safety}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that safety_settings is configured as a list
|
||||
assert "safety_settings" in result
|
||||
assert isinstance(result["safety_settings"], list)
|
||||
|
||||
# Should have one entry for each supported HarmCategory
|
||||
assert len(result["safety_settings"]) == len(supported_categories)
|
||||
|
||||
for setting in result["safety_settings"]:
|
||||
if setting["category"] == HarmCategory.HARM_CATEGORY_HATE_SPEECH:
|
||||
assert setting["threshold"] == HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
|
||||
# Other categories should use the default
|
||||
for setting in result["safety_settings"]:
|
||||
if setting["category"] != HarmCategory.HARM_CATEGORY_HATE_SPEECH:
|
||||
assert setting["threshold"] == HarmBlockThreshold.OFF
|
||||
|
||||
|
||||
def test_update_genai_kwargs_safety_settings_excludes_image_categories():
|
||||
"""IMAGE_* harm categories must never be sent to the standard Gemini API.
|
||||
|
||||
They are only supported by Vertex AI and cause 400 INVALID_ARGUMENT errors.
|
||||
See: https://github.com/567-labs/instructor/issues/2146
|
||||
"""
|
||||
from google.genai import types
|
||||
|
||||
kwargs = {
|
||||
"contents": [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[types.Part.from_bytes(data=b"123", mime_type="image/png")],
|
||||
)
|
||||
]
|
||||
}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
assert "safety_settings" in result
|
||||
assert isinstance(result["safety_settings"], list)
|
||||
# No IMAGE_* categories should be present, even with image content
|
||||
for setting in result["safety_settings"]:
|
||||
assert not setting["category"].name.startswith("HARM_CATEGORY_IMAGE_"), (
|
||||
f"IMAGE_ category {setting['category'].name} must not be sent to the "
|
||||
"standard Gemini API"
|
||||
)
|
||||
|
||||
|
||||
def test_update_genai_kwargs_text_categories_with_image_content():
|
||||
"""Even with image content, only text harm categories should be used."""
|
||||
from google.genai import types
|
||||
from google.genai.types import HarmBlockThreshold, HarmCategory
|
||||
|
||||
custom_safety = {
|
||||
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
}
|
||||
|
||||
kwargs = {
|
||||
"contents": [
|
||||
types.Content(
|
||||
role="user",
|
||||
parts=[types.Part.from_bytes(data=b"123", mime_type="image/png")],
|
||||
)
|
||||
],
|
||||
"safety_settings": custom_safety,
|
||||
}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Custom threshold should be preserved for text category
|
||||
found_hate_speech = False
|
||||
for setting in result["safety_settings"]:
|
||||
assert not setting["category"].name.startswith("HARM_CATEGORY_IMAGE_")
|
||||
if setting["category"] == HarmCategory.HARM_CATEGORY_HATE_SPEECH:
|
||||
assert setting["threshold"] == HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
found_hate_speech = True
|
||||
assert found_hate_speech, "HARM_CATEGORY_HATE_SPEECH should be in safety_settings"
|
||||
|
||||
|
||||
def test_update_genai_kwargs_none_values():
|
||||
"""Test that None values are not set in the result."""
|
||||
kwargs = {
|
||||
"generation_config": {
|
||||
"max_tokens": None,
|
||||
"temperature": 0.7,
|
||||
"n": None,
|
||||
}
|
||||
}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that None values are not included
|
||||
assert "max_output_tokens" not in result
|
||||
assert "candidate_count" not in result
|
||||
assert result["temperature"] == 0.7
|
||||
|
||||
|
||||
def test_update_genai_kwargs_empty():
|
||||
"""Test with empty kwargs."""
|
||||
kwargs = {}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Should still have safety_settings configured
|
||||
assert "safety_settings" in result
|
||||
|
||||
|
||||
def test_update_genai_kwargs_preserves_original():
|
||||
"""Test that the function doesn't modify the original kwargs."""
|
||||
original_kwargs = {
|
||||
"generation_config": {
|
||||
"max_tokens": 100,
|
||||
"temperature": 0.7,
|
||||
},
|
||||
"safety_settings": {},
|
||||
}
|
||||
kwargs = original_kwargs.copy()
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# The function should not modify the original kwargs (works on a copy)
|
||||
assert kwargs == original_kwargs
|
||||
# But result should have the mapped parameters
|
||||
assert "max_output_tokens" in result
|
||||
assert "temperature" in result
|
||||
|
||||
|
||||
def test_update_genai_kwargs_thinking_config():
|
||||
"""Test that thinking_config is properly passed through."""
|
||||
|
||||
thinking_config = {"thinking_budget": 1024}
|
||||
kwargs = {"thinking_config": thinking_config}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that thinking_config is passed through unchanged
|
||||
assert "thinking_config" in result
|
||||
assert result["thinking_config"] == thinking_config
|
||||
|
||||
|
||||
def test_update_genai_kwargs_thinking_config_none():
|
||||
"""Test that None thinking_config is not included in result."""
|
||||
kwargs = {"thinking_config": None}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that thinking_config is not included when None
|
||||
assert "thinking_config" not in result
|
||||
|
||||
|
||||
def test_update_genai_kwargs_no_thinking_config():
|
||||
"""Test that missing thinking_config doesn't affect other parameters."""
|
||||
kwargs = {
|
||||
"generation_config": {
|
||||
"max_tokens": 100,
|
||||
"temperature": 0.7,
|
||||
}
|
||||
}
|
||||
base_config = {}
|
||||
|
||||
result = update_genai_kwargs(kwargs, base_config)
|
||||
|
||||
# Check that normal parameters still work
|
||||
assert result["max_output_tokens"] == 100
|
||||
assert result["temperature"] == 0.7
|
||||
# Check that thinking_config is not included when not provided
|
||||
assert "thinking_config" not in result
|
||||
|
||||
|
||||
def test_handle_genai_structured_outputs_thinking_config_in_config():
|
||||
"""Test that thinking_config inside config parameter is extracted (issue #1966)."""
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
|
||||
from instructor.providers.gemini.utils import handle_genai_structured_outputs
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
text: str
|
||||
|
||||
# Create a mock ThinkingConfig-like object
|
||||
thinking_config = types.ThinkingConfig(thinking_budget=1024)
|
||||
|
||||
# User passes thinking_config inside config parameter
|
||||
user_config = types.GenerateContentConfig(
|
||||
temperature=0.7,
|
||||
max_output_tokens=1000,
|
||||
thinking_config=thinking_config,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"config": user_config,
|
||||
}
|
||||
|
||||
_, result_kwargs = handle_genai_structured_outputs(SimpleModel, kwargs)
|
||||
|
||||
# The resulting config should include thinking_config
|
||||
assert "config" in result_kwargs
|
||||
assert result_kwargs["config"].thinking_config is not None
|
||||
assert result_kwargs["config"].thinking_config.thinking_budget == 1024
|
||||
|
||||
|
||||
def test_handle_genai_structured_outputs_thinking_config_kwarg_priority():
|
||||
"""Test that thinking_config as separate kwarg takes priority over config.thinking_config."""
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
|
||||
from instructor.providers.gemini.utils import handle_genai_structured_outputs
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
text: str
|
||||
|
||||
# User passes thinking_config both ways - kwarg should take priority
|
||||
config_thinking = types.ThinkingConfig(thinking_budget=500)
|
||||
kwarg_thinking = types.ThinkingConfig(thinking_budget=2000)
|
||||
|
||||
user_config = types.GenerateContentConfig(
|
||||
temperature=0.7,
|
||||
thinking_config=config_thinking,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"config": user_config,
|
||||
"thinking_config": kwarg_thinking,
|
||||
}
|
||||
|
||||
_, result_kwargs = handle_genai_structured_outputs(SimpleModel, kwargs)
|
||||
|
||||
# The kwarg thinking_config should take priority
|
||||
assert result_kwargs["config"].thinking_config.thinking_budget == 2000
|
||||
|
||||
|
||||
def test_handle_genai_tools_thinking_config_in_config():
|
||||
"""Test that thinking_config inside config parameter is extracted for tools mode (issue #1966)."""
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
|
||||
from instructor.providers.gemini.utils import handle_genai_tools
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
text: str
|
||||
|
||||
thinking_config = types.ThinkingConfig(thinking_budget=1024)
|
||||
|
||||
user_config = types.GenerateContentConfig(
|
||||
temperature=0.7,
|
||||
thinking_config=thinking_config,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"config": user_config,
|
||||
}
|
||||
|
||||
_, result_kwargs = handle_genai_tools(SimpleModel, kwargs)
|
||||
|
||||
# The resulting config should include thinking_config
|
||||
assert "config" in result_kwargs
|
||||
assert result_kwargs["config"].thinking_config is not None
|
||||
assert result_kwargs["config"].thinking_config.thinking_budget == 1024
|
||||
5
참고/instructor-main/tests/llm/test_genai/util.py
Normal file
5
참고/instructor-main/tests/llm/test_genai/util.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
import instructor
|
||||
|
||||
models = [os.getenv("GOOGLE_GENAI_MODEL", "google/gemini-2.0-flash")]
|
||||
modes = [instructor.Mode.GENAI_STRUCTURED_OUTPUTS]
|
||||
Reference in New Issue
Block a user