참고소스 수정본
This commit is contained in:
19
참고/instructor-main/instructor/dsl/__init__.py
Normal file
19
참고/instructor-main/instructor/dsl/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .iterable import IterableModel
|
||||
from .maybe import Maybe
|
||||
from .partial import Partial
|
||||
from .citation import CitationMixin
|
||||
from .simple_type import is_simple_type, ModelAdapter
|
||||
from .response_list import ListResponse, ResponseList
|
||||
from . import validators # Backwards compatibility module
|
||||
|
||||
__all__ = [ # noqa: F405
|
||||
"CitationMixin",
|
||||
"IterableModel",
|
||||
"ListResponse",
|
||||
"Maybe",
|
||||
"Partial",
|
||||
"ResponseList",
|
||||
"is_simple_type",
|
||||
"ModelAdapter",
|
||||
"validators",
|
||||
]
|
||||
97
참고/instructor-main/instructor/dsl/citation.py
Normal file
97
참고/instructor-main/instructor/dsl/citation.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from pydantic import BaseModel, Field, model_validator, ValidationInfo
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
class CitationMixin(BaseModel):
|
||||
"""
|
||||
Helpful mixing that can use `validation_context={"context": context}` in `from_response` to find the span of the substring_phrase in the context.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import CitationMixin
|
||||
|
||||
class User(BaseModel):
|
||||
name: str = Field(description="The name of the person")
|
||||
age: int = Field(description="The age of the person")
|
||||
role: str = Field(description="The role of the person")
|
||||
|
||||
|
||||
context = "Betty was a student. Jason was a student. Jason is 20 years old"
|
||||
|
||||
user = openai.ChatCompletion.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Extract jason from {context}",
|
||||
},
|
||||
response_model=User,
|
||||
validation_context={"context": context},
|
||||
]
|
||||
)
|
||||
|
||||
for quote in user.substring_quotes:
|
||||
assert quote in context
|
||||
|
||||
print(user.model_dump())
|
||||
```
|
||||
|
||||
## Result
|
||||
```
|
||||
{
|
||||
"name": "Jason Liu",
|
||||
"age": 20,
|
||||
"role": "student",
|
||||
"substring_quotes": [
|
||||
"Jason was a student",
|
||||
"Jason is 20 years old",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
substring_quotes: list[str] = Field(
|
||||
description="List of unique and specific substrings of the quote that was used to answer the question.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after") # type: ignore[misc]
|
||||
def validate_sources(self, info: ValidationInfo) -> "CitationMixin":
|
||||
"""
|
||||
For each substring_phrase, find the span of the substring_phrase in the context.
|
||||
If the span is not found, remove the substring_phrase from the list.
|
||||
"""
|
||||
if info.context is None:
|
||||
return self
|
||||
|
||||
# Get the context from the info
|
||||
text_chunks = info.context.get("context", None)
|
||||
|
||||
# Get the spans of the substring_phrase in the context
|
||||
spans = list(self.get_spans(text_chunks))
|
||||
# Replace the substring_phrase with the actual substring
|
||||
self.substring_quotes = [text_chunks[span[0] : span[1]] for span in spans]
|
||||
return self
|
||||
|
||||
def _get_span(
|
||||
self, quote: str, context: str, errs: int = 5
|
||||
) -> Generator[tuple[int, int], None, None]:
|
||||
import regex
|
||||
|
||||
minor = quote
|
||||
major = context
|
||||
|
||||
errs_ = 0
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
while s is None and errs_ <= errs:
|
||||
errs_ += 1
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
|
||||
if s is not None:
|
||||
yield from s.spans()
|
||||
|
||||
def get_spans(self, context: str) -> Generator[tuple[int, int], None, None]:
|
||||
for quote in self.substring_quotes:
|
||||
yield from self._get_span(quote, context)
|
||||
681
참고/instructor-main/instructor/dsl/iterable.py
Normal file
681
참고/instructor-main/instructor/dsl/iterable.py
Normal file
@@ -0,0 +1,681 @@
|
||||
from collections.abc import AsyncGenerator, Generator, Iterable
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
Optional,
|
||||
cast,
|
||||
get_origin,
|
||||
get_args,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import json
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from ..mode import Mode
|
||||
from ..utils import extract_json_from_stream, extract_json_from_stream_async
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class IterableBase:
|
||||
task_type: ClassVar[Optional[type[BaseModel]]] = None
|
||||
|
||||
@classmethod
|
||||
def from_streaming_response(
|
||||
cls, completion: Iterable[Any], mode: Mode, **kwargs: Any
|
||||
) -> Generator[BaseModel, None, None]: # noqa: ARG003
|
||||
json_chunks = cls.extract_json(completion, mode)
|
||||
|
||||
if mode in {Mode.MD_JSON, Mode.GEMINI_TOOLS}:
|
||||
json_chunks = extract_json_from_stream(json_chunks)
|
||||
|
||||
if mode in {Mode.VERTEXAI_TOOLS, Mode.MISTRAL_TOOLS}:
|
||||
response = next(json_chunks)
|
||||
if not response:
|
||||
return
|
||||
|
||||
json_response = json.loads(response)
|
||||
if not json_response["tasks"]:
|
||||
return
|
||||
|
||||
for item in json_response["tasks"]:
|
||||
yield cls.extract_cls_task_type(json.dumps(item), **kwargs)
|
||||
|
||||
yield from cls.tasks_from_chunks(json_chunks, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def from_streaming_response_async(
|
||||
cls, completion: AsyncGenerator[Any, None], mode: Mode, **kwargs: Any
|
||||
) -> AsyncGenerator[BaseModel, None]:
|
||||
json_chunks = cls.extract_json_async(completion, mode)
|
||||
|
||||
if mode in {Mode.MD_JSON, Mode.GEMINI_TOOLS}:
|
||||
json_chunks = extract_json_from_stream_async(json_chunks)
|
||||
|
||||
if mode in {Mode.MISTRAL_TOOLS, Mode.VERTEXAI_TOOLS}:
|
||||
async for item in cls.tasks_from_mistral_chunks(json_chunks, **kwargs):
|
||||
yield item
|
||||
else:
|
||||
async for item in cls.tasks_from_chunks_async(json_chunks, **kwargs):
|
||||
yield item
|
||||
|
||||
@classmethod
|
||||
async def tasks_from_mistral_chunks(
|
||||
cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
|
||||
) -> AsyncGenerator[BaseModel, None]:
|
||||
"""Process streaming chunks from Mistral and VertexAI.
|
||||
|
||||
Handles the specific JSON format used by these providers when streaming."""
|
||||
|
||||
async for chunk in json_chunks:
|
||||
if not chunk:
|
||||
continue
|
||||
json_response = json.loads(chunk)
|
||||
if not json_response["tasks"]:
|
||||
continue
|
||||
|
||||
for item in json_response["tasks"]:
|
||||
obj = cls.extract_cls_task_type(json.dumps(item), **kwargs)
|
||||
yield obj
|
||||
|
||||
@classmethod
|
||||
def tasks_from_chunks(
|
||||
cls, json_chunks: Iterable[str], **kwargs: Any
|
||||
) -> Generator[BaseModel, None, None]:
|
||||
started = False
|
||||
potential_object = ""
|
||||
for chunk in json_chunks:
|
||||
potential_object += chunk
|
||||
if not started:
|
||||
if "[" in chunk:
|
||||
started = True
|
||||
potential_object = chunk[chunk.find("[") + 1 :]
|
||||
|
||||
while True:
|
||||
task_json, potential_object = cls.get_object(potential_object, 0)
|
||||
if task_json:
|
||||
assert cls.task_type is not None
|
||||
obj = cls.extract_cls_task_type(task_json, **kwargs)
|
||||
yield obj
|
||||
else:
|
||||
break
|
||||
|
||||
@classmethod
|
||||
async def tasks_from_chunks_async(
|
||||
cls, json_chunks: AsyncGenerator[str, None], **kwargs: Any
|
||||
) -> AsyncGenerator[BaseModel, None]:
|
||||
started = False
|
||||
potential_object = ""
|
||||
async for chunk in json_chunks:
|
||||
potential_object += chunk
|
||||
if not started:
|
||||
if "[" in chunk:
|
||||
started = True
|
||||
potential_object = chunk[chunk.find("[") + 1 :]
|
||||
|
||||
while True:
|
||||
task_json, potential_object = cls.get_object(potential_object, 0)
|
||||
if task_json:
|
||||
assert cls.task_type is not None
|
||||
obj = cls.extract_cls_task_type(task_json, **kwargs)
|
||||
yield obj
|
||||
else:
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def extract_cls_task_type(
|
||||
cls,
|
||||
task_json: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
assert cls.task_type is not None
|
||||
if get_origin(cls.task_type) is Union:
|
||||
union_members = get_args(cls.task_type)
|
||||
for member in union_members:
|
||||
try:
|
||||
obj = member.model_validate_json(task_json, **kwargs)
|
||||
return obj
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return cls.task_type.model_validate_json(task_json, **kwargs)
|
||||
raise ValueError(
|
||||
f"Failed to extract task type with {task_json} for {cls.task_type}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def extract_json(
|
||||
completion: Iterable[Any], mode: Mode
|
||||
) -> Generator[str, None, None]:
|
||||
json_started = False
|
||||
for chunk in completion:
|
||||
try:
|
||||
if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
|
||||
event_type = getattr(chunk, "event_type", None)
|
||||
if event_type == "text-generation":
|
||||
if text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif event_type == "tool-calls-chunk":
|
||||
delta = getattr(chunk, "tool_call_delta", None)
|
||||
args = getattr(delta, "parameters", None) or getattr(
|
||||
delta, "text", None
|
||||
)
|
||||
if args:
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (args.find("{"), args.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
elif text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif event_type == "tool-calls-generation":
|
||||
tool_calls = getattr(chunk, "tool_calls", None)
|
||||
if tool_calls:
|
||||
args = json.dumps(tool_calls[0].parameters)
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (args.find("{"), args.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
elif text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
else:
|
||||
chunk_type = getattr(chunk, "type", None)
|
||||
if chunk_type == "content-delta":
|
||||
delta = getattr(chunk, "delta", None)
|
||||
message = getattr(delta, "message", None)
|
||||
content = getattr(message, "content", None)
|
||||
if text := getattr(content, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (
|
||||
text.find("{"),
|
||||
text.find("["),
|
||||
)
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif chunk_type == "tool-call-delta":
|
||||
delta = getattr(chunk, "delta", None)
|
||||
message = getattr(delta, "message", None)
|
||||
tool_calls = getattr(message, "tool_calls", None)
|
||||
function = getattr(tool_calls, "function", None)
|
||||
if args := getattr(function, "arguments", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (
|
||||
args.find("{"),
|
||||
args.find("["),
|
||||
)
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
if mode == Mode.ANTHROPIC_JSON:
|
||||
if json_chunk := chunk.delta.text:
|
||||
yield json_chunk
|
||||
if mode == Mode.ANTHROPIC_TOOLS:
|
||||
yield chunk.delta.partial_json
|
||||
if mode == Mode.GEMINI_JSON:
|
||||
yield chunk.text
|
||||
if mode == Mode.VERTEXAI_JSON:
|
||||
yield chunk.candidates[0].content.parts[0].text
|
||||
if mode == Mode.VERTEXAI_TOOLS:
|
||||
yield json.dumps(
|
||||
chunk.candidates[0].content.parts[0].function_call.args
|
||||
)
|
||||
if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
|
||||
yield chunk.data.choices[0].delta.content
|
||||
if mode == Mode.MISTRAL_TOOLS:
|
||||
if not chunk.data.choices[0].delta.tool_calls:
|
||||
continue
|
||||
yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
|
||||
|
||||
if mode in {Mode.GENAI_TOOLS}:
|
||||
yield json.dumps(
|
||||
chunk.candidates[0].content.parts[0].function_call.args
|
||||
)
|
||||
if mode in {Mode.GENAI_STRUCTURED_OUTPUTS}:
|
||||
yield chunk.candidates[0].content.parts[0].text
|
||||
|
||||
if mode in {Mode.GEMINI_TOOLS}:
|
||||
resp = chunk.candidates[0].content.parts[0].function_call
|
||||
resp_dict = type(resp).to_dict(resp) # type:ignore
|
||||
|
||||
if "args" in resp_dict:
|
||||
yield json.dumps(resp_dict["args"])
|
||||
|
||||
if mode in {
|
||||
Mode.RESPONSES_TOOLS,
|
||||
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
|
||||
}:
|
||||
from openai.types.responses import (
|
||||
ResponseFunctionCallArgumentsDeltaEvent,
|
||||
)
|
||||
|
||||
if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
|
||||
yield chunk.delta
|
||||
elif chunk.choices:
|
||||
if mode == Mode.FUNCTIONS:
|
||||
Mode.warn_mode_functions_deprecation()
|
||||
if json_chunk := chunk.choices[0].delta.function_call.arguments:
|
||||
yield json_chunk
|
||||
elif mode in {
|
||||
Mode.JSON,
|
||||
Mode.MD_JSON,
|
||||
Mode.JSON_SCHEMA,
|
||||
Mode.CEREBRAS_JSON,
|
||||
Mode.FIREWORKS_JSON,
|
||||
Mode.PERPLEXITY_JSON,
|
||||
Mode.WRITER_JSON,
|
||||
}:
|
||||
if json_chunk := chunk.choices[0].delta.content:
|
||||
yield json_chunk
|
||||
elif mode in {
|
||||
Mode.TOOLS,
|
||||
Mode.TOOLS_STRICT,
|
||||
Mode.FIREWORKS_TOOLS,
|
||||
Mode.WRITER_TOOLS,
|
||||
}:
|
||||
if json_chunk := chunk.choices[0].delta.tool_calls:
|
||||
if json_chunk[0].function.arguments is not None:
|
||||
yield json_chunk[0].function.arguments
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Mode {mode} is not supported for MultiTask streaming"
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def extract_json_async(
|
||||
completion: AsyncGenerator[Any, None], mode: Mode
|
||||
) -> AsyncGenerator[str, None]:
|
||||
json_started = False
|
||||
async for chunk in completion:
|
||||
try:
|
||||
if mode in {Mode.COHERE_TOOLS, Mode.COHERE_JSON_SCHEMA}:
|
||||
event_type = getattr(chunk, "event_type", None)
|
||||
if event_type == "text-generation":
|
||||
if text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif event_type == "tool-calls-chunk":
|
||||
delta = getattr(chunk, "tool_call_delta", None)
|
||||
args = getattr(delta, "parameters", None) or getattr(
|
||||
delta, "text", None
|
||||
)
|
||||
if args:
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (args.find("{"), args.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
elif text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif event_type == "tool-calls-generation":
|
||||
tool_calls = getattr(chunk, "tool_calls", None)
|
||||
if tool_calls:
|
||||
args = json.dumps(tool_calls[0].parameters)
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (args.find("{"), args.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
elif text := getattr(chunk, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (text.find("{"), text.find("["))
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
else:
|
||||
chunk_type = getattr(chunk, "type", None)
|
||||
if chunk_type == "content-delta":
|
||||
delta = getattr(chunk, "delta", None)
|
||||
message = getattr(delta, "message", None)
|
||||
content = getattr(message, "content", None)
|
||||
if text := getattr(content, "text", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (
|
||||
text.find("{"),
|
||||
text.find("["),
|
||||
)
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
text = text[json_start:]
|
||||
yield text
|
||||
elif chunk_type == "tool-call-delta":
|
||||
delta = getattr(chunk, "delta", None)
|
||||
message = getattr(delta, "message", None)
|
||||
tool_calls = getattr(message, "tool_calls", None)
|
||||
function = getattr(tool_calls, "function", None)
|
||||
if args := getattr(function, "arguments", None):
|
||||
if not json_started:
|
||||
json_start = min(
|
||||
(
|
||||
pos
|
||||
for pos in (
|
||||
args.find("{"),
|
||||
args.find("["),
|
||||
)
|
||||
if pos != -1
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
if json_start == -1:
|
||||
continue
|
||||
json_started = True
|
||||
args = args[json_start:]
|
||||
yield args
|
||||
if mode == Mode.ANTHROPIC_JSON:
|
||||
if json_chunk := chunk.delta.text:
|
||||
yield json_chunk
|
||||
if mode == Mode.ANTHROPIC_TOOLS:
|
||||
yield chunk.delta.partial_json
|
||||
if mode == Mode.VERTEXAI_JSON:
|
||||
yield chunk.candidates[0].content.parts[0].text
|
||||
if mode == Mode.VERTEXAI_TOOLS:
|
||||
yield json.dumps(
|
||||
chunk.candidates[0].content.parts[0].function_call.args
|
||||
)
|
||||
if mode == Mode.MISTRAL_STRUCTURED_OUTPUTS:
|
||||
yield chunk.data.choices[0].delta.content
|
||||
if mode == Mode.MISTRAL_TOOLS:
|
||||
if not chunk.data.choices[0].delta.tool_calls:
|
||||
continue
|
||||
yield chunk.data.choices[0].delta.tool_calls[0].function.arguments
|
||||
if mode == Mode.GENAI_STRUCTURED_OUTPUTS:
|
||||
yield chunk.text
|
||||
if mode in {Mode.GENAI_TOOLS}:
|
||||
yield json.dumps(
|
||||
chunk.candidates[0].content.parts[0].function_call.args
|
||||
)
|
||||
if mode in {
|
||||
Mode.RESPONSES_TOOLS,
|
||||
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
|
||||
}:
|
||||
from openai.types.responses import (
|
||||
ResponseFunctionCallArgumentsDeltaEvent,
|
||||
)
|
||||
|
||||
if isinstance(chunk, ResponseFunctionCallArgumentsDeltaEvent):
|
||||
yield chunk.delta
|
||||
elif chunk.choices:
|
||||
if mode == Mode.FUNCTIONS:
|
||||
Mode.warn_mode_functions_deprecation()
|
||||
if json_chunk := chunk.choices[0].delta.function_call.arguments:
|
||||
yield json_chunk
|
||||
elif mode in {
|
||||
Mode.JSON,
|
||||
Mode.MD_JSON,
|
||||
Mode.JSON_SCHEMA,
|
||||
Mode.CEREBRAS_JSON,
|
||||
Mode.FIREWORKS_JSON,
|
||||
Mode.PERPLEXITY_JSON,
|
||||
Mode.WRITER_JSON,
|
||||
}:
|
||||
if json_chunk := chunk.choices[0].delta.content:
|
||||
yield json_chunk
|
||||
elif mode in {
|
||||
Mode.TOOLS,
|
||||
Mode.TOOLS_STRICT,
|
||||
Mode.FIREWORKS_TOOLS,
|
||||
Mode.WRITER_TOOLS,
|
||||
}:
|
||||
if json_chunk := chunk.choices[0].delta.tool_calls:
|
||||
if json_chunk[0].function.arguments is not None:
|
||||
yield json_chunk[0].function.arguments
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Mode {mode} is not supported for MultiTask streaming"
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_object(s: str, stack: int) -> tuple[Optional[str], str]:
|
||||
start_index = s.find("{")
|
||||
for i, c in enumerate(s):
|
||||
if c == "{":
|
||||
stack += 1
|
||||
if c == "}":
|
||||
stack -= 1
|
||||
if stack == 0:
|
||||
return s[start_index : i + 1], s[i + 2 :]
|
||||
return None, s
|
||||
|
||||
|
||||
def IterableModel(
|
||||
subtask_class: type[BaseModel],
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> type[BaseModel]:
|
||||
# Import at runtime to avoid circular import
|
||||
from ..processing.function_calls import OpenAISchema
|
||||
|
||||
"""
|
||||
Dynamically create a IterableModel OpenAISchema that can be used to segment multiple
|
||||
tasks given a base class. This creates class that can be used to create a toolkit
|
||||
for a specific task, names and descriptions are automatically generated. However
|
||||
they can be overridden.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import IterableModel
|
||||
|
||||
class User(BaseModel):
|
||||
name: str = Field(description="The name of the person")
|
||||
age: int = Field(description="The age of the person")
|
||||
role: str = Field(description="The role of the person")
|
||||
|
||||
MultiUser = IterableModel(User)
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
```python
|
||||
class MultiUser(OpenAISchema, MultiTaskBase):
|
||||
tasks: List[User] = Field(
|
||||
default_factory=list,
|
||||
repr=False,
|
||||
description="Correctly segmented list of `User` tasks",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_streaming_response(cls, completion) -> Generator[User]:
|
||||
'''
|
||||
Parse the streaming response from OpenAI and yield a `User` object
|
||||
for each task in the response
|
||||
'''
|
||||
json_chunks = cls.extract_json(completion)
|
||||
yield from cls.tasks_from_chunks(json_chunks)
|
||||
```
|
||||
|
||||
Parameters:
|
||||
subtask_class (Type[OpenAISchema]): The base class to use for the MultiTask
|
||||
name (Optional[str]): The name of the MultiTask class, if None then the name
|
||||
of the subtask class is used as `Multi{subtask_class.__name__}`
|
||||
description (Optional[str]): The description of the MultiTask class, if None
|
||||
then the description is set to `Correct segmentation of `{subtask_class.__name__}` tasks`
|
||||
|
||||
Returns:
|
||||
schema (OpenAISchema): A new class that can be used to segment multiple tasks
|
||||
"""
|
||||
if name is not None:
|
||||
task_name = name
|
||||
else:
|
||||
# Handle `Union[A, B]` / `A | B` task types.
|
||||
# `types.UnionType` does not have `__name__`, so fall back to a stable name.
|
||||
task_name = getattr(subtask_class, "__name__", None)
|
||||
if task_name is None and get_origin(subtask_class) is Union:
|
||||
members = get_args(subtask_class)
|
||||
task_name = "Or".join(getattr(m, "__name__", str(m)) for m in members)
|
||||
if task_name is None:
|
||||
task_name = str(subtask_class)
|
||||
|
||||
name = f"Iterable{task_name}"
|
||||
|
||||
list_tasks = (
|
||||
list[subtask_class], # type: ignore
|
||||
Field(
|
||||
default_factory=list,
|
||||
repr=False,
|
||||
description=f"Correctly segmented list of `{task_name}` tasks",
|
||||
),
|
||||
)
|
||||
|
||||
base_models = cast(tuple[type[BaseModel], ...], (OpenAISchema, IterableBase))
|
||||
new_cls = create_model(
|
||||
name,
|
||||
tasks=list_tasks,
|
||||
__base__=base_models,
|
||||
)
|
||||
new_cls = cast(type[IterableBase], new_cls)
|
||||
|
||||
# set the class constructor BaseModel
|
||||
new_cls.task_type = subtask_class
|
||||
|
||||
new_cls.__doc__ = (
|
||||
f"Correct segmentation of `{task_name}` tasks"
|
||||
if description is None
|
||||
else description
|
||||
)
|
||||
assert issubclass(new_cls, OpenAISchema), (
|
||||
"The new class should be a subclass of OpenAISchema"
|
||||
)
|
||||
return new_cls
|
||||
138
참고/instructor-main/instructor/dsl/json_tracker.py
Normal file
138
참고/instructor-main/instructor/dsl/json_tracker.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
JSON Completeness Tracker for Partial Streaming.
|
||||
|
||||
Tracks which parts of accumulated JSON are "closed" (complete) vs "open" (incomplete).
|
||||
Uses jiter for parsing and a simple heuristic: if a value has a next sibling,
|
||||
it must be complete (because jiter had to finish parsing it to find the next one).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from jiter import from_json
|
||||
|
||||
|
||||
def is_json_complete(json_str: str) -> bool:
|
||||
"""
|
||||
Check if a JSON string represents a complete structure.
|
||||
|
||||
Uses jiter in strict mode - parsing fails if JSON is incomplete.
|
||||
"""
|
||||
if not json_str or not json_str.strip():
|
||||
return False
|
||||
try:
|
||||
from_json(json_str.encode()) # No partial_mode = strict parsing
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
class JsonCompleteness:
|
||||
"""
|
||||
Track completeness of JSON structures during streaming.
|
||||
|
||||
Uses a simple heuristic: if a value has a next sibling in the parsed
|
||||
structure, it must be complete. For the last sibling, we don't know
|
||||
until the parent completes - but that's fine because parent validation
|
||||
will cover it.
|
||||
|
||||
Example:
|
||||
tracker = JsonCompleteness()
|
||||
|
||||
# Incomplete - missing closing brace
|
||||
tracker.analyze('{"name": "Alice", "address": {"city": "NY')
|
||||
tracker.is_path_complete("") # False - root incomplete
|
||||
tracker.is_path_complete("name") # True - has next sibling "address"
|
||||
tracker.is_path_complete("address") # False - last sibling, unknown
|
||||
|
||||
# Complete
|
||||
tracker.analyze('{"name": "Alice"}')
|
||||
tracker.is_path_complete("") # True - root complete
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._complete_paths: set[str] = set()
|
||||
|
||||
def analyze(self, json_str: str) -> None:
|
||||
"""Analyze a JSON string and determine completeness of each path."""
|
||||
self._complete_paths = set()
|
||||
|
||||
if not json_str or not json_str.strip():
|
||||
return
|
||||
|
||||
# Try strict parsing first - if it succeeds, JSON is complete
|
||||
try:
|
||||
parsed = from_json(json_str.encode())
|
||||
self._mark_all(parsed, "")
|
||||
return
|
||||
except ValueError:
|
||||
pass # JSON is incomplete, continue with partial parsing
|
||||
|
||||
# Root incomplete - use sibling heuristic
|
||||
try:
|
||||
parsed = from_json(json_str.encode(), partial_mode="trailing-strings")
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
self._check_siblings(parsed, "")
|
||||
|
||||
def _mark_all(self, data: Any, path: str) -> None:
|
||||
"""Recursively mark path and all children as complete."""
|
||||
self._complete_paths.add(path)
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
child_path = f"{path}.{key}" if path else key
|
||||
self._mark_all(value, child_path)
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
self._mark_all(item, f"{path}[{i}]")
|
||||
|
||||
def _check_siblings(self, data: Any, path: str) -> None:
|
||||
"""
|
||||
Check completeness using sibling heuristic.
|
||||
|
||||
If a value has a next sibling, it's complete (jiter had to finish
|
||||
parsing it to find the next sibling). Last sibling is unknown.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
keys = list(data.keys())
|
||||
for i, key in enumerate(keys):
|
||||
child_path = f"{path}.{key}" if path else key
|
||||
if i < len(keys) - 1:
|
||||
# Has next sibling → complete
|
||||
self._mark_all(data[key], child_path)
|
||||
else:
|
||||
# Last sibling → recurse to check children
|
||||
self._check_siblings(data[key], child_path)
|
||||
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
child_path = f"{path}[{i}]"
|
||||
if i < len(data) - 1:
|
||||
# Has next sibling → complete
|
||||
self._mark_all(item, child_path)
|
||||
else:
|
||||
# Last sibling → recurse
|
||||
self._check_siblings(item, child_path)
|
||||
|
||||
def is_path_complete(self, path: str) -> bool:
|
||||
"""
|
||||
Check if the sub-structure at the given path is complete.
|
||||
|
||||
Args:
|
||||
path: Dot-separated path (e.g., "user.address.city", "items[0]")
|
||||
Use "" for root object.
|
||||
|
||||
Returns:
|
||||
True if the structure at path is complete (closed), False otherwise.
|
||||
"""
|
||||
return path in self._complete_paths
|
||||
|
||||
def get_complete_paths(self) -> set[str]:
|
||||
"""Return all paths that are complete."""
|
||||
return self._complete_paths.copy()
|
||||
|
||||
def is_root_complete(self) -> bool:
|
||||
"""Check if the root JSON structure is complete."""
|
||||
return "" in self._complete_paths
|
||||
74
참고/instructor-main/instructor/dsl/maybe.py
Normal file
74
참고/instructor-main/instructor/dsl/maybe.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from typing import Generic, Optional, TypeVar
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class MaybeBase(BaseModel, Generic[T]):
|
||||
"""
|
||||
Extract a result from a model, if any, otherwise set the error and message fields.
|
||||
"""
|
||||
|
||||
result: Optional[T]
|
||||
error: bool = Field(default=False)
|
||||
message: Optional[str]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.result is not None
|
||||
|
||||
|
||||
def Maybe(model: type[T]) -> type[MaybeBase[T]]:
|
||||
"""
|
||||
Create a Maybe model for a given Pydantic model. This allows you to return a model that includes fields for `result`, `error`, and `message` for sitatations where the data may not be present in the context.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import Maybe
|
||||
|
||||
class User(BaseModel):
|
||||
name: str = Field(description="The name of the person")
|
||||
age: int = Field(description="The age of the person")
|
||||
role: str = Field(description="The role of the person")
|
||||
|
||||
MaybeUser = Maybe(User)
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
```python
|
||||
class MaybeUser(BaseModel):
|
||||
result: Optional[User]
|
||||
error: bool = Field(default=False)
|
||||
message: Optional[str]
|
||||
|
||||
def __bool__(self):
|
||||
return self.result is not None
|
||||
```
|
||||
|
||||
Parameters:
|
||||
model (Type[BaseModel]): The Pydantic model to wrap with Maybe.
|
||||
|
||||
Returns:
|
||||
MaybeModel (Type[BaseModel]): A new Pydantic model that includes fields for `result`, `error`, and `message`.
|
||||
"""
|
||||
return create_model(
|
||||
f"Maybe{model.__name__}",
|
||||
__base__=MaybeBase,
|
||||
result=(
|
||||
Optional[model],
|
||||
Field(
|
||||
default=None,
|
||||
description="Correctly extracted result from the model, if any, otherwise None",
|
||||
),
|
||||
),
|
||||
error=(bool, Field(default=False)),
|
||||
message=(
|
||||
Optional[str],
|
||||
Field(
|
||||
default=None,
|
||||
description="Error message if no result was found, should be short and concise",
|
||||
),
|
||||
),
|
||||
)
|
||||
173
참고/instructor-main/instructor/dsl/parallel.py
Normal file
173
참고/instructor-main/instructor/dsl/parallel.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import sys
|
||||
import json
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
from collections.abc import Generator
|
||||
from pydantic import BaseModel
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ..mode import Mode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..processing.function_calls import OpenAISchema
|
||||
|
||||
T = TypeVar("T", bound=OpenAISchema)
|
||||
else:
|
||||
# At runtime, we'll bind to BaseModel instead to avoid circular import
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class ParallelBase:
|
||||
def __init__(self, *models: type[BaseModel]):
|
||||
# Note that for everything else we've created a class, but for parallel base it is an instance
|
||||
assert len(models) > 0, "At least one model is required"
|
||||
self.models = models
|
||||
self.registry = {
|
||||
model.__name__ if hasattr(model, "__name__") else str(model): model
|
||||
for model in models
|
||||
}
|
||||
|
||||
def from_response(
|
||||
self,
|
||||
response: Any,
|
||||
mode: Mode,
|
||||
validation_context: Optional[Any] = None,
|
||||
strict: Optional[bool] = None,
|
||||
) -> Generator[BaseModel, None, None]:
|
||||
#! We expect this from the OpenAISchema class, We should address
|
||||
#! this with a protocol or an abstract class... @jxnlco
|
||||
assert mode == Mode.PARALLEL_TOOLS, "Mode must be PARALLEL_TOOLS"
|
||||
for tool_call in response.choices[0].message.tool_calls:
|
||||
name = tool_call.function.name
|
||||
arguments = tool_call.function.arguments
|
||||
yield self.registry[name].model_validate_json(
|
||||
arguments, context=validation_context, strict=strict
|
||||
)
|
||||
|
||||
|
||||
class VertexAIParallelBase(ParallelBase):
|
||||
def from_response(
|
||||
self,
|
||||
response: Any,
|
||||
mode: Mode,
|
||||
validation_context: Optional[Any] = None,
|
||||
strict: Optional[bool] = None,
|
||||
) -> Generator[BaseModel, None, None]:
|
||||
assert mode == Mode.VERTEXAI_PARALLEL_TOOLS, (
|
||||
"Mode must be VERTEXAI_PARALLEL_TOOLS"
|
||||
)
|
||||
|
||||
if not response or not response.candidates:
|
||||
return
|
||||
|
||||
for candidate in response.candidates:
|
||||
if not candidate.content or not candidate.content.parts:
|
||||
continue
|
||||
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, "function_call") and part.function_call is not None:
|
||||
name = part.function_call.name
|
||||
arguments = part.function_call.args
|
||||
|
||||
if name in self.registry:
|
||||
# Convert dict to JSON string before validation
|
||||
json_str = json.dumps(arguments)
|
||||
yield self.registry[name].model_validate_json(
|
||||
json_str, context=validation_context, strict=strict
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from types import UnionType
|
||||
|
||||
def is_union_type(typehint: type[Iterable[T]]) -> bool:
|
||||
return get_origin(get_args(typehint)[0]) in (Union, UnionType)
|
||||
|
||||
else:
|
||||
|
||||
def is_union_type(typehint: type[Iterable[T]]) -> bool:
|
||||
return get_origin(get_args(typehint)[0]) is Union
|
||||
|
||||
|
||||
def get_types_array(typehint: type[Iterable[T]]) -> tuple[type[T], ...]:
|
||||
should_be_iterable = get_origin(typehint)
|
||||
|
||||
if should_be_iterable is not Iterable:
|
||||
raise TypeError(f"Model should be with Iterable instead of {typehint}")
|
||||
|
||||
if is_union_type(typehint):
|
||||
# works for Iterable[Union[int, str]], Iterable[int | str]
|
||||
the_types = get_args(get_args(typehint)[0])
|
||||
return the_types
|
||||
|
||||
# works for Iterable[int]
|
||||
return get_args(typehint)
|
||||
|
||||
|
||||
def handle_parallel_model(typehint: type[Iterable[T]]) -> list[dict[str, Any]]:
|
||||
# Import at runtime to avoid circular import
|
||||
from ..processing.function_calls import openai_schema
|
||||
|
||||
the_types = get_types_array(typehint)
|
||||
return [
|
||||
{"type": "function", "function": openai_schema(model).openai_schema}
|
||||
for model in the_types
|
||||
]
|
||||
|
||||
|
||||
def handle_anthropic_parallel_model(
|
||||
typehint: type[Iterable[T]],
|
||||
) -> list[dict[str, Any]]:
|
||||
# Import at runtime to avoid circular import
|
||||
from ..processing.function_calls import openai_schema
|
||||
|
||||
the_types = get_types_array(typehint)
|
||||
return [openai_schema(model).anthropic_schema for model in the_types]
|
||||
|
||||
|
||||
def ParallelModel(typehint: type[Iterable[T]]) -> ParallelBase:
|
||||
the_types = get_types_array(typehint)
|
||||
return ParallelBase(*[model for model in the_types])
|
||||
|
||||
|
||||
def VertexAIParallelModel(typehint: type[Iterable[T]]) -> VertexAIParallelBase:
|
||||
the_types = get_types_array(typehint)
|
||||
return VertexAIParallelBase(*[model for model in the_types])
|
||||
|
||||
|
||||
class AnthropicParallelBase(ParallelBase):
|
||||
def from_response(
|
||||
self,
|
||||
response: Any,
|
||||
mode: Mode,
|
||||
validation_context: Optional[Any] = None,
|
||||
strict: Optional[bool] = None,
|
||||
) -> Generator[BaseModel, None, None]:
|
||||
assert mode == Mode.ANTHROPIC_PARALLEL_TOOLS, (
|
||||
"Mode must be ANTHROPIC_PARALLEL_TOOLS"
|
||||
)
|
||||
|
||||
if not response or not hasattr(response, "content"):
|
||||
return
|
||||
|
||||
for content in response.content:
|
||||
if getattr(content, "type", None) == "tool_use":
|
||||
name = content.name
|
||||
arguments = content.input
|
||||
if name in self.registry:
|
||||
json_str = json.dumps(arguments)
|
||||
yield self.registry[name].model_validate_json(
|
||||
json_str, context=validation_context, strict=strict
|
||||
)
|
||||
|
||||
|
||||
def AnthropicParallelModel(typehint: type[Iterable[T]]) -> AnthropicParallelBase:
|
||||
the_types = get_types_array(typehint)
|
||||
return AnthropicParallelBase(*[model for model in the_types])
|
||||
1125
참고/instructor-main/instructor/dsl/partial.py
Normal file
1125
참고/instructor-main/instructor/dsl/partial.py
Normal file
File diff suppressed because it is too large
Load Diff
43
참고/instructor-main/instructor/dsl/response_list.py
Normal file
43
참고/instructor-main/instructor/dsl/response_list.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""List-like response wrapper.
|
||||
|
||||
When a response model returns a list (for example `list[User]`), we still want to
|
||||
attach the provider's raw response so `create_with_completion()` can return it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ListResponse(list[T], Generic[T]):
|
||||
"""A list that preserves the underlying provider response.
|
||||
|
||||
This is used when a call returns a list of objects (e.g. `list[User]`), so
|
||||
`create_with_completion()` can still return `(result, raw_response)` without
|
||||
crashing on a plain `list`.
|
||||
"""
|
||||
|
||||
_raw_response: Any | None
|
||||
|
||||
def __init__(self, iterable=(), _raw_response: Any | None = None): # type: ignore[no-untyped-def]
|
||||
super().__init__(iterable)
|
||||
self._raw_response = _raw_response
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, items: list[T], *, raw_response: Any | None) -> ListResponse[T]:
|
||||
return cls(items, _raw_response=raw_response)
|
||||
|
||||
def get_raw_response(self) -> Any | None:
|
||||
return self._raw_response
|
||||
|
||||
def __getitem__(self, key): # type: ignore[no-untyped-def]
|
||||
value = super().__getitem__(key)
|
||||
if isinstance(key, slice):
|
||||
return type(self)(value, _raw_response=self._raw_response)
|
||||
return value
|
||||
|
||||
|
||||
# Backwards-friendly alias
|
||||
ResponseList = ListResponse
|
||||
176
참고/instructor-main/instructor/dsl/simple_type.py
Normal file
176
참고/instructor-main/instructor/dsl/simple_type.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
from inspect import isclass
|
||||
import typing
|
||||
from pydantic import BaseModel, create_model
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from instructor.dsl.partial import Partial
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
class AdapterBase(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ModelAdapter(typing.Generic[T]):
|
||||
"""
|
||||
Accepts a response model and returns a BaseModel with the response model as the content.
|
||||
"""
|
||||
|
||||
def __class_getitem__(cls, response_model: type[BaseModel]) -> type[BaseModel]:
|
||||
# Import at runtime to avoid circular import
|
||||
from ..processing.function_calls import OpenAISchema
|
||||
|
||||
assert is_simple_type(response_model), "Only simple types are supported"
|
||||
return create_model(
|
||||
"Response",
|
||||
content=(response_model, ...),
|
||||
__doc__="Correctly Formatted and Extracted Response.",
|
||||
__base__=(AdapterBase, OpenAISchema),
|
||||
)
|
||||
|
||||
|
||||
def validateIsSubClass(response_model: type):
|
||||
"""
|
||||
Temporary guard against issues with generics in Python 3.9
|
||||
"""
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
if len(typing.get_args(response_model)) == 0:
|
||||
return False
|
||||
return issubclass(typing.get_args(response_model)[0], BaseModel)
|
||||
try:
|
||||
# Add a guard here to prevent issues with GenericAlias
|
||||
import types
|
||||
|
||||
if isinstance(response_model, types.GenericAlias):
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return issubclass(response_model, BaseModel)
|
||||
|
||||
|
||||
def is_simple_type(
|
||||
response_model: type[BaseModel] | str | int | float | bool | typing.Any,
|
||||
) -> bool:
|
||||
# ! we're getting mixes between classes and instances due to how we handle some
|
||||
# ! response model types, we should fix this in later PRs
|
||||
|
||||
# Special case for Python 3.9: Directly handle list[Union[int, str]] pattern
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
# Check if it's a list type with Union arguments using string representation
|
||||
if str(response_model).startswith("list[typing.Union[") or "list[Union[" in str(
|
||||
response_model
|
||||
):
|
||||
return True
|
||||
|
||||
try:
|
||||
if isclass(response_model) and validateIsSubClass(response_model):
|
||||
return False
|
||||
except TypeError:
|
||||
# ! In versions < 3.11, typing.Iterable is not a class, so we can't use isclass
|
||||
# ! for now if `response_model` is an Iterable isclass and issubclass will raise
|
||||
# ! TypeError, so we need to check if `response_model` is an Iterable
|
||||
# ! This is a workaround for now, we should fix this in later PRs
|
||||
return False
|
||||
|
||||
# Get the origin of the response model
|
||||
origin = typing.get_origin(response_model)
|
||||
|
||||
# Handle special case for list[int | str], list[Union[int, str]] or similar type patterns
|
||||
# Identify a list type by checking for various origins it might have
|
||||
if origin in {typing.Iterable, Partial, list}:
|
||||
# For list types, check the contents before deciding
|
||||
if origin is list:
|
||||
# Extract the inner types from the list
|
||||
args = typing.get_args(response_model)
|
||||
if args and len(args) == 1:
|
||||
inner_arg = args[0]
|
||||
# Special handling for Union types
|
||||
inner_origin = typing.get_origin(inner_arg)
|
||||
|
||||
# Explicit check for Union types - try different patterns across Python versions
|
||||
if (
|
||||
inner_origin is typing.Union
|
||||
or inner_origin == typing.Union
|
||||
or str(inner_origin) == "typing.Union"
|
||||
or str(type(inner_arg)) == "<class 'typing._UnionGenericAlias'>"
|
||||
):
|
||||
return True
|
||||
|
||||
# Check for Python 3.10+ pipe syntax
|
||||
if hasattr(inner_arg, "__or__"):
|
||||
return True
|
||||
|
||||
# For simple list with basic types, also return True
|
||||
if inner_arg in {str, int, float, bool}:
|
||||
return True
|
||||
|
||||
# Check if inner type is a BaseModel - if so, not a simple type
|
||||
try:
|
||||
if isclass(inner_arg) and issubclass(inner_arg, BaseModel):
|
||||
return False
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# If no args or unknown pattern, treat as simple list
|
||||
return len(args) == 0
|
||||
|
||||
# Extract the inner types from the list for other iterable types
|
||||
args = typing.get_args(response_model)
|
||||
if args and len(args) == 1:
|
||||
inner_arg = args[0]
|
||||
# Special handling for Union types
|
||||
inner_origin = typing.get_origin(inner_arg)
|
||||
|
||||
# Explicit check for Union types - try different patterns across Python versions
|
||||
if (
|
||||
inner_origin is typing.Union
|
||||
or inner_origin == typing.Union
|
||||
or str(inner_origin) == "typing.Union"
|
||||
or str(type(inner_arg)) == "<class 'typing._UnionGenericAlias'>"
|
||||
):
|
||||
return True
|
||||
|
||||
# Check for Python 3.10+ pipe syntax
|
||||
if hasattr(inner_arg, "__or__"):
|
||||
return True
|
||||
|
||||
# For simple list with basic types, also return True
|
||||
if inner_arg in {str, int, float, bool}:
|
||||
return True
|
||||
|
||||
# For other iterable patterns, return False (e.g., streaming types)
|
||||
return False
|
||||
|
||||
if response_model in {
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
}:
|
||||
return True
|
||||
|
||||
# If the response_model is a simple type like annotated
|
||||
if origin in {
|
||||
typing.Annotated,
|
||||
typing.Literal,
|
||||
typing.Union,
|
||||
list, # origin of List[T] is list
|
||||
}:
|
||||
return True
|
||||
|
||||
if isclass(response_model) and issubclass(response_model, Enum):
|
||||
return True
|
||||
|
||||
return False
|
||||
20
참고/instructor-main/instructor/dsl/validators.py
Normal file
20
참고/instructor-main/instructor/dsl/validators.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Backwards compatibility module for instructor.dsl.validators.
|
||||
|
||||
This module provides lazy imports to avoid circular import issues.
|
||||
"""
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import to avoid circular dependencies."""
|
||||
from ..processing import validators as processing_validators
|
||||
from .. import validation
|
||||
|
||||
# Try processing.validators first
|
||||
if hasattr(processing_validators, name):
|
||||
return getattr(processing_validators, name)
|
||||
|
||||
# Then try validation module
|
||||
if hasattr(validation, name):
|
||||
return getattr(validation, name)
|
||||
|
||||
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||
Reference in New Issue
Block a user