roseflow-pinecone 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '026090287ba3476e5380c6c608deb67ddca3900e29ec73039095fcc8958622db'
4
+ data.tar.gz: 7e8b2e32985dca38c6ed694ab64f64996b640f437de9beec0c6df9dd37842f16
5
+ SHA512:
6
+ metadata.gz: ea6af6e5198bb2dc34bef0cfa538eb188efb0873d2d3945502ba5d2773bce04743462ff9cbb6c072de0ae5d12b45d0aa5d154a5fab835a37717a8a4262a478c3
7
+ data.tar.gz: 821b6c710661e44eca7b2e3e5f0330afd006a55b38810bc71e9e02ba623b1ee8703582eb51aee8fca717c512de9959d6c43937a8199d1aea28838bfc5bfa178e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-05-10
4
+
5
+ - Initial release
@@ -0,0 +1,7 @@
1
+ # Contributor Code of Conduct
2
+
3
+ The Roseflow team is committed to fostering a welcoming community.
4
+
5
+ **Our Code of Conduct can be found here**:
6
+
7
+ https://roseflow.ai/conduct
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in roseflow-pinecone.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.5"
10
+ gem "standard", "~> 1.3"
11
+
12
+ eval File.read("Gemfile.local") if File.exist?("Gemfile.local")
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Lauri Jutila
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Roseflow Pinecone integration
2
+
3
+ This gem adds Pinecone vector database support and integration for Roseflow, a framework for interacting with AI in Ruby.
4
+
5
+ ## Prerequisites
6
+
7
+ To use this gem effectively, you need the [core Roseflow gem](https://github.com/roseflow-ai/roseflow).
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add roseflow-pinecone
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install roseflow-pinecone
18
+
19
+ ## Usage
20
+
21
+ See full documentation how to configure and use Roseflow with Pinecone at [docs.roseflow.ai](https://docs.roseflow.ai/pinecone).
22
+
23
+ ## Contributing
24
+
25
+ Bug reports and pull requests are welcome on GitHub at https://github.com/roseflow-ai/roseflow-pinecone.
26
+
27
+ ## Community
28
+
29
+ ### Discord
30
+
31
+ Join us in our [Discord](https://discord.gg/roseflow).
32
+
33
+ ### Twitter
34
+
35
+ Connect with the core team on Twitter.
36
+
37
+ <a href="https://twitter.com/ljuti" target="_blank">
38
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/ljuti?logo=twitter&style=social">
39
+ </a>
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
44
+
45
+ ## Code of Conduct
46
+
47
+ Everyone interacting in the Roseflow OpenAI project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/roseflow-ai/roseflow-pinecone/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,3 @@
1
+ default: &default
2
+ environment: <YOUR PINECONE ENVIRONMENT>
3
+ api_key: <YOUR PINECONE API KEY>
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+
6
+ require "roseflow/pinecone/index"
7
+ require "roseflow/pinecone/vector"
8
+
9
+ module Roseflow
10
+ module Pinecone
11
+ class Client
12
+ attr_accessor :index_url
13
+
14
+ def initialize(config = Config.new)
15
+ @environment = config.environment
16
+ @api_key = config.api_key
17
+ @index_url = nil
18
+ end
19
+
20
+ def indices
21
+ Index.new(connection).list.body
22
+ end
23
+
24
+ alias_method :indexes, :indices
25
+
26
+ def describe_index(name)
27
+ Index.new(connection).describe(name)
28
+ end
29
+
30
+ def create_index(name, options = {})
31
+ Index.new(connection).create(name, options)
32
+ end
33
+
34
+ def delete_index(name)
35
+ Index.new(connection).delete(name)
36
+ end
37
+
38
+ def collections
39
+ Collection.new(connection).list
40
+ end
41
+
42
+ def describe_collection(name)
43
+ Collection.new(connection).describe(name)
44
+ end
45
+
46
+ def create_collection(name, source)
47
+ Collection.new(connection).create(name, source)
48
+ end
49
+
50
+ def delete_collection(name)
51
+ Collection.new(connection).delete(name)
52
+ end
53
+
54
+ def index(name)
55
+ @index ||= get_vector_index(name)
56
+ end
57
+
58
+ private
59
+
60
+ def get_vector_index(name)
61
+ return Vector.new(index_connection(index_url)) if index_url && index_url.match(/#{name}/)
62
+ @index_url = describe_index(name).object.status.host
63
+ return Vector.new(index_connection(@index_url))
64
+ end
65
+
66
+ def environment_url(env)
67
+ "https://controller.#{env}.pinecone.io"
68
+ end
69
+
70
+ def index_url_for(url)
71
+ "https://#{url}"
72
+ end
73
+
74
+ def connection
75
+ @connection ||= Faraday.new(
76
+ url: environment_url(@environment),
77
+ headers: {
78
+ "accept": "application/json; charset=utf-8",
79
+ "Api-Key": @api_key,
80
+ }
81
+ ) do |faraday|
82
+ faraday.request :json
83
+ faraday.adapter Faraday.default_adapter
84
+ end
85
+ end
86
+
87
+ def index_connection(index_url = nil)
88
+ raise IndexURLNotSetError, "You must provide index URL to access an index" unless index_url
89
+ @index_connection ||= Faraday.new(
90
+ url: index_url_for(index_url),
91
+ headers: {
92
+ "accept": "application/json; charset=utf-8",
93
+ "Api-Key": @api_key,
94
+ }
95
+ ) do |faraday|
96
+ faraday.request :json
97
+ faraday.adapter Faraday.default_adapter
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/pinecone/response"
4
+ require "roseflow/pinecone/structs"
5
+
6
+ module Roseflow
7
+ module Pinecone
8
+ class Collection
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ def list
14
+ CollectionResponse.new(
15
+ method: :list,
16
+ response: connection.get("/collections")
17
+ )
18
+ end
19
+
20
+ def describe(name)
21
+ CollectionResponse.new(
22
+ method: :describe,
23
+ response: connection.get("/collections/#{name}")
24
+ )
25
+ end
26
+
27
+ def create(name = "", source_index = "")
28
+ CollectionResponse.new(
29
+ method: :create,
30
+ response: connection.post("/collections", { name: name, source: source_index })
31
+ )
32
+ end
33
+
34
+ def delete(name)
35
+ CollectionResponse.new(
36
+ method: :delete,
37
+ response: connection.delete("/collections/#{name}")
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :connection
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+
5
+ module Roseflow
6
+ module Pinecone
7
+ class Config < Anyway::Config
8
+ config_name :pinecone
9
+
10
+ attr_config :api_key, :environment
11
+
12
+ required :api_key
13
+ required :environment
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/pinecone/response"
4
+ require "roseflow/pinecone/structs"
5
+
6
+ module Roseflow
7
+ module Pinecone
8
+ class Index
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ def list
14
+ IndexResponse.new(
15
+ method: :list,
16
+ response: connection.get("/databases")
17
+ )
18
+ end
19
+
20
+ def describe(name)
21
+ IndexResponse.new(
22
+ method: :describe,
23
+ response: connection.get("/databases/#{name}")
24
+ )
25
+ end
26
+
27
+ def create(name, options = {})
28
+ IndexResponse.new(
29
+ method: :create,
30
+ response: connection.post("/databases", create_payload(name, options))
31
+ )
32
+ end
33
+
34
+ def delete(name)
35
+ IndexResponse.new(
36
+ method: :delete,
37
+ response: connection.delete("/databases/#{name}")
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :connection
44
+
45
+ def create_payload(name, options)
46
+ options.merge({
47
+ name: name
48
+ })
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module Roseflow
6
+ module Pinecone
7
+ class PineconeObject < Dry::Struct
8
+ defines :contract_object
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roseflow
4
+ module Pinecone
5
+ class BaseResponse
6
+ attr_reader :body
7
+ attr_reader :status
8
+ attr_reader :object
9
+
10
+ def initialize(method:, response:)
11
+ @response = response
12
+ @method_ = method
13
+ handle_response
14
+ end
15
+
16
+ def success?
17
+ @response.success?
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :response
23
+ end
24
+
25
+ class CollectionResponse < BaseResponse
26
+ private
27
+
28
+ def handle_response
29
+ case @method_
30
+ when :list
31
+ @body = JSON.parse(response.body)
32
+ when :describe
33
+ if response.body.match(/{*.}/)
34
+ @body = JSON.parse(response.body)
35
+ @object = Collection::Description.new(@body)
36
+ end
37
+ when :create
38
+ if response.body.match(/{*.}/)
39
+ @body = JSON.parse(response.body)
40
+ else
41
+ @body = response.body
42
+ end
43
+ end
44
+ @status = response.status
45
+ end
46
+ end
47
+
48
+ class VectorResponse < BaseResponse
49
+ attr_reader :upsert_count, :vectors
50
+
51
+ def upsert_count
52
+ @body.fetch("upsertedCount")
53
+ end
54
+
55
+ def message
56
+ @body.fetch("message")
57
+ end
58
+
59
+ def success?
60
+ @response.success? && @status == 200
61
+ end
62
+
63
+ def failure?
64
+ !success?
65
+ end
66
+
67
+ def vectors
68
+ @vectors.map do |vector|
69
+ Vector::VectorObject.new(vector.last)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def handle_response
76
+ case @method_
77
+ when :upsert
78
+ @body = JSON.parse(@response.body)
79
+ when :delete
80
+ @body = JSON.parse(@response.body)
81
+ when :update
82
+ @body = JSON.parse(@response.body)
83
+ end
84
+ @status = response.status
85
+ end
86
+ end
87
+
88
+ class VectorQueryResponse < VectorResponse
89
+ def vectors
90
+ case @method_
91
+ when :fetch
92
+ @vectors.map do |vector|
93
+ Vector::VectorObject.new(vector.last)
94
+ end
95
+ when :query
96
+ @vectors.map do |vector|
97
+ Vector::VectorObject.new(vector)
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def handle_response
105
+ case @method_
106
+ when :fetch
107
+ @body = JSON.parse(@response.body)
108
+ @vectors = @body["vectors"]
109
+ when :query
110
+ @body = JSON.parse(@response.body)
111
+ @vectors = @body["matches"]
112
+ end
113
+ @status = @response.status
114
+ end
115
+ end
116
+
117
+ class IndexResponse < BaseResponse
118
+ private
119
+
120
+ def handle_response
121
+ case @method_
122
+ when :list
123
+ @body = JSON.parse(@response.body)
124
+ when :describe
125
+ if @response.body.match(/{*.}/)
126
+ @body = JSON.parse(@response.body)
127
+ attrs = @body["database"].merge({ status: @body["status"] })
128
+ @object = Index::Description.new(attrs)
129
+ end
130
+ when :create
131
+ if @response.body.match(/{*.}/)
132
+ @body = JSON.parse(@response.body)
133
+ else
134
+ @body = @response.body
135
+ end
136
+ end
137
+
138
+ @status = @response.status
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,44 @@
1
+ require "dry-struct"
2
+
3
+ module Types
4
+ include Dry.Types()
5
+ end
6
+
7
+ module Roseflow
8
+ module Pinecone
9
+ class Index
10
+ class Status < Dry::Struct
11
+ transform_keys(&:to_sym)
12
+
13
+ attribute :host, Types::Strict::String
14
+ attribute :port, Types::Strict::Integer
15
+ attribute :state, Types::Strict::String
16
+ attribute :ready, Types::Strict::Bool
17
+ end
18
+
19
+ class Description < Dry::Struct
20
+ transform_keys(&:to_sym)
21
+
22
+ attribute :name, Types::Strict::String
23
+ attribute :metric, Types::Strict::String
24
+ attribute :dimension, Types::Strict::Integer
25
+ attribute :replicas, Types::Strict::Integer
26
+ attribute :shards, Types::Strict::Integer
27
+ attribute :pods, Types::Strict::Integer
28
+ attribute :pod_type, Types::Strict::String
29
+
30
+ attribute :status, Status
31
+ end
32
+ end
33
+
34
+ class Collection
35
+ class Description < Dry::Struct
36
+ transform_keys(&:to_sym)
37
+
38
+ attribute :name, Types::Strict::String
39
+ attribute :status, Types::Strict::String
40
+ attribute :size, Types::Strict::Integer.default(0)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/pinecone/vectors/deletion"
4
+ require "roseflow/pinecone/vectors/query"
5
+ require "roseflow/pinecone/vectors/upsert"
6
+ require "roseflow/pinecone/vectors/update"
7
+ require "roseflow/pinecone/response"
8
+
9
+ module Roseflow
10
+ module Pinecone
11
+ class Vector
12
+ attr_reader :connection
13
+
14
+ def initialize(connection)
15
+ @connection = connection
16
+ end
17
+
18
+ def self.build(id:, values:, sparse_values: nil, metadata: nil)
19
+ VectorObject.new(
20
+ id: id,
21
+ values: values,
22
+ sparse_values: sparse_values,
23
+ metadata: metadata,
24
+ )
25
+ end
26
+
27
+ def empty?(namespace = "")
28
+ query(namespace: namespace).vectors.empty?
29
+ end
30
+
31
+ def query(query)
32
+ object = query.is_a?(Query) ? query : Query.new(query)
33
+ VectorQueryResponse.new(
34
+ method: :query,
35
+ response: @connection.post("/query", object.to_json),
36
+ )
37
+ end
38
+
39
+ def delete(query)
40
+ object = query.is_a?(Deletion) ? query : Deletion.new(query)
41
+ VectorResponse.new(
42
+ method: :delete,
43
+ response: @connection.post("/vectors/delete", object.to_json),
44
+ )
45
+ end
46
+
47
+ def update(vectors)
48
+ object = vectors.is_a?(Update) ? vectors : Update.new(vectors)
49
+ VectorResponse.new(
50
+ method: :update,
51
+ response: @connection.post("/vectors/update", object.to_json),
52
+ )
53
+ end
54
+
55
+ def upsert(data)
56
+ object = data.is_a?(Upsert) ? data : Upsert.from(data)
57
+ VectorResponse.new(
58
+ method: :upsert,
59
+ response: @connection.post("/vectors/upsert", object.to_json),
60
+ )
61
+ end
62
+
63
+ def fetch(options)
64
+ query_string = URI.encode_www_form(
65
+ {
66
+ namespace: options.fetch(:namespace, ""),
67
+ ids: options.fetch(:ids, []),
68
+ }
69
+ )
70
+ VectorQueryResponse.new(
71
+ method: :fetch,
72
+ response: @connection.get("/vectors/fetch?#{query_string}"),
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/vector_stores/base"
4
+ require "roseflow/pinecone"
5
+ require "roseflow/pinecone/vector"
6
+ require "roseflow/pinecone/vectors/vector_object"
7
+
8
+ module Roseflow
9
+ module Pinecone
10
+ class VectorStore < Roseflow::VectorStores::Base
11
+ attr_reader :client
12
+
13
+ def initialize
14
+ @client = Roseflow::Pinecone::Client.new
15
+ end
16
+
17
+ def index(name)
18
+ client.index(name)
19
+ end
20
+
21
+ def build_vector(name, content)
22
+ Vector.build(id: name, values: content)
23
+ end
24
+
25
+ def create_vector(name, vector, **options)
26
+ index(name).upsert(options.merge(vectors: [vector]))
27
+ end
28
+
29
+ def delete_vector(name, id)
30
+ index(name).delete(ids: [id])
31
+ end
32
+
33
+ def update_vector(name, vector)
34
+ index(name).update(vector)
35
+ end
36
+
37
+ def query(name, query)
38
+ index(name).query(query).vectors
39
+ end
40
+
41
+ alias_method :where, :query
42
+
43
+ def find(name, ids, **options)
44
+ index(name).fetch(options.merge(ids: ids)).vectors.first
45
+ end
46
+ end
47
+ end
48
+ end