Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 90 additions & 82 deletions instructor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
import importlib
import importlib.util
from typing import Any

__version__ = "1.15.1"

from .mode import Mode
from .processing.multimodal import Image, Audio

from .dsl import (
CitationMixin,
Maybe,
Partial,
IterableModel,
)

from .validation import llm_validator, openai_moderation
from .processing.function_calls import OpenAISchema, openai_schema
from .processing.schema import (
generate_openai_schema,
generate_anthropic_schema,
generate_gemini_schema,
)
from .core.patch import apatch, patch
from .core.client import (
Instructor,
AsyncInstructor,
from_openai,
from_litellm,
)
from .core import hooks
from .utils.providers import Provider
from .auto_client import from_provider
from .batch import BatchProcessor, BatchRequest, BatchJob
from .distil import FinetuneFormat, Instructions

# Backward compatibility: Re-export removed functions
from .processing.response import handle_response_model
from .dsl.parallel import handle_parallel_model
_LAZY_ATTRS: dict[str, tuple[str, str | None]] = {
"Mode": (".mode", "Mode"),
"Image": (".processing.multimodal", "Image"),
"Audio": (".processing.multimodal", "Audio"),
"CitationMixin": (".dsl", "CitationMixin"),
"Maybe": (".dsl", "Maybe"),
"Partial": (".dsl", "Partial"),
"IterableModel": (".dsl", "IterableModel"),
"llm_validator": (".validation", "llm_validator"),
"openai_moderation": (".validation", "openai_moderation"),
"OpenAISchema": (".processing.function_calls", "OpenAISchema"),
"openai_schema": (".processing.function_calls", "openai_schema"),
"generate_openai_schema": (".processing.schema", "generate_openai_schema"),
"generate_anthropic_schema": (
".processing.schema",
"generate_anthropic_schema",
),
"generate_gemini_schema": (".processing.schema", "generate_gemini_schema"),
"apatch": (".core.patch", "apatch"),
"patch": (".core.patch", "patch"),
"Instructor": (".core.client", "Instructor"),
"AsyncInstructor": (".core.client", "AsyncInstructor"),
"from_openai": (".core.client", "from_openai"),
"from_litellm": (".core.client", "from_litellm"),
"hooks": (".core", "hooks"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy hooks resolution fails via getattr on submodule

High Severity

The lazy entry "hooks": (".core", "hooks") tries to resolve instructor.hooks by importing instructor.core and then calling getattr(module, "hooks"). However, instructor.core.__getattr__ only handles names in its _LAZY_ATTRS dict, which contains "Hooks" and "HookName" but not the submodule name "hooks". Unlike the from .core import hooks import statement (which has a submodule fallback), getattr on a module with __getattr__ does not attempt submodule resolution per PEP 562. This causes AttributeError when accessing instructor.hooks unless another lazy attribute has already triggered importing instructor.core.hooks as a side effect. The mapping needs to be (".core.hooks", None) to directly import the submodule.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b743282. Configure here.

"Provider": (".utils.providers", "Provider"),
"from_provider": (".auto_client", "from_provider"),
"BatchProcessor": (".batch", "BatchProcessor"),
"BatchRequest": (".batch", "BatchRequest"),
"BatchJob": (".batch", "BatchJob"),
"FinetuneFormat": (".distil", "FinetuneFormat"),
"Instructions": (".distil", "Instructions"),
"handle_response_model": (".processing.response", "handle_response_model"),
"handle_parallel_model": (".dsl.parallel", "handle_parallel_model"),
"client": (".client", None),
}

__all__ = [
"Instructor",
Expand Down Expand Up @@ -65,90 +70,93 @@
"llm_validator",
"openai_moderation",
"hooks",
"client", # Backward compatibility
# Backward compatibility exports
"client",
"handle_response_model",
"handle_parallel_model",
]

# Backward compatibility: Make instructor.client available as an attribute
# This allows code like `instructor.client.Instructor` to work
from . import client

def _register_optional_attr(
export_name: str,
module_name: str,
attr_name: str,
) -> None:
_LAZY_ATTRS[export_name] = (module_name, attr_name)
__all__.append(export_name)

if importlib.util.find_spec("anthropic") is not None:
from .providers.anthropic.client import from_anthropic

__all__ += ["from_anthropic"]
if importlib.util.find_spec("anthropic") is not None:
_register_optional_attr(
"from_anthropic", ".providers.anthropic.client", "from_anthropic"
)

# Keep from_gemini for backward compatibility but it's deprecated
if (
importlib.util.find_spec("google")
and importlib.util.find_spec("google.generativeai") is not None
):
from .providers.gemini.client import from_gemini

__all__ += ["from_gemini"]
_register_optional_attr("from_gemini", ".providers.gemini.client", "from_gemini")

if importlib.util.find_spec("fireworks") is not None:
from .providers.fireworks.client import from_fireworks

__all__ += ["from_fireworks"]
_register_optional_attr(
"from_fireworks", ".providers.fireworks.client", "from_fireworks"
)

if importlib.util.find_spec("cerebras") is not None:
from .providers.cerebras.client import from_cerebras

__all__ += ["from_cerebras"]
_register_optional_attr(
"from_cerebras", ".providers.cerebras.client", "from_cerebras"
)

if importlib.util.find_spec("groq") is not None:
from .providers.groq.client import from_groq

__all__ += ["from_groq"]
_register_optional_attr("from_groq", ".providers.groq.client", "from_groq")

if importlib.util.find_spec("mistralai") is not None:
from .providers.mistral.client import from_mistral

__all__ += ["from_mistral"]
_register_optional_attr("from_mistral", ".providers.mistral.client", "from_mistral")

if importlib.util.find_spec("cohere") is not None:
from .providers.cohere.client import from_cohere

__all__ += ["from_cohere"]
_register_optional_attr("from_cohere", ".providers.cohere.client", "from_cohere")

if all(importlib.util.find_spec(pkg) for pkg in ("vertexai", "jsonref")):
try:
from .providers.vertexai.client import from_vertexai
except Exception:
# Optional dependency may be present but broken/misconfigured at import time.
# Avoid failing `import instructor` in that case.
pass
else:
__all__ += ["from_vertexai"]
_register_optional_attr(
"from_vertexai", ".providers.vertexai.client", "from_vertexai"
)

if importlib.util.find_spec("boto3") is not None:
from .providers.bedrock.client import from_bedrock

__all__ += ["from_bedrock"]
_register_optional_attr("from_bedrock", ".providers.bedrock.client", "from_bedrock")

if importlib.util.find_spec("writerai") is not None:
from .providers.writer.client import from_writer

__all__ += ["from_writer"]
_register_optional_attr("from_writer", ".providers.writer.client", "from_writer")

if importlib.util.find_spec("xai_sdk") is not None:
from .providers.xai.client import from_xai

__all__ += ["from_xai"]
_register_optional_attr("from_xai", ".providers.xai.client", "from_xai")

if importlib.util.find_spec("openai") is not None:
from .providers.perplexity.client import from_perplexity

__all__ += ["from_perplexity"]
_register_optional_attr(
"from_perplexity", ".providers.perplexity.client", "from_perplexity"
)

if (
importlib.util.find_spec("google")
and importlib.util.find_spec("google.genai") is not None
):
from .providers.genai.client import from_genai
_register_optional_attr("from_genai", ".providers.genai.client", "from_genai")


def __getattr__(name: str) -> Any:
if name not in _LAZY_ATTRS:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

module_name, attr_name = _LAZY_ATTRS[name]
module = importlib.import_module(module_name, __name__)
value: Any

if attr_name is None:
value = module
else:
value = getattr(module, attr_name)

globals()[name] = value
return value


__all__ += ["from_genai"]
def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
89 changes: 45 additions & 44 deletions instructor/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
"""Core components of the instructor package."""

from .client import Instructor, AsyncInstructor, Response, from_openai, from_litellm
from .exceptions import (
InstructorRetryException,
InstructorError,
ConfigurationError,
IncompleteOutputException,
ValidationError,
ProviderError,
ModeError,
ClientError,
AsyncValidationError,
FailedAttempt,
ResponseParsingError,
MultimodalError,
)
from .hooks import Hooks, HookName
from .patch import patch, apatch
from .retry import retry_sync, retry_async

__all__ = [
"Instructor",
"AsyncInstructor",
"Response",
"InstructorRetryException",
"InstructorError",
"ConfigurationError",
"IncompleteOutputException",
"ValidationError",
"ProviderError",
"ModeError",
"ClientError",
"AsyncValidationError",
"FailedAttempt",
"ResponseParsingError",
"MultimodalError",
"Hooks",
"HookName",
"patch",
"apatch",
"from_openai",
"from_litellm",
"retry_sync",
"retry_async",
]
import importlib
from typing import Any

_LAZY_ATTRS: dict[str, tuple[str, str]] = {
"Instructor": (".client", "Instructor"),
"AsyncInstructor": (".client", "AsyncInstructor"),
"Response": (".client", "Response"),
"from_openai": (".client", "from_openai"),
"from_litellm": (".client", "from_litellm"),
"InstructorRetryException": (".exceptions", "InstructorRetryException"),
"InstructorError": (".exceptions", "InstructorError"),
"ConfigurationError": (".exceptions", "ConfigurationError"),
"IncompleteOutputException": (".exceptions", "IncompleteOutputException"),
"ValidationError": (".exceptions", "ValidationError"),
"ProviderError": (".exceptions", "ProviderError"),
"ModeError": (".exceptions", "ModeError"),
"ClientError": (".exceptions", "ClientError"),
"AsyncValidationError": (".exceptions", "AsyncValidationError"),
"FailedAttempt": (".exceptions", "FailedAttempt"),
"ResponseParsingError": (".exceptions", "ResponseParsingError"),
"MultimodalError": (".exceptions", "MultimodalError"),
"Hooks": (".hooks", "Hooks"),
"HookName": (".hooks", "HookName"),
"patch": (".patch", "patch"),
"apatch": (".patch", "apatch"),
"retry_sync": (".retry", "retry_sync"),
"retry_async": (".retry", "retry_async"),
}

__all__ = list(_LAZY_ATTRS)


def __getattr__(name: str) -> Any:
if name not in _LAZY_ATTRS:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

module_name, attr_name = _LAZY_ATTRS[name]
module = importlib.import_module(module_name, __name__)
value = getattr(module, attr_name)
globals()[name] = value
return value


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
61 changes: 33 additions & 28 deletions instructor/processing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
"""Processing components for request/response handling."""

from .function_calls import OpenAISchema, openai_schema
from .multimodal import convert_messages
from .response import (
handle_response_model,
process_response,
process_response_async,
handle_reask_kwargs,
)
from .schema import (
generate_openai_schema,
generate_anthropic_schema,
generate_gemini_schema,
)
from .validators import Validator

__all__ = [
"OpenAISchema",
"openai_schema",
"convert_messages",
"handle_response_model",
"process_response",
"process_response_async",
"handle_reask_kwargs",
"generate_openai_schema",
"generate_anthropic_schema",
"generate_gemini_schema",
"Validator",
]
import importlib
from typing import Any

_LAZY_ATTRS: dict[str, tuple[str, str]] = {
"OpenAISchema": (".function_calls", "OpenAISchema"),
"openai_schema": (".function_calls", "openai_schema"),
"convert_messages": (".multimodal", "convert_messages"),
"handle_response_model": (".response", "handle_response_model"),
"process_response": (".response", "process_response"),
"process_response_async": (".response", "process_response_async"),
"handle_reask_kwargs": (".response", "handle_reask_kwargs"),
"generate_openai_schema": (".schema", "generate_openai_schema"),
"generate_anthropic_schema": (".schema", "generate_anthropic_schema"),
"generate_gemini_schema": (".schema", "generate_gemini_schema"),
"Validator": (".validators", "Validator"),
}

__all__ = list(_LAZY_ATTRS)


def __getattr__(name: str) -> Any:
if name not in _LAZY_ATTRS:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

module_name, attr_name = _LAZY_ATTRS[name]
module = importlib.import_module(module_name, __name__)
value = getattr(module, attr_name)
globals()[name] = value
return value


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
2 changes: 1 addition & 1 deletion instructor/processing/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class User(BaseModel):
from pydantic import BaseModel
from typing_extensions import ParamSpec

from instructor.core.exceptions import InstructorError, ConfigurationError
from ..core.exceptions import InstructorError, ConfigurationError

from ..dsl.iterable import IterableBase
from ..dsl.parallel import ParallelBase
Expand Down
Loading
Loading