alexrudall / ruby-openai

OpenAI API + Ruby! 🤖❤️ NEW: Assistant Vector Stores
MIT License
2.61k stars 302 forks source link

Validated Schema #417

Closed jxnl closed 1 month ago

jxnl commented 6 months ago

Is your feature request related to a problem? Please describe.

I'm the maintainer of Instructor which uses pydantic (python's validation) library to improve the sdk usaibility. If I wanted to do something like this in ruby, do you recommend ruby-openai support it? or should there be some seperate library that patchs this one, and adds the various new keywords?

In python it looks like

import instructor
from openai import OpenAI
from pydantic import BaseModel

# This enables response_model keyword
# from client.chat.completions.create
client = instructor.patch(OpenAI())

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

user = client.chat.completions.create(
    model="gpt-3.5-turbo",
    response_model=UserDetail,
    messages=[
        {"role": "user", "content": "Extract Jason is 25 years old"},
    ]
)

assert isinstance(user, UserDetail)
assert user.name == "Jason"
assert user.age == 25

In ruby you can imagine using

UserSchema = Dry::Validation.Schema do
  required(:name).filled(:str?)
  required(:age).filled(:int?, gt?: 0)
end

response = client.chat(
    parameters: {
        model: "gpt-3.5-turbo", # Required.
        messages: [{ role: "user", content: "Extract Jason is 25 years old"}], # Required.
        temperature: 0.7,
    },
    response_model: UserSchema
)

 if response.success?
  # The response is valid, assert each attribute
  user = validation_result.output
  raise 'Name is incorrect' unless user[:name] == "Jason"
  raise 'Age is incorrect'  unless user[:age] == 25

Instructor is mostly a bunch of docs on how to 'think' about the idea. like: https://jxnl.github.io/instructor/concepts/prompting/

kaiwren commented 5 months ago

Somewhat tangentially - I personally tend to favour encapsulating the API call inside a factory method on a PORO domain class that then builds an instance from the response. I then use ActiveModel::Validations for validations on that instance. Something like:

user = User.extract_from('Jason is 25 years old')
if user.valid?
  puts user.name, user.age
else
  puts user.errors.full_messages.join("\n") user.valid?
end
jxnl commented 5 months ago

Somewhat tangentially - I personally tend to favour encapsulating the API call inside a factory method on a PORO domain class that then builds an instance from the response. I then use ActiveModel::Validations for validations on that instance. Something like:

user = User.extract_from('Jason is 25 years old')
if user.valid?
  puts user.name, user.age
else
  puts user.errors.full_messages.join("\n") user.valid?
end

ok, you should check out marvin i think they have the cleanest version of that.

I think its importnat to give the users constrol of the whole messages array.

sergiobayona commented 5 months ago

@jxnl seems like ruby-openai would be able to support it with some patching. Although I wouldn't use dry-validation. I would use ActiveModel.

Something else to consider is that these libs don't output json schema so that'd need to be added... I think.

btw fantastic work you're doing with Instructor. I'm following closely. Here's how it might look like in Ruby:

class UserDetail
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :age, :integer
end

client = OpenAI::Client.new

user = client.chat(
  parameters: {
    model: "gpt-3.5-turbo",
    response_model: UserDetail,
    messages: [{ "role": "user", "content": "Extract Jason is 25 years old" }]
  }
)

RSpec.describe "Attribute Assignment" do
  it "assigns a value to an attribute" do
    expect(user).to be_instance_of(UserDetail)
    expect(user.name).to eq("Jason")
    expect(user.age).to eq(25)
  end
end
jxnl commented 5 months ago

I think that would be awesome. good go everyone.

@jxnl seems like ruby-openai would be able to support it with some patching. Although I wouldn't use dry-validation. I would use ActiveModel.

Something else to consider is that these libs don't output json schema so that'd need to be added... I think.

btw fantastic work you're doing with Instructor. I'm following closely. Here's how it might look like in Ruby:

class UserDetail
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :age, :integer
end

client = OpenAI::Client.new

user = client.chat(
  parameters: {
    model: "gpt-3.5-turbo",
    response_model: UserDetail,
    messages: [{ "role": "user", "content": "Extract Jason is 25 years old" }]
  }
)

RSpec.describe "Attribute Assignment" do
  it "assigns a value to an attribute" do
    expect(user).to be_instance_of(UserDetail)
    expect(user.name).to eq("Jason")
    expect(user.age).to eq(25)
  end
end

That would be awesome. I'm not much of a Ruby developer, but I think it could benefit a lot of folks. We've had a lot of progress on the JavaScript side.

sergiobayona commented 5 months ago

@jxnl I'd like to take a stab at a solution for Ruby for the problem you are solving with Instructor. Would you be interested in collaborating?

jxnl commented 5 months ago

For sure! Are you on Twitter dm me @jxnlco

sergiobayona commented 1 month ago

I think this issue can be closed since instructor-rb solves it. cc @alexrudall