Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for perplexity sonar #1319

Merged
merged 7 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,37 @@ assert resp.name == "Jason"
assert resp.age == 25
```

### Using Perplexity Sonar Models

```python
import instructor
from openai import OpenAI
from pydantic import BaseModel


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


client = instructor.from_perplexity(OpenAI(base_url="https://api.perplexity.ai"))

resp = client.chat.completions.create(
model="sonar",
messages=[
{
"role": "user",
"content": "Extract Jason is 25 years old.",
}
],
response_model=User,
)

assert isinstance(resp, User)
assert resp.name == "Jason"
assert resp.age == 25
```

### Using Litellm

```python
Expand Down
6 changes: 5 additions & 1 deletion instructor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,8 @@
if importlib.util.find_spec("writerai") is not None:
from .client_writer import from_writer

__all__ += ["from_writer"]
__all__ += ["from_writer"]

if importlib.util.find_spec("openai") is not None:
from .client_perplexity import from_perplexity
__all__ += ["from_perplexity"]
63 changes: 63 additions & 0 deletions instructor/client_perplexity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import openai
import instructor
from typing import overload, Any


@overload
def from_perplexity(
client: openai.OpenAI,
mode: instructor.Mode = instructor.Mode.PERPLEXITY_JSON,
**kwargs: Any,
) -> instructor.Instructor: ...


@overload
def from_perplexity(
client: openai.AsyncOpenAI,
mode: instructor.Mode = instructor.Mode.PERPLEXITY_JSON,
**kwargs: Any,
) -> instructor.AsyncInstructor: ...


def from_perplexity(
client: openai.OpenAI | openai.AsyncOpenAI,
mode: instructor.Mode = instructor.Mode.PERPLEXITY_JSON,
**kwargs: Any,
) -> instructor.Instructor | instructor.AsyncInstructor:
"""Create an Instructor client from a Perplexity client.

Args:
client: A Perplexity client (sync or async)
mode: The mode to use for the client (must be PERPLEXITY_JSON)
**kwargs: Additional arguments to pass to the client

Returns:
An Instructor client
"""
assert mode == instructor.Mode.PERPLEXITY_JSON, "Mode must be PERPLEXITY_JSON"

assert isinstance(
client,
(openai.OpenAI, openai.AsyncOpenAI),
), "Client must be an instance of openai.OpenAI or openai.AsyncOpenAI"

if isinstance(client, openai.AsyncOpenAI):
create = client.chat.completions.create
return instructor.AsyncInstructor(
client=client,
create=instructor.patch(create=create, mode=mode),
provider=instructor.Provider.PERPLEXITY,
mode=mode,
**kwargs,
)

create = client.chat.completions.create
return instructor.Instructor(
client=client,
create=instructor.patch(create=create, mode=mode),
provider=instructor.Provider.PERPLEXITY,
mode=mode,
**kwargs,
)
2 changes: 2 additions & 0 deletions instructor/dsl/iterable.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def extract_json(
Mode.JSON_SCHEMA,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down Expand Up @@ -147,6 +148,7 @@ async def extract_json_async(
Mode.JSON_SCHEMA,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down
2 changes: 2 additions & 0 deletions instructor/dsl/partial.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ def extract_json(
Mode.JSON_SCHEMA,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down Expand Up @@ -334,6 +335,7 @@ async def extract_json_async(
Mode.JSON_SCHEMA,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down
1 change: 1 addition & 0 deletions instructor/function_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def from_response(
Mode.JSON_O1,
Mode.CEREBRAS_JSON,
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
}:
return cls.parse_json(completion, validation_context, strict)

Expand Down
1 change: 1 addition & 0 deletions instructor/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Mode(enum.Enum):
FIREWORKS_TOOLS = "fireworks_tools"
FIREWORKS_JSON = "fireworks_json"
WRITER_TOOLS = "writer_tools"
PERPLEXITY_JSON = "perplexity_json"

@classmethod
def warn_mode_functions_deprecation(cls):
Expand Down
14 changes: 14 additions & 0 deletions instructor/process_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,19 @@ def handle_writer_tools(
return response_model, new_kwargs


def handle_perplexity_json(
response_model: type[T], new_kwargs: dict[str, Any]
) -> tuple[type[T], dict[str, Any]]:
new_kwargs["response_format"] = {
"type": "json_schema",
"json_schema": {
"schema": response_model.model_json_schema()
}
}

return response_model, new_kwargs


def is_typed_dict(cls) -> bool:
return (
isinstance(cls, type)
Expand Down Expand Up @@ -744,6 +757,7 @@ def handle_response_model(
Mode.FIREWORKS_JSON: handle_fireworks_json,
Mode.FIREWORKS_TOOLS: handle_fireworks_tools,
Mode.WRITER_TOOLS: handle_writer_tools,
Mode.PERPLEXITY_JSON: handle_perplexity_json,
}

if mode in mode_handlers:
Expand Down
19 changes: 19 additions & 0 deletions instructor/reask.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,24 @@ def reask_writer_tools(
return kwargs


def reask_perplexity_json(
kwargs: dict[str, Any],
response: Any,
exception: Exception,
):
"""Handle reasking for Perplexity JSON mode."""
kwargs = kwargs.copy()
reask_msgs = [dump_message(response.choices[0].message)]
reask_msgs.append(
{
"role": "user",
"content": f"Correct your JSON ONLY RESPONSE, based on the following errors:\n{exception}",
}
)
kwargs["messages"].extend(reask_msgs)
return kwargs


def handle_reask_kwargs(
kwargs: dict[str, Any],
mode: Mode,
Expand All @@ -350,6 +368,7 @@ def handle_reask_kwargs(
Mode.FIREWORKS_TOOLS: reask_fireworks_tools,
Mode.FIREWORKS_JSON: reask_fireworks_json,
Mode.WRITER_TOOLS: reask_writer_tools,
Mode.PERPLEXITY_JSON: reask_perplexity_json,
}
reask_function = functions.get(mode, reask_default)
return reask_function(kwargs=kwargs, response=response, exception=exception)
3 changes: 3 additions & 0 deletions instructor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Provider(Enum):
FIREWORKS = "fireworks"
WRITER = "writer"
UNKNOWN = "unknown"
PERPLEXITY = "perplexity"


def get_provider(base_url: str) -> Provider:
Expand Down Expand Up @@ -84,6 +85,8 @@ def get_provider(base_url: str) -> Provider:
return Provider.VERTEXAI
elif "writer" in str(base_url):
return Provider.WRITER
elif "perplexity" in str(base_url):
return Provider.PERPLEXITY
return Provider.UNKNOWN


Expand Down
Empty file.
13 changes: 13 additions & 0 deletions tests/llm/test_perplexity/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import os

import pytest
from openai import OpenAI


@pytest.fixture(scope="session")
def client():
if os.environ.get("PERPLEXITY_API_KEY"):
yield OpenAI(
api_key=os.environ["PERPLEXITY_API_KEY"],
base_url="https://api.perplexity.ai",
)
96 changes: 96 additions & 0 deletions tests/llm/test_perplexity/test_modes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from itertools import product
from pydantic import BaseModel, Field
from openai import OpenAI
import pytest

import instructor
from .util import models, modes


class Item(BaseModel):
name: str
price: float


class Order(BaseModel):
items: list[Item] = Field(default_factory=list)
customer: str


@pytest.mark.parametrize("model, mode", product(models, modes))
def test_nested(model: str, mode: instructor.Mode, client: OpenAI):
instructor_client = instructor.from_perplexity(client, mode=mode)
content = """
Order Details:
Customer: Jason
Items:

Name: Apple, Price: 0.50
Name: Bread, Price: 2.00
Name: Milk, Price: 1.50
"""

resp = instructor_client.chat.completions.create(
model=model,
response_model=Order,
messages=[
{
"role": "user",
"content": content,
},
],
)

assert len(resp.items) == 3
assert {x.name.lower() for x in resp.items} == {"apple", "bread", "milk"}
assert {x.price for x in resp.items} == {0.5, 2.0, 1.5}
assert resp.customer.lower() == "jason"


class Book(BaseModel):
title: str
author: str
genre: str
isbn: str


class LibraryRecord(BaseModel):
books: list[Book] = Field(default_factory=list)
visitor: str
library_id: str


@pytest.mark.parametrize("model, mode", product(models, modes))
def test_complex_nested_model(model: str, mode: instructor.Mode, client: OpenAI):
instructor_client = instructor.from_perplexity(client, mode=mode)

content = """
Library visit details:
Visitor: Jason
Library ID: LIB123456
Books checked out:
- Title: The Great Adventure, Author: Jane Doe, Genre: Fantasy, ISBN: 1234567890
- Title: History of Tomorrow, Author: John Smith, Genre: Non-Fiction, ISBN: 0987654321
"""

resp = instructor_client.chat.completions.create(
model=model,
response_model=LibraryRecord,
messages=[
{
"role": "user",
"content": content,
},
],
)

assert resp.visitor.lower() == "jason"
assert resp.library_id == "LIB123456"
assert len(resp.books) == 2
assert {book.title for book in resp.books} == {
"The Great Adventure",
"History of Tomorrow",
}
assert {book.author for book in resp.books} == {"Jane Doe", "John Smith"}
assert {book.genre for book in resp.books} == {"Fantasy", "Non-Fiction"}
assert {book.isbn for book in resp.books} == {"1234567890", "0987654321"}
4 changes: 4 additions & 0 deletions tests/llm/test_perplexity/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from instructor import Mode

models = ["sonar", "sonar-pro"]
modes = [Mode.PERPLEXITY_JSON]