참고소스 수정본
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Provider implementation."""
|
||||
118
참고/instructor-main/instructor/providers/anthropic/client.py
Normal file
118
참고/instructor-main/instructor/providers/anthropic/client.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import anthropic
|
||||
import instructor
|
||||
|
||||
from typing import overload, Any
|
||||
|
||||
|
||||
@overload
|
||||
def from_anthropic(
|
||||
client: (
|
||||
anthropic.Anthropic | anthropic.AnthropicBedrock | anthropic.AnthropicVertex
|
||||
),
|
||||
mode: instructor.Mode = instructor.Mode.ANTHROPIC_TOOLS,
|
||||
beta: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> instructor.Instructor: ...
|
||||
|
||||
|
||||
@overload
|
||||
def from_anthropic(
|
||||
client: (
|
||||
anthropic.AsyncAnthropic
|
||||
| anthropic.AsyncAnthropicBedrock
|
||||
| anthropic.AsyncAnthropicVertex
|
||||
),
|
||||
mode: instructor.Mode = instructor.Mode.ANTHROPIC_TOOLS,
|
||||
beta: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> instructor.AsyncInstructor: ...
|
||||
|
||||
|
||||
def from_anthropic(
|
||||
client: (
|
||||
anthropic.Anthropic
|
||||
| anthropic.AsyncAnthropic
|
||||
| anthropic.AnthropicBedrock
|
||||
| anthropic.AsyncAnthropicBedrock
|
||||
| anthropic.AsyncAnthropicVertex
|
||||
| anthropic.AnthropicVertex
|
||||
),
|
||||
mode: instructor.Mode = instructor.Mode.ANTHROPIC_TOOLS,
|
||||
beta: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> instructor.Instructor | instructor.AsyncInstructor:
|
||||
"""Create an Instructor instance from an Anthropic client.
|
||||
|
||||
Args:
|
||||
client: An instance of Anthropic client (sync or async)
|
||||
mode: The mode to use for the client (ANTHROPIC_JSON or ANTHROPIC_TOOLS)
|
||||
beta: Whether to use beta API features (uses client.beta.messages.create)
|
||||
**kwargs: Additional keyword arguments to pass to the Instructor constructor
|
||||
|
||||
Returns:
|
||||
An Instructor instance (sync or async depending on the client type)
|
||||
|
||||
Raises:
|
||||
ModeError: If mode is not one of the valid Anthropic modes
|
||||
ClientError: If client is not a valid Anthropic client instance
|
||||
"""
|
||||
valid_modes = {
|
||||
instructor.Mode.ANTHROPIC_JSON,
|
||||
instructor.Mode.ANTHROPIC_TOOLS,
|
||||
instructor.Mode.ANTHROPIC_REASONING_TOOLS,
|
||||
instructor.Mode.ANTHROPIC_PARALLEL_TOOLS,
|
||||
}
|
||||
|
||||
if mode not in valid_modes:
|
||||
from ...core.exceptions import ModeError
|
||||
|
||||
raise ModeError(
|
||||
mode=str(mode),
|
||||
provider="Anthropic",
|
||||
valid_modes=[str(m) for m in valid_modes],
|
||||
)
|
||||
|
||||
valid_client_types = (
|
||||
anthropic.Anthropic,
|
||||
anthropic.AsyncAnthropic,
|
||||
anthropic.AnthropicBedrock,
|
||||
anthropic.AnthropicVertex,
|
||||
anthropic.AsyncAnthropicBedrock,
|
||||
anthropic.AsyncAnthropicVertex,
|
||||
)
|
||||
|
||||
if not isinstance(client, valid_client_types):
|
||||
from ...core.exceptions import ClientError
|
||||
|
||||
raise ClientError(
|
||||
f"Client must be an instance of one of: {', '.join(t.__name__ for t in valid_client_types)}. "
|
||||
f"Got: {type(client).__name__}"
|
||||
)
|
||||
|
||||
if beta:
|
||||
create = client.beta.messages.create
|
||||
else:
|
||||
create = client.messages.create
|
||||
|
||||
if isinstance(
|
||||
client,
|
||||
(anthropic.Anthropic, anthropic.AnthropicBedrock, anthropic.AnthropicVertex),
|
||||
):
|
||||
return instructor.Instructor(
|
||||
client=client,
|
||||
create=instructor.patch(create=create, mode=mode),
|
||||
provider=instructor.Provider.ANTHROPIC,
|
||||
mode=mode,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
else:
|
||||
return instructor.AsyncInstructor(
|
||||
client=client,
|
||||
create=instructor.patch(create=create, mode=mode),
|
||||
provider=instructor.Provider.ANTHROPIC,
|
||||
mode=mode,
|
||||
**kwargs,
|
||||
)
|
||||
484
참고/instructor-main/instructor/providers/anthropic/utils.py
Normal file
484
참고/instructor-main/instructor/providers/anthropic/utils.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""Anthropic-specific utilities.
|
||||
|
||||
This module contains utilities specific to the Anthropic provider,
|
||||
including reask functions, response handlers, and message formatting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Any, TypedDict, Union
|
||||
|
||||
|
||||
from ...mode import Mode
|
||||
from ...processing.schema import generate_anthropic_schema
|
||||
|
||||
|
||||
class SystemMessage(TypedDict, total=False):
|
||||
type: str
|
||||
text: str
|
||||
cache_control: dict[str, str]
|
||||
|
||||
|
||||
def combine_system_messages(
|
||||
existing_system: Union[str, list[SystemMessage], None], # noqa: UP007
|
||||
new_system: Union[str, list[SystemMessage]], # noqa: UP007
|
||||
) -> Union[str, list[SystemMessage]]: # noqa: UP007
|
||||
"""
|
||||
Combine existing and new system messages.
|
||||
|
||||
This optimized version uses a more direct approach with fewer branches.
|
||||
|
||||
Args:
|
||||
existing_system: Existing system message(s) or None
|
||||
new_system: New system message(s) to add
|
||||
|
||||
Returns:
|
||||
Combined system message(s)
|
||||
"""
|
||||
# Fast path for None existing_system (avoid unnecessary operations)
|
||||
if existing_system is None:
|
||||
return new_system
|
||||
|
||||
# Validate input types
|
||||
if not isinstance(existing_system, (str, list)) or not isinstance(
|
||||
new_system, (str, list)
|
||||
):
|
||||
raise ValueError(
|
||||
f"System messages must be strings or lists, got {type(existing_system)} and {type(new_system)}"
|
||||
)
|
||||
|
||||
# Use direct type comparison instead of isinstance for better performance
|
||||
if isinstance(existing_system, str) and isinstance(new_system, str):
|
||||
# Both are strings, join with newlines
|
||||
# Avoid creating intermediate strings by joining only once
|
||||
return f"{existing_system}\n\n{new_system}"
|
||||
elif isinstance(existing_system, list) and isinstance(new_system, list):
|
||||
# Both are lists, use list extension in place to avoid creating intermediate lists
|
||||
# First create a new list to avoid modifying the original
|
||||
result = list(existing_system)
|
||||
result.extend(new_system)
|
||||
return result
|
||||
elif isinstance(existing_system, str) and isinstance(new_system, list):
|
||||
# existing is string, new is list
|
||||
# Create a pre-sized list to avoid resizing
|
||||
result = [SystemMessage(type="text", text=existing_system)]
|
||||
result.extend(new_system)
|
||||
return result
|
||||
elif isinstance(existing_system, list) and isinstance(new_system, str):
|
||||
# existing is list, new is string
|
||||
# Create message once and add to existing
|
||||
new_message = SystemMessage(type="text", text=new_system)
|
||||
result = list(existing_system)
|
||||
result.append(new_message)
|
||||
return result
|
||||
|
||||
# This should never happen due to validation above
|
||||
return existing_system
|
||||
|
||||
|
||||
def extract_system_messages(messages: list[dict[str, Any]]) -> list[SystemMessage]:
|
||||
"""
|
||||
Extract system messages from a list of messages.
|
||||
|
||||
This optimized version pre-allocates the result list and
|
||||
reduces function call overhead.
|
||||
|
||||
Args:
|
||||
messages: List of messages to extract system messages from
|
||||
|
||||
Returns:
|
||||
List of system messages
|
||||
"""
|
||||
# Fast path for empty messages
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
# First count system messages to pre-allocate result list
|
||||
system_count = sum(1 for m in messages if m.get("role") == "system")
|
||||
|
||||
# If no system messages, return empty list
|
||||
if system_count == 0:
|
||||
return []
|
||||
|
||||
# Helper function to convert a message content to SystemMessage
|
||||
def convert_message(content: Any) -> SystemMessage:
|
||||
if isinstance(content, str):
|
||||
return SystemMessage(type="text", text=content)
|
||||
elif isinstance(content, dict):
|
||||
return SystemMessage(**content)
|
||||
else:
|
||||
raise ValueError(f"Unsupported content type: {type(content)}")
|
||||
|
||||
# Process system messages
|
||||
result: list[SystemMessage] = []
|
||||
|
||||
for message in messages:
|
||||
if message.get("role") == "system":
|
||||
content = message.get("content")
|
||||
|
||||
# Skip empty content
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Handle list or single content
|
||||
if isinstance(content, list):
|
||||
# Process each item in the list
|
||||
for item in content:
|
||||
if item: # Skip empty items
|
||||
result.append(convert_message(item))
|
||||
else:
|
||||
# Process single content
|
||||
result.append(convert_message(content))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def reask_anthropic_tools(
|
||||
kwargs: dict[str, Any],
|
||||
response: Any,
|
||||
exception: Exception,
|
||||
):
|
||||
"""
|
||||
Handle reask for Anthropic tools mode when validation fails.
|
||||
|
||||
Kwargs modifications:
|
||||
- Adds: "messages" (tool result messages indicating validation errors)
|
||||
"""
|
||||
kwargs = kwargs.copy()
|
||||
from anthropic.types import Message
|
||||
|
||||
# Handle Stream objects which are not Message instances
|
||||
# This happens when streaming mode is used with retries
|
||||
if not isinstance(response, Message):
|
||||
kwargs["messages"].append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Validation Error found:\n{exception}\n"
|
||||
"Recall the function correctly, fix the errors"
|
||||
),
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
assistant_content = []
|
||||
tool_use_id = None
|
||||
for content in response.content:
|
||||
assistant_content.append(content.model_dump(exclude_none=True)) # type: ignore
|
||||
if content.type == "tool_use":
|
||||
tool_use_id = content.id
|
||||
|
||||
reask_msgs = [{"role": "assistant", "content": assistant_content}] # type: ignore
|
||||
if tool_use_id is not None:
|
||||
reask_msgs.append( # type: ignore
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_use_id,
|
||||
"content": f"Validation Error found:\n{exception}\nRecall the function correctly, fix the errors",
|
||||
"is_error": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
else:
|
||||
reask_msgs.append( # type: ignore
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Validation Error due to no tool invocation:\n{exception}\nRecall the function correctly, fix the errors",
|
||||
}
|
||||
)
|
||||
kwargs["messages"].extend(reask_msgs)
|
||||
return kwargs
|
||||
|
||||
|
||||
def reask_anthropic_json(
|
||||
kwargs: dict[str, Any],
|
||||
response: Any,
|
||||
exception: Exception,
|
||||
):
|
||||
"""
|
||||
Handle reask for Anthropic JSON mode when validation fails.
|
||||
|
||||
Kwargs modifications:
|
||||
- Adds: "messages" (user message requesting JSON correction)
|
||||
"""
|
||||
kwargs = kwargs.copy()
|
||||
from anthropic.types import Message
|
||||
|
||||
# Handle Stream objects which are not Message instances
|
||||
# This happens when streaming mode is used with retries
|
||||
if not isinstance(response, Message):
|
||||
kwargs["messages"].append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Validation Errors found:\n{exception}\n"
|
||||
"Recall the function correctly, fix the errors"
|
||||
),
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
# Filter for text blocks to handle ThinkingBlock and other non-text content
|
||||
text_blocks = [c for c in response.content if c.type == "text"]
|
||||
if not text_blocks:
|
||||
# Fallback if no text blocks found
|
||||
text_content = "No text content found in response"
|
||||
else:
|
||||
# Use the last text block, similar to function_calls.py:396-397
|
||||
text_content = text_blocks[-1].text
|
||||
|
||||
reask_msg = {
|
||||
"role": "user",
|
||||
"content": f"""Validation Errors found:\n{exception}\nRecall the function correctly, fix the errors found in the following attempt:\n{text_content}""",
|
||||
}
|
||||
kwargs["messages"].append(reask_msg)
|
||||
return kwargs
|
||||
|
||||
|
||||
def handle_anthropic_message_conversion(new_kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Handle message conversion for Anthropic modes when response_model is None.
|
||||
|
||||
Kwargs modifications:
|
||||
- Modifies: "messages" (removes system messages)
|
||||
- Adds/Modifies: "system" (if system messages found in messages)
|
||||
"""
|
||||
messages = new_kwargs.get("messages", [])
|
||||
|
||||
# Handle Anthropic style messages
|
||||
new_kwargs["messages"] = [m for m in messages if m["role"] != "system"]
|
||||
|
||||
if "system" not in new_kwargs:
|
||||
system_messages = extract_system_messages(messages)
|
||||
if system_messages:
|
||||
new_kwargs["system"] = system_messages
|
||||
|
||||
return new_kwargs
|
||||
|
||||
|
||||
def handle_anthropic_tools(
|
||||
response_model: type[Any] | None, new_kwargs: dict[str, Any]
|
||||
) -> tuple[type[Any] | None, dict[str, Any]]:
|
||||
"""
|
||||
Handle Anthropic tools mode.
|
||||
|
||||
When response_model is None:
|
||||
- Extracts system messages from the messages list and moves them to the 'system' parameter
|
||||
- Filters out system messages from the messages list
|
||||
- No tools are configured
|
||||
- Allows for unstructured responses from Claude
|
||||
|
||||
When response_model is provided:
|
||||
- Generates Anthropic tool schema from the response model
|
||||
- Sets up forced tool use with the specific tool name
|
||||
- Extracts and combines system messages
|
||||
- Filters system messages from the messages list
|
||||
|
||||
Kwargs modifications:
|
||||
- Modifies: "messages" (removes system messages)
|
||||
- Adds/Modifies: "system" (combines existing with extracted system messages)
|
||||
- Adds: "tools" (list with tool schema) - only when response_model provided
|
||||
- Adds: "tool_choice" (forced tool use) - only when response_model provided
|
||||
"""
|
||||
if response_model is None:
|
||||
# Just handle message conversion
|
||||
new_kwargs = handle_anthropic_message_conversion(new_kwargs)
|
||||
return None, new_kwargs
|
||||
|
||||
tool_descriptions = generate_anthropic_schema(response_model)
|
||||
new_kwargs["tools"] = [tool_descriptions]
|
||||
new_kwargs["tool_choice"] = {
|
||||
"type": "tool",
|
||||
"name": response_model.__name__,
|
||||
}
|
||||
|
||||
system_messages = extract_system_messages(new_kwargs.get("messages", []))
|
||||
|
||||
if system_messages:
|
||||
new_kwargs["system"] = combine_system_messages(
|
||||
new_kwargs.get("system"), system_messages
|
||||
)
|
||||
|
||||
new_kwargs["messages"] = [
|
||||
m for m in new_kwargs.get("messages", []) if m["role"] != "system"
|
||||
]
|
||||
|
||||
return response_model, new_kwargs
|
||||
|
||||
|
||||
def handle_anthropic_reasoning_tools(
|
||||
response_model: type[Any] | None, new_kwargs: dict[str, Any]
|
||||
) -> tuple[type[Any] | None, dict[str, Any]]:
|
||||
"""
|
||||
Handle Anthropic reasoning tools mode.
|
||||
|
||||
This mode is similar to regular tools mode but with reasoning enabled:
|
||||
- Uses "auto" tool choice instead of forced tool use
|
||||
- Adds a system message encouraging tool use only when relevant
|
||||
- Allows Claude to reason about whether to use tools
|
||||
|
||||
When response_model is None:
|
||||
- Performs the same message conversion as handle_anthropic_tools
|
||||
- No tools are configured
|
||||
|
||||
When response_model is provided:
|
||||
- Sets up tools as in regular tools mode
|
||||
- Changes tool_choice to "auto" to allow reasoning
|
||||
- Adds system message to guide tool usage
|
||||
|
||||
Kwargs modifications:
|
||||
- All modifications from handle_anthropic_tools, plus:
|
||||
- Modifies: "tool_choice" (changes to {"type": "auto"}) - only when response_model provided
|
||||
- Modifies: "system" (adds implicit forced tool message)
|
||||
"""
|
||||
# https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview#forcing-tool-use
|
||||
|
||||
response_model, new_kwargs = handle_anthropic_tools(response_model, new_kwargs)
|
||||
|
||||
if response_model is None:
|
||||
# Just handle message conversion - already done by handle_anthropic_tools
|
||||
return None, new_kwargs
|
||||
|
||||
# https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview#forcing-tool-use
|
||||
# Reasoning does not allow forced tool use
|
||||
new_kwargs["tool_choice"] = {"type": "auto"}
|
||||
|
||||
# But add a message recommending only to use the tools if they are relevant
|
||||
implict_forced_tool_message = dedent(
|
||||
f"""
|
||||
Return only the tool call and no additional text.
|
||||
"""
|
||||
)
|
||||
new_kwargs["system"] = combine_system_messages(
|
||||
new_kwargs.get("system"),
|
||||
[{"type": "text", "text": implict_forced_tool_message}],
|
||||
)
|
||||
return response_model, new_kwargs
|
||||
|
||||
|
||||
def handle_anthropic_json(
|
||||
response_model: type[Any] | None, new_kwargs: dict[str, Any]
|
||||
) -> tuple[type[Any] | None, dict[str, Any]]:
|
||||
"""
|
||||
Handle Anthropic JSON mode.
|
||||
|
||||
This mode instructs Claude to return JSON responses:
|
||||
- System messages are extracted and combined
|
||||
- A JSON schema message is added to guide the response format
|
||||
|
||||
When response_model is None:
|
||||
- Extracts and moves system messages to the 'system' parameter
|
||||
- Filters system messages from the messages list
|
||||
- No JSON schema is added
|
||||
|
||||
When response_model is provided:
|
||||
- Performs system message handling as above
|
||||
- Adds a system message with the JSON schema
|
||||
- Instructs Claude to return an instance matching the schema
|
||||
|
||||
Kwargs modifications:
|
||||
- Modifies: "messages" (removes system messages)
|
||||
- Adds/Modifies: "system" (combines existing with extracted system messages)
|
||||
- Modifies: "system" (adds JSON schema message) - only when response_model provided
|
||||
"""
|
||||
import json
|
||||
|
||||
system_messages = extract_system_messages(new_kwargs.get("messages", []))
|
||||
|
||||
if system_messages:
|
||||
new_kwargs["system"] = combine_system_messages(
|
||||
new_kwargs.get("system"), system_messages
|
||||
)
|
||||
|
||||
new_kwargs["messages"] = [
|
||||
m for m in new_kwargs.get("messages", []) if m["role"] != "system"
|
||||
]
|
||||
|
||||
if response_model is None:
|
||||
# Just handle message conversion - already done above
|
||||
return None, new_kwargs
|
||||
|
||||
json_schema_message = dedent(
|
||||
f"""
|
||||
As a genius expert, your task is to understand the content and provide
|
||||
the parsed objects in json that match the following json_schema:\n
|
||||
|
||||
{json.dumps(response_model.model_json_schema(), indent=2, ensure_ascii=False)}
|
||||
|
||||
Make sure to return an instance of the JSON, not the schema itself
|
||||
"""
|
||||
)
|
||||
|
||||
new_kwargs["system"] = combine_system_messages(
|
||||
new_kwargs.get("system"),
|
||||
[{"type": "text", "text": json_schema_message}],
|
||||
)
|
||||
|
||||
return response_model, new_kwargs
|
||||
|
||||
|
||||
def handle_anthropic_parallel_tools(
|
||||
response_model: type[Any], new_kwargs: dict[str, Any]
|
||||
) -> tuple[Any, dict[str, Any]]:
|
||||
"""
|
||||
Handle Anthropic parallel tools mode.
|
||||
|
||||
Kwargs modifications:
|
||||
- Adds: "tools" (multiple function schemas from parallel model)
|
||||
- Adds: "tool_choice" ("auto" to allow model to choose which tools to call)
|
||||
- Modifies: "system" (moves system messages into system parameter)
|
||||
- Removes: "system" messages from "messages" list
|
||||
- Validates: stream=False
|
||||
"""
|
||||
from ...dsl.parallel import (
|
||||
AnthropicParallelModel,
|
||||
handle_anthropic_parallel_model,
|
||||
)
|
||||
from ...core.exceptions import ConfigurationError
|
||||
|
||||
if new_kwargs.get("stream", False):
|
||||
raise ConfigurationError(
|
||||
"stream=True is not supported when using ANTHROPIC_PARALLEL_TOOLS mode"
|
||||
)
|
||||
|
||||
new_kwargs["tools"] = handle_anthropic_parallel_model(response_model)
|
||||
new_kwargs["tool_choice"] = {"type": "auto"}
|
||||
|
||||
system_messages = extract_system_messages(new_kwargs.get("messages", []))
|
||||
|
||||
if system_messages:
|
||||
new_kwargs["system"] = combine_system_messages(
|
||||
new_kwargs.get("system"), system_messages
|
||||
)
|
||||
|
||||
new_kwargs["messages"] = [
|
||||
m for m in new_kwargs.get("messages", []) if m["role"] != "system"
|
||||
]
|
||||
|
||||
return AnthropicParallelModel(typehint=response_model), new_kwargs
|
||||
|
||||
|
||||
# Handler registry for Anthropic
|
||||
ANTHROPIC_HANDLERS = {
|
||||
Mode.ANTHROPIC_TOOLS: {
|
||||
"reask": reask_anthropic_tools,
|
||||
"response": handle_anthropic_tools,
|
||||
},
|
||||
Mode.ANTHROPIC_JSON: {
|
||||
"reask": reask_anthropic_json,
|
||||
"response": handle_anthropic_json,
|
||||
},
|
||||
Mode.ANTHROPIC_REASONING_TOOLS: {
|
||||
"reask": reask_anthropic_tools,
|
||||
"response": handle_anthropic_reasoning_tools,
|
||||
},
|
||||
Mode.ANTHROPIC_PARALLEL_TOOLS: {
|
||||
"reask": reask_anthropic_tools,
|
||||
"response": handle_anthropic_parallel_tools,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user