참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View File

@@ -0,0 +1 @@
"""Provider implementation."""

View File

@@ -0,0 +1,377 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING, cast, overload
import json
from instructor.dsl.iterable import IterableBase
from instructor.dsl.partial import PartialBase
from instructor.dsl.simple_type import AdapterBase
from instructor.utils.core import prepare_response_model
from pydantic import BaseModel
import instructor
from .utils import _convert_messages
def _raise_xai_sdk_missing() -> None:
from ...core.exceptions import ConfigurationError
raise ConfigurationError(
"The xAI provider needs the optional dependency `xai-sdk`. "
'Install it with `uv pip install "instructor[xai]"` (or `pip install "instructor[xai]"`). '
"Note: xai-sdk requires Python 3.10+."
) from None
def _get_model_schema(response_model: Any) -> dict[str, Any]:
"""
Safely get JSON schema from a response model.
Handles both regular models and wrapped types by checking for the
model_json_schema method with hasattr.
Args:
response_model: The response model (may be regular or wrapped)
Returns:
The JSON schema dictionary
"""
if hasattr(response_model, "model_json_schema") and callable(
response_model.model_json_schema
):
schema_method = response_model.model_json_schema
return schema_method()
return {}
def _get_model_name(response_model: Any) -> str:
"""
Safely get the name of a response model.
Args:
response_model: The response model
Returns:
The model name or 'Model' as fallback
"""
return getattr(response_model, "__name__", "Model")
def _finalize_parsed_response(parsed: Any, raw_response: Any) -> Any:
if isinstance(parsed, BaseModel):
parsed._raw_response = raw_response
if isinstance(parsed, IterableBase):
return [task for task in parsed.tasks]
if isinstance(parsed, AdapterBase):
return parsed.content
return parsed
if TYPE_CHECKING:
from xai_sdk.sync.client import Client as SyncClient
from xai_sdk.aio.client import Client as AsyncClient
from xai_sdk import chat as xchat
else:
try:
from xai_sdk.sync.client import Client as SyncClient
from xai_sdk.aio.client import Client as AsyncClient
from xai_sdk import chat as xchat
except ImportError:
SyncClient = None
AsyncClient = None
xchat = None
@overload
def from_xai(
client: SyncClient,
mode: instructor.Mode = instructor.Mode.XAI_JSON,
**kwargs: Any,
) -> instructor.Instructor: ...
@overload
def from_xai(
client: AsyncClient,
mode: instructor.Mode = instructor.Mode.XAI_JSON,
**kwargs: Any,
) -> instructor.AsyncInstructor: ...
def from_xai(
client: SyncClient | AsyncClient,
mode: instructor.Mode = instructor.Mode.XAI_JSON,
**kwargs: Any,
) -> instructor.Instructor | instructor.AsyncInstructor:
if SyncClient is None or AsyncClient is None or xchat is None:
_raise_xai_sdk_missing()
valid_modes = {instructor.Mode.XAI_JSON, instructor.Mode.XAI_TOOLS}
if mode not in valid_modes:
from ...core.exceptions import ModeError
raise ModeError(
mode=str(mode), provider="xAI", valid_modes=[str(m) for m in valid_modes]
)
if not isinstance(client, (SyncClient, AsyncClient)):
from ...core.exceptions import ClientError
raise ClientError(
"Client must be an instance of xai_sdk.sync.client.Client or xai_sdk.aio.client.Client. "
f"Got: {type(client).__name__}"
)
async def acreate(
response_model: type[BaseModel] | None,
messages: list[dict[str, Any]],
strict: bool = True,
**call_kwargs: Any,
):
x_messages = _convert_messages(messages)
model = call_kwargs.pop("model")
# Remove instructor-specific kwargs that xAI doesn't support
call_kwargs.pop("max_retries", None)
call_kwargs.pop("validation_context", None)
call_kwargs.pop("context", None)
call_kwargs.pop("hooks", None)
is_stream = call_kwargs.pop("stream", False)
chat = client.chat.create(model=model, messages=x_messages, **call_kwargs)
if response_model is None:
resp = await chat.sample() # type: ignore[misc]
return resp
assert response_model is not None
prepared_model = response_model
if mode == instructor.Mode.XAI_TOOLS or is_stream:
prepared_model = prepare_response_model(response_model)
assert prepared_model is not None
if mode == instructor.Mode.XAI_JSON:
if is_stream:
# code from xai_sdk.chat.parse
chat.proto.response_format.CopyFrom(
xchat.chat_pb2.ResponseFormat(
format_type=xchat.chat_pb2.FormatType.FORMAT_TYPE_JSON_SCHEMA,
schema=json.dumps(_get_model_schema(prepared_model)),
)
)
json_chunks = (chunk.content async for _, chunk in chat.stream()) # type: ignore[misc]
# response_model is guaranteed to be a type[BaseModel] at this point due to earlier assertion
rm = cast(type[BaseModel], prepared_model)
if issubclass(rm, IterableBase):
return rm.tasks_from_chunks_async(json_chunks) # type: ignore
elif issubclass(rm, PartialBase):
return rm.model_from_chunks_async(json_chunks) # type: ignore
else:
raise ValueError(
f"Unsupported response model type for streaming: {_get_model_name(response_model)}"
)
else:
raw, parsed = await chat.parse(response_model) # type: ignore[misc]
parsed._raw_response = raw
return parsed
else:
tool_obj = xchat.tool(
name=_get_model_name(prepared_model),
description=prepared_model.__doc__ or "",
parameters=_get_model_schema(prepared_model),
)
chat.proto.tools.append(tool_obj) # type: ignore[arg-type]
tool_name = tool_obj.function.name # type: ignore[attr-defined]
chat.proto.tool_choice.CopyFrom(xchat.required_tool(tool_name))
if is_stream:
stream_iter = chat.stream() # type: ignore[misc]
args = (
resp.tool_calls[0].function.arguments # type: ignore[index,attr-defined]
async for resp, _ in stream_iter # type: ignore[assignment]
if resp.tool_calls and resp.finish_reason == "REASON_INVALID" # type: ignore[attr-defined]
)
rm = cast(type[BaseModel], prepared_model)
if issubclass(rm, IterableBase):
return rm.tasks_from_chunks_async(args) # type: ignore
elif issubclass(rm, PartialBase):
return rm.model_from_chunks_async(args) # type: ignore
else:
raise ValueError(
f"Unsupported response model type for streaming: {_get_model_name(response_model)}"
)
else:
resp = await chat.sample() # type: ignore[misc]
if not resp.tool_calls: # type: ignore[attr-defined]
# If no tool calls, try to extract from text content
from ...processing.function_calls import _validate_model_from_json
from ...utils import extract_json_from_codeblock
# Try to extract JSON from text content
text_content: str = ""
if hasattr(resp, "text") and resp.text: # type: ignore[attr-defined]
text_content = str(resp.text) # type: ignore[attr-defined]
elif hasattr(resp, "content") and resp.content: # type: ignore[attr-defined]
content = resp.content # type: ignore[attr-defined]
if isinstance(content, str):
text_content = content
elif isinstance(content, list) and content:
text_content = str(content[0])
if text_content:
json_str = extract_json_from_codeblock(text_content)
model_for_validation = cast(type[Any], prepared_model)
parsed = _validate_model_from_json(
model_for_validation, json_str, None, strict
)
return _finalize_parsed_response(parsed, resp)
raise ValueError(
f"No tool calls returned from xAI and no text content available. "
f"Response: {resp}"
)
args = resp.tool_calls[0].function.arguments # type: ignore[index,attr-defined]
from ...processing.function_calls import _validate_model_from_json
model_for_validation = cast(type[Any], prepared_model)
parsed = _validate_model_from_json(
model_for_validation, args, None, strict
)
return _finalize_parsed_response(parsed, resp)
def create(
response_model: type[BaseModel] | None,
messages: list[dict[str, Any]],
strict: bool = True,
**call_kwargs: Any,
):
x_messages = _convert_messages(messages)
model = call_kwargs.pop("model")
# Remove instructor-specific kwargs that xAI doesn't support
call_kwargs.pop("max_retries", None)
call_kwargs.pop("validation_context", None)
call_kwargs.pop("context", None)
call_kwargs.pop("hooks", None)
# Check if streaming is requested
is_stream = call_kwargs.pop("stream", False)
chat = client.chat.create(model=model, messages=x_messages, **call_kwargs)
if response_model is None:
resp = chat.sample() # type: ignore[misc]
return resp
assert response_model is not None
prepared_model = response_model
if mode == instructor.Mode.XAI_TOOLS or is_stream:
prepared_model = prepare_response_model(response_model)
assert prepared_model is not None
if mode == instructor.Mode.XAI_JSON:
if is_stream:
# code from xai_sdk.chat.parse
chat.proto.response_format.CopyFrom(
xchat.chat_pb2.ResponseFormat(
format_type=xchat.chat_pb2.FormatType.FORMAT_TYPE_JSON_SCHEMA,
schema=json.dumps(_get_model_schema(prepared_model)),
)
)
json_chunks = (chunk.content for _, chunk in chat.stream()) # type: ignore[misc]
rm = cast(type[BaseModel], prepared_model)
if issubclass(rm, IterableBase):
return rm.tasks_from_chunks(json_chunks)
elif issubclass(rm, PartialBase):
return rm.model_from_chunks(json_chunks)
else:
raise ValueError(
f"Unsupported response model type for streaming: {_get_model_name(response_model)}"
)
else:
raw, parsed = chat.parse(response_model) # type: ignore[misc]
parsed._raw_response = raw
return parsed
else:
tool_obj = xchat.tool(
name=_get_model_name(prepared_model),
description=prepared_model.__doc__ or "",
parameters=_get_model_schema(prepared_model),
)
chat.proto.tools.append(tool_obj) # type: ignore[arg-type]
tool_name = tool_obj.function.name # type: ignore[attr-defined]
chat.proto.tool_choice.CopyFrom(xchat.required_tool(tool_name))
if is_stream:
stream_iter = chat.stream() # type: ignore[misc]
for resp, _ in stream_iter: # type: ignore[assignment]
# For xAI, tool_calls are returned at the end of the response.
# Effectively, it is not a streaming response.
# See: https://docs.x.ai/docs/guides/function-calling
if resp.tool_calls: # type: ignore[attr-defined]
args = resp.tool_calls[0].function.arguments # type: ignore[index,attr-defined]
rm = cast(type[BaseModel], prepared_model)
if issubclass(rm, IterableBase):
return rm.tasks_from_chunks(args)
elif issubclass(rm, PartialBase):
return rm.model_from_chunks(args)
else:
raise ValueError(
f"Unsupported response model type for streaming: {_get_model_name(response_model)}"
)
else:
resp = chat.sample() # type: ignore[misc]
if not resp.tool_calls: # type: ignore[attr-defined]
# If no tool calls, try to extract from text content
from ...processing.function_calls import _validate_model_from_json
from ...utils import extract_json_from_codeblock
# Try to extract JSON from text content
text_content: str = ""
if hasattr(resp, "text") and resp.text: # type: ignore[attr-defined]
text_content = str(resp.text) # type: ignore[attr-defined]
elif hasattr(resp, "content") and resp.content: # type: ignore[attr-defined]
content = resp.content # type: ignore[attr-defined]
if isinstance(content, str):
text_content = content
elif isinstance(content, list) and content:
text_content = str(content[0])
if text_content:
json_str = extract_json_from_codeblock(text_content)
model_for_validation = cast(type[Any], prepared_model)
parsed = _validate_model_from_json(
model_for_validation, json_str, None, strict
)
return _finalize_parsed_response(parsed, resp)
raise ValueError(
f"No tool calls returned from xAI and no text content available. "
f"Response: {resp}"
)
args = resp.tool_calls[0].function.arguments # type: ignore[index,attr-defined]
from ...processing.function_calls import _validate_model_from_json
model_for_validation = cast(type[Any], prepared_model)
parsed = _validate_model_from_json(
model_for_validation, args, None, strict
)
return _finalize_parsed_response(parsed, resp)
if isinstance(client, AsyncClient):
return instructor.AsyncInstructor(
client=client,
create=acreate,
provider=instructor.Provider.XAI,
mode=mode,
**kwargs,
)
else:
return instructor.Instructor(
client=client,
create=create,
provider=instructor.Provider.XAI,
mode=mode,
**kwargs,
)

View File

@@ -0,0 +1,185 @@
"""xAI-specific utilities.
This module contains utilities specific to the xAI provider,
including reask functions, response handlers, and message formatting.
"""
from __future__ import annotations
from typing import Any, TYPE_CHECKING
from ...mode import Mode
if TYPE_CHECKING:
from xai_sdk import chat as xchat
else:
try:
from xai_sdk import chat as xchat
except ImportError:
xchat = None
def _convert_messages(messages: list[dict[str, Any]]):
"""Convert OpenAI-style messages to xAI format."""
if xchat is None:
from ...core.exceptions import ConfigurationError
raise ConfigurationError(
"The xAI provider needs the optional dependency `xai-sdk`. "
'Install it with `uv pip install "instructor[xai]"` (or `pip install "instructor[xai]"`). '
"Note: xai-sdk requires Python 3.10+."
) from None
converted = []
for m in messages:
role = m["role"]
content = m.get("content", "")
if isinstance(content, str):
c = xchat.text(content)
else:
raise ValueError("Only string content supported for xAI provider")
if role == "user":
converted.append(xchat.user(c))
elif role == "assistant":
converted.append(xchat.assistant(c))
elif role == "system":
converted.append(xchat.system(c))
elif role == "tool":
converted.append(xchat.tool_result(content))
else:
raise ValueError(f"Unsupported role: {role}")
return converted
def reask_xai_json(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
):
"""
Handle reask for xAI JSON mode when validation fails.
Kwargs modifications:
- Modifies: "messages" (appends user message requesting correction)
"""
kwargs = kwargs.copy()
reask_msg = {
"role": "user",
"content": f"Validation Errors found:\n{exception}\nRecall the function correctly, fix the errors found in the following attempt:\n{response}",
}
kwargs["messages"].append(reask_msg)
return kwargs
def reask_xai_tools(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
):
"""
Handle reask for xAI tools mode when validation fails.
Kwargs modifications:
- Modifies: "messages" (appends assistant and user messages for tool correction)
"""
kwargs = kwargs.copy()
# Add assistant response to conversation history
assistant_msg = {
"role": "assistant",
"content": str(response),
}
kwargs["messages"].append(assistant_msg)
# Add user correction request
reask_msg = {
"role": "user",
"content": f"Validation Error found:\n{exception}\nRecall the function correctly, fix the errors",
}
kwargs["messages"].append(reask_msg)
return kwargs
def handle_xai_json(
response_model: type[Any] | None, new_kwargs: dict[str, Any]
) -> tuple[type[Any] | None, dict[str, Any]]:
"""
Handle xAI JSON mode.
When response_model is None:
- Converts messages from OpenAI format to xAI format
- No schema is added to the request
When response_model is provided:
- Converts messages from OpenAI format to xAI format
- Sets up the model for JSON parsing mode
Kwargs modifications:
- Modifies: "messages" (converts from OpenAI to xAI format)
- Removes: instructor-specific kwargs (max_retries, validation_context, context, hooks)
"""
# Convert messages to xAI format
messages = new_kwargs.get("messages", [])
new_kwargs["x_messages"] = _convert_messages(messages)
# Remove instructor-specific kwargs that xAI doesn't support
new_kwargs.pop("max_retries", None)
new_kwargs.pop("validation_context", None)
new_kwargs.pop("context", None)
new_kwargs.pop("hooks", None)
return response_model, new_kwargs
def handle_xai_tools(
response_model: type[Any] | None, new_kwargs: dict[str, Any]
) -> tuple[type[Any] | None, dict[str, Any]]:
"""
Handle xAI tools mode.
When response_model is None:
- Converts messages from OpenAI format to xAI format
- No tools are configured
When response_model is provided:
- Converts messages from OpenAI format to xAI format
- Sets up tool schema from the response model
- Configures tool choice for automatic tool selection
Kwargs modifications:
- Modifies: "messages" (converts from OpenAI to xAI format)
- Adds: "tool" (xAI tool schema) - only when response_model provided
- Removes: instructor-specific kwargs (max_retries, validation_context, context, hooks)
"""
# Convert messages to xAI format
messages = new_kwargs.get("messages", [])
new_kwargs["x_messages"] = _convert_messages(messages)
# Remove instructor-specific kwargs that xAI doesn't support
new_kwargs.pop("max_retries", None)
new_kwargs.pop("validation_context", None)
new_kwargs.pop("context", None)
new_kwargs.pop("hooks", None)
if response_model is not None and xchat is not None:
# Set up tool schema for structured output
new_kwargs["tool"] = xchat.tool(
name=response_model.__name__,
description=response_model.__doc__ or "",
parameters=response_model.model_json_schema(),
)
return response_model, new_kwargs
# Handler registry for xAI
XAI_HANDLERS = {
Mode.XAI_JSON: {
"reask": reask_xai_json,
"response": handle_xai_json,
},
Mode.XAI_TOOLS: {
"reask": reask_xai_tools,
"response": handle_xai_tools,
},
}