roseflow-pinecone 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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