Skip to content

Commit

Permalink
feat(instructor): introduce ANTHROPIC_JSON mode (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
jxnl committed Mar 29, 2024
1 parent 3addfea commit 5c2496c
Show file tree
Hide file tree
Showing 38 changed files with 252 additions and 211 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/evals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name: Weekly Tests
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # Runs at 00:00 UTC every Sunday
- cron: "0 0 * * 0" # Runs at 00:00 UTC every Sunday
push:
branches: [ main ]
branches: [main]
paths-ignore:
- '**' # Ignore all paths to ensure it only triggers on schedule
- "**" # Ignore all paths to ensure it only triggers on schedule

jobs:
weekly-tests:
Expand All @@ -20,15 +20,15 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: 'poetry'
cache: "poetry"

- name: Install Poetry
uses: snok/install-poetry@v1.3.1

- name: Install dependencies
run: poetry install --with dev
run: poetry install --with dev,anthropic

- name: Run all tests
run: poetry run pytest tests/
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
4 changes: 2 additions & 2 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Ruff
on:
push:
pull_request:
branches: [ main ]
branches: [main]

env:
WORKING_DIRECTORY: "."
Expand Down Expand Up @@ -42,4 +42,4 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: ruff-log
path: ${{ env.WORKING_DIRECTORY }}/${{ env.RUFF_OUTPUT_FILENAME }}
path: ${{ env.WORKING_DIRECTORY }}/${{ env.RUFF_OUTPUT_FILENAME }}
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ jobs:
uses: snok/install-poetry@v1.3.1

- name: Install dependencies
run: poetry install --with dev anthropic
run: poetry install --with dev,anthropic

- name: Run tests
run: poetry run pytest tests/ -k "not openai"
run: poetry run pytest tests/ -k "not openai and not anthropic and not evals"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

- name: Generate coverage report
if: matrix.python-version == '3.11'
run: |
poetry run coverage run -m pytest tests/ -k "not openai"
poetry run coverage run -m pytest tests/
poetry run coverage report
poetry run coverage html
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

- name: Coveralls GitHub Action
if: matrix.python-version == '3.11'
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/test_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

strategy:
matrix:
python-version: ['3.11']
python-version: ["3.11"]

steps:
- uses: actions/checkout@v2
Expand All @@ -22,8 +22,8 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
cache: "poetry"

- name: Cache Poetry virtualenv
uses: actions/cache@v2
with:
Expand All @@ -33,9 +33,9 @@ jobs:
${{ runner.os }}-poetry-
- name: Install dependencies
run: poetry install --with dev,docs,test-docs
run: poetry install --with dev,docs,test-docs,anthropic

- name: Run tests
run: poetry run pytest tests/openai/docs
run: poetry run pytest tests/llm/test_openai/docs
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
4 changes: 2 additions & 2 deletions docs/blog/posts/anthropic.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ authors:

A special shoutout to [Shreya](https://twitter.com/shreyaw_) for her contributions to the anthropic support. As of now, all features are operational with the exception of streaming support.

For those eager to experiment, simply patch the client with `ANTHROPIC_TOOLS`, which will enable you to leverage the `anthropic` client for making requests.
For those eager to experiment, simply patch the client with `ANTHROPIC_JSON`, which will enable you to leverage the `anthropic` client for making requests.

```
pip install instructor[anthropic]
Expand All @@ -28,7 +28,7 @@ import instructor
# Patching the Anthropics client with the instructor for enhanced capabilities
anthropic_client = instructor.patch(
create=anthropic.Anthropic().messages.create,
mode=instructor.Mode.ANTHROPIC_TOOLS
mode=instructor.Mode.ANTHROPIC_JSON
)

class Properties(BaseModel):
Expand Down
10 changes: 5 additions & 5 deletions docs/concepts/partial.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ print(extraction.model_dump_json(indent=2))
"twitter": "@CodeMaster2023"
}
],
"date": "March 15th, 2024",
"location": "Grand Tech Arena located at 4521 Innovation Drive",
"date": "2024-03-15",
"location": "Grand Tech Arena, 4521 Innovation Drive",
"budget": 50000,
"deadline": "February 20th"
"deadline": "2024-02-20"
}
"""
```
Expand Down Expand Up @@ -161,8 +161,8 @@ async def print_partial_results():
async for m in user:
print(m)
#> name=None age=None
#> name='' age=None
#> name='Jason' age=None
#> name=None age=12
#> name='' age=12
#> name='Jason' age=12


Expand Down
31 changes: 31 additions & 0 deletions docs/concepts/patching.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,34 @@ from openai import OpenAI

client = instructor.patch(OpenAI(), mode=instructor.Mode.MD_JSON)
```

## Anthropic JSON Mode

Anthropic JSON mode uses Anthropic's JSON format for responses by setting the `mode` parameter to `instructor.Mode.ANTHROPIC_JSON` when patching the client.

```python
import instructor
from anthropic import Anthropic

create = instructor.patch(
create=anthropic.Anthropic().messages.create, mode=instructor.Mode.ANTHROPIC_JSON
)


class User(BaseModel):
name: str
age: int


resp = create(
model="claude-3-haiku-20240307",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Create a user",
}
],
response_model=User,
)
```
8 changes: 4 additions & 4 deletions docs/concepts/raw_response.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ user: UserExtract = client.chat.completions.create(
print(user._raw_response)
"""
ChatCompletion(
id='chatcmpl-8zpltT9vXJdO5OE3AfDsOhAUr911A',
id='chatcmpl-97whMjk6Nyh2s2Nacd4Yi1r5a4RvV',
choices=[
Choice(
finish_reason='stop',
Expand All @@ -37,7 +37,7 @@ ChatCompletion(
function_call=None,
tool_calls=[
ChatCompletionMessageToolCall(
id='call_vXI3foz7jqlzFILU9pwuYJZB',
id='call_kjS8AZWNS5yElb0ZxmkjTHY0',
function=Function(
arguments='{"name":"Jason","age":25}', name='UserExtract'
),
Expand All @@ -47,10 +47,10 @@ ChatCompletion(
),
)
],
created=1709747709,
created=1711680960,
model='gpt-3.5-turbo-0125',
object='chat.completion',
system_fingerprint='fp_2b778c6b35',
system_fingerprint='fp_3bc1b5746c',
usage=CompletionUsage(completion_tokens=9, prompt_tokens=82, total_tokens=91),
)
"""
Expand Down
8 changes: 4 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ print(response.model_dump_json(indent=2))
print(user._raw_response.model_dump_json(indent=2))
"""
{
"id": "chatcmpl-8zplvRbNM8iKSVa3Ld9NmVICeXZZ9",
"id": "chatcmpl-97whNVWUbRXd44lZd9gG8sS0xKDXF",
"choices": [
{
"finish_reason": "stop",
Expand All @@ -145,7 +145,7 @@ print(response.model_dump_json(indent=2))
"function_call": null,
"tool_calls": [
{
"id": "call_V5FRMSXrHFFTTqTjpwA76h7t",
"id": "call_CsCj1hzAMZ3mik8rvRfMienA",
"function": {
"arguments": "{\"name\":\"Jason\",\"age\":25}",
"name": "UserDetail"
Expand All @@ -156,10 +156,10 @@ print(response.model_dump_json(indent=2))
}
}
],
"created": 1709747711,
"created": 1711680961,
"model": "gpt-3.5-turbo-0125",
"object": "chat.completion",
"system_fingerprint": "fp_2b778c6b35",
"system_fingerprint": "fp_3bc1b5746c",
"usage": {
"completion_tokens": 9,
"prompt_tokens": 81,
Expand Down
32 changes: 0 additions & 32 deletions examples/classification/test_run.py

This file was deleted.

2 changes: 1 addition & 1 deletion examples/match_language/run_v1.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field
from pydantic import BaseModel
from instructor import patch
from openai import AsyncOpenAI
from langdetect import detect
Expand Down
8 changes: 7 additions & 1 deletion instructor/function_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ def from_response(
assert hasattr(completion, "content")
return xml_to_model(cls, extract_xml(completion.content[0].text)) # type:ignore

assert hasattr(completion, "choices")
if mode == Mode.ANTHROPIC_JSON:
assert hasattr(completion, "content")
text = completion.content[0].text # type: ignore
extra_text = extract_json_from_codeblock(text)
return cls.model_validate_json(extra_text)

assert hasattr(completion, "choices"), "No choices in completion"

if completion.choices[0].finish_reason == "length":
logger.error("Incomplete output detected, should increase max_tokens")
Expand Down
1 change: 1 addition & 0 deletions instructor/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Mode(enum.Enum):
MD_JSON = "markdown_json_mode"
JSON_SCHEMA = "json_schema_mode"
ANTHROPIC_TOOLS = "anthropic_tools"
ANTHROPIC_JSON = "anthropic_json"

def __new__(cls, value: str) -> "Mode":
member = object.__new__(cls)
Expand Down
32 changes: 30 additions & 2 deletions instructor/process_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from openai.types.chat import ChatCompletion
from pydantic import BaseModel


import json
import inspect
import logging
from typing import (
Expand Down Expand Up @@ -245,7 +245,7 @@ def handle_response_model(
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
{response_model.model_json_schema()}
{json.dumps(response_model.model_json_schema(), indent=2)}
Make sure to return an instance of the JSON, not the schema itself
"""
Expand Down Expand Up @@ -305,6 +305,34 @@ def handle_response_model(
new_kwargs["system"] = f"{system_prompt}\n{new_kwargs['system']}"
else:
new_kwargs["system"] = system_prompt
elif mode == Mode.ANTHROPIC_JSON:
# anthropic wants system message to be a string so we first extract out any system message
openai_system_messages = [
message["content"]
for message in new_kwargs.get("messages", [])
if message["role"] == "system"
]

new_kwargs["system"] = (
new_kwargs.get("system", "")
+ "\n\n"
+ "\n\n".join(openai_system_messages)
)

new_kwargs["system"] += f"""
You must only response in JSON format that adheres to the following schema:
<JSON_SCHEMA>
{json.dumps(response_model.model_json_schema(), indent=2)}
</JSON_SCHEMA>
"""
new_kwargs["system"] = dedent(new_kwargs["system"])

new_kwargs["messages"] = [
message
for message in new_kwargs.get("messages", [])
if message["role"] != "system"
]
else:
raise ValueError(f"Invalid patch mode: {mode}")

Expand Down
Loading

0 comments on commit 5c2496c

Please sign in to comment.