sveda-ruby-sdk 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/lib/sveda/chat.rb +23 -0
- data/lib/sveda/client.rb +51 -0
- data/lib/sveda/embed.rb +24 -0
- data/lib/sveda/embed_token.rb +26 -0
- data/lib/sveda/errors.rb +30 -0
- data/lib/sveda/histories.rb +33 -0
- data/lib/sveda/http.rb +145 -0
- data/lib/sveda/message.rb +27 -0
- data/lib/sveda/stream_event.rb +31 -0
- data/lib/sveda/stream_parser.rb +69 -0
- data/lib/sveda/version.rb +5 -0
- data/lib/sveda-ai.rb +3 -0
- data/lib/sveda-ruby-sdk.rb +3 -0
- data/lib/sveda.rb +16 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c41afde9523f250320a9ab947ca732354edf6643cfe53841433113fe19fcb5d0
|
|
4
|
+
data.tar.gz: 1e204829c48bfb1c2c03e2b3e69ccf540518b71f64c3589624e8c1a9c674e15b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 22f8a78f1e68f2f57565fee6e5fbcaa334150d23125447e8d9f5c7361306d59afefe5ee903fe3606d38454075b3b86491f8f8e56d3c714c0e2eeb1c00e3ccf25
|
|
7
|
+
data.tar.gz: aec392c0354deb512a72edfd86c2bf22c4bb75bd60f74e74ba04c6ba011593fb1c4ce255182091f6c1604cf856306559e1caddd123a03a587e3b2ccd189aeaca
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sveda AI Contributors
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# sveda-ruby-sdk
|
|
2
|
+
|
|
3
|
+
Ruby SDK for the [Sveda AI](https://github.com/neresson/sveda) sidecar HTTP API.
|
|
4
|
+
|
|
5
|
+
RubyGems: `sveda-ruby-sdk`
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "sveda-ruby-sdk"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
gem install sveda-ruby-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "sveda"
|
|
21
|
+
|
|
22
|
+
client = Sveda::Client.new(base_url: "https://sveda.example.com", host_api_key: host_key)
|
|
23
|
+
token = client.embed.create_token(visitor_id: "user-1")
|
|
24
|
+
|
|
25
|
+
client = Sveda::Client.new(base_url: "https://sveda.example.com", embed_token: token.token)
|
|
26
|
+
client.chat.create_streamed(
|
|
27
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
28
|
+
chatId: "chat-1"
|
|
29
|
+
) do |event|
|
|
30
|
+
puts event.type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
session = Sveda::Client.start_host_session(
|
|
34
|
+
base_url: "https://sveda.example.com",
|
|
35
|
+
host_api_key: host_key,
|
|
36
|
+
visitor_id: "user-1"
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
data/lib/sveda/chat.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class Chat
|
|
5
|
+
def initialize(transport)
|
|
6
|
+
@transport = transport
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create(params = {}, **kwargs)
|
|
10
|
+
payload = params.merge(kwargs)
|
|
11
|
+
Message.from_payload(@transport.request_json("POST", "/sveda/message", payload))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_streamed(params = {}, **kwargs, &block)
|
|
15
|
+
payload = params.merge(kwargs)
|
|
16
|
+
body = @transport.request_stream("POST", "/sveda/stream", payload)
|
|
17
|
+
parser = StreamParser.new
|
|
18
|
+
return parser.each(body, &block) if block
|
|
19
|
+
|
|
20
|
+
parser.each(body)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/sveda/client.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class Client
|
|
5
|
+
def initialize(base_url:, host_api_key: nil, embed_token: nil, transport: nil, timeout: 30, open_timeout: 5, headers: {})
|
|
6
|
+
@base_url = base_url.to_s.sub(%r{/\z}, "")
|
|
7
|
+
auth_headers = headers.dup
|
|
8
|
+
auth_headers["Authorization"] = "Bearer #{host_api_key}" if host_api_key.to_s != ""
|
|
9
|
+
auth_headers["X-Sveda-Embed-Token"] = embed_token.to_s if embed_token.to_s != ""
|
|
10
|
+
inner = transport || Http.new(base_url: @base_url, timeout: timeout, open_timeout: open_timeout)
|
|
11
|
+
@transport = HeaderTransport.new(inner, auth_headers)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def embed
|
|
15
|
+
Embed.new(@transport)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def chat
|
|
19
|
+
Chat.new(@transport)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def histories
|
|
23
|
+
Histories.new(@transport)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.start_host_session(base_url:, host_api_key:, visitor_id:, host_mcp_url: nil, host_mcp_token: nil, **client_kwargs)
|
|
27
|
+
origin = base_url.to_s.strip.sub(%r{/\z}, "")
|
|
28
|
+
key = host_api_key.to_s.strip
|
|
29
|
+
if origin.empty? || key.empty?
|
|
30
|
+
raise ConfigurationError, "Задайте SVEDA_CLIENT_BASE_URL и SVEDA_CLIENT_HOST_API_KEY"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
client = new(base_url: origin, host_api_key: key, **client_kwargs)
|
|
34
|
+
token = client.embed.create_token(
|
|
35
|
+
visitor_id: visitor_id,
|
|
36
|
+
host_mcp_url: host_mcp_url,
|
|
37
|
+
host_mcp_token: host_mcp_token
|
|
38
|
+
)
|
|
39
|
+
if token.token.empty?
|
|
40
|
+
raise Error, "Sidecar returned an empty embed token."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
origin: origin,
|
|
45
|
+
token: token.token,
|
|
46
|
+
expires_in: token.expires_in,
|
|
47
|
+
appearance: token.appearance
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/sveda/embed.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class Embed
|
|
5
|
+
def initialize(transport)
|
|
6
|
+
@transport = transport
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create_token(visitor_id: nil, host_mcp_url: nil, host_mcp_token: nil)
|
|
10
|
+
payload = {}
|
|
11
|
+
payload["visitor_id"] = visitor_id if visitor_id.to_s != ""
|
|
12
|
+
if host_mcp_url.to_s != "" && host_mcp_token.to_s != ""
|
|
13
|
+
payload["host_mcp_url"] = host_mcp_url
|
|
14
|
+
payload["host_mcp_token"] = host_mcp_token
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
EmbedToken.from_payload(@transport.request_json("POST", "/sveda/embed/token", payload))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config
|
|
21
|
+
@transport.request_json("GET", "/sveda/embed/config")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class EmbedToken
|
|
5
|
+
attr_reader :token, :visitor_id, :expires_in, :appearance
|
|
6
|
+
|
|
7
|
+
def initialize(token:, visitor_id:, expires_in:, appearance: nil)
|
|
8
|
+
@token = token
|
|
9
|
+
@visitor_id = visitor_id
|
|
10
|
+
@expires_in = expires_in
|
|
11
|
+
@appearance = appearance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_payload(payload)
|
|
15
|
+
appearance = payload["appearance"]
|
|
16
|
+
appearance = nil unless appearance.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
new(
|
|
19
|
+
token: payload["token"].to_s,
|
|
20
|
+
visitor_id: payload["visitor_id"].to_s,
|
|
21
|
+
expires_in: [payload.fetch("expires_in", 3600).to_i, 60].max,
|
|
22
|
+
appearance: appearance
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/sveda/errors.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class AuthenticationError < Error; end
|
|
9
|
+
|
|
10
|
+
class UnserializableResponseError < Error; end
|
|
11
|
+
|
|
12
|
+
class TransportError < Error
|
|
13
|
+
attr_reader :status_code
|
|
14
|
+
|
|
15
|
+
def initialize(message, status_code: nil)
|
|
16
|
+
@status_code = status_code
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class APIError < Error
|
|
22
|
+
attr_reader :status_code, :response
|
|
23
|
+
|
|
24
|
+
def initialize(message, status_code: 0, response: nil)
|
|
25
|
+
@status_code = status_code
|
|
26
|
+
@response = response
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Sveda
|
|
6
|
+
class Histories
|
|
7
|
+
def initialize(transport)
|
|
8
|
+
@transport = transport
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list
|
|
12
|
+
@transport.request_json("GET", "/sveda/chat-histories")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(chat_id)
|
|
16
|
+
@transport.request_json("GET", history_path(chat_id))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rename(chat_id, title)
|
|
20
|
+
@transport.request_json("PATCH", history_path(chat_id), { "title" => title })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(chat_id)
|
|
24
|
+
@transport.request_json("DELETE", history_path(chat_id))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def history_path(chat_id)
|
|
30
|
+
"/sveda/chat-histories/#{URI.encode_uri_component(chat_id.to_s)}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/sveda/http.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Sveda
|
|
8
|
+
class Http
|
|
9
|
+
def initialize(base_url:, timeout: 30, open_timeout: 5)
|
|
10
|
+
@base_url = base_url.to_s.sub(%r{/\z}, "")
|
|
11
|
+
@timeout = timeout
|
|
12
|
+
@open_timeout = open_timeout
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def request_json(method, path, payload = {}, headers = {})
|
|
16
|
+
response = send_request(
|
|
17
|
+
method,
|
|
18
|
+
path,
|
|
19
|
+
payload,
|
|
20
|
+
headers.merge(
|
|
21
|
+
"Accept" => "application/json",
|
|
22
|
+
"Content-Type" => "application/json"
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
self.class.decode_json_body(response.code.to_i, response.body.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def request_stream(method, path, payload = {}, headers = {})
|
|
29
|
+
chunks = +""
|
|
30
|
+
send_request(
|
|
31
|
+
method,
|
|
32
|
+
path,
|
|
33
|
+
payload,
|
|
34
|
+
headers.merge(
|
|
35
|
+
"Accept" => "application/vnd.sveda.stream+json",
|
|
36
|
+
"Content-Type" => "application/json"
|
|
37
|
+
),
|
|
38
|
+
stream: true
|
|
39
|
+
) do |response|
|
|
40
|
+
self.class.decode_json_body(response.code.to_i, "") unless (200...300).cover?(response.code.to_i)
|
|
41
|
+
response.read_body do |chunk|
|
|
42
|
+
chunks << chunk
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
chunks
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.decode_json_body(status, body)
|
|
49
|
+
if [401, 403].include?(status)
|
|
50
|
+
raise AuthenticationError, "Sveda API authentication failed with status #{status}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless (200...300).cover?(status)
|
|
54
|
+
decoded = parse_object(body)
|
|
55
|
+
message = decoded.is_a?(Hash) && decoded["message"].is_a?(String) ? decoded["message"] : "Sveda API request failed with status #{status}"
|
|
56
|
+
raise APIError.new(message, status_code: status, response: decoded)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return {} if body.nil? || body.empty?
|
|
60
|
+
|
|
61
|
+
decoded = parse_object(body)
|
|
62
|
+
unless decoded.is_a?(Hash)
|
|
63
|
+
raise UnserializableResponseError, "Unable to decode Sveda API response as JSON."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
decoded
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.stringify_keys(object)
|
|
70
|
+
case object
|
|
71
|
+
when Hash
|
|
72
|
+
object.each_with_object({}) do |(key, value), memo|
|
|
73
|
+
memo[key.to_s] = stringify_keys(value)
|
|
74
|
+
end
|
|
75
|
+
when Array
|
|
76
|
+
object.map { |value| stringify_keys(value) }
|
|
77
|
+
else
|
|
78
|
+
object
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.parse_object(body)
|
|
83
|
+
return nil if body.nil? || body.empty?
|
|
84
|
+
|
|
85
|
+
JSON.parse(body)
|
|
86
|
+
rescue JSON::ParserError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
REQUESTS = {
|
|
93
|
+
"GET" => Net::HTTP::Get,
|
|
94
|
+
"POST" => Net::HTTP::Post,
|
|
95
|
+
"PATCH" => Net::HTTP::Patch,
|
|
96
|
+
"PUT" => Net::HTTP::Put,
|
|
97
|
+
"DELETE" => Net::HTTP::Delete
|
|
98
|
+
}.freeze
|
|
99
|
+
|
|
100
|
+
def send_request(method, path, payload, headers, stream: false, &block)
|
|
101
|
+
uri = resolve_uri(path)
|
|
102
|
+
request_class = REQUESTS.fetch(method.to_s.upcase)
|
|
103
|
+
request = request_class.new(uri)
|
|
104
|
+
headers.each { |name, value| request[name] = value }
|
|
105
|
+
body = self.class.stringify_keys(payload || {})
|
|
106
|
+
request.body = JSON.generate(body) if request.request_body_permitted?
|
|
107
|
+
|
|
108
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
109
|
+
http.use_ssl = uri.scheme == "https"
|
|
110
|
+
http.read_timeout = @timeout
|
|
111
|
+
http.open_timeout = @open_timeout
|
|
112
|
+
|
|
113
|
+
if stream
|
|
114
|
+
http.request(request, &block)
|
|
115
|
+
else
|
|
116
|
+
http.request(request)
|
|
117
|
+
end
|
|
118
|
+
rescue AuthenticationError, APIError, UnserializableResponseError
|
|
119
|
+
raise
|
|
120
|
+
rescue StandardError => error
|
|
121
|
+
raise TransportError, error.message
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def resolve_uri(path)
|
|
125
|
+
return URI.parse(path) if path.start_with?("http://", "https://")
|
|
126
|
+
|
|
127
|
+
URI.parse("#{@base_url}/#{path.sub(%r{\A/}, "")}")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class HeaderTransport
|
|
132
|
+
def initialize(inner, headers)
|
|
133
|
+
@inner = inner
|
|
134
|
+
@headers = headers
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def request_json(method, path, payload = {}, headers = {})
|
|
138
|
+
@inner.request_json(method, path, payload, @headers.merge(headers))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def request_stream(method, path, payload = {}, headers = {})
|
|
142
|
+
@inner.request_stream(method, path, payload, @headers.merge(headers))
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class Message
|
|
5
|
+
attr_reader :payload
|
|
6
|
+
|
|
7
|
+
def initialize(payload)
|
|
8
|
+
@payload = payload
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def explanation
|
|
12
|
+
payload["explanation"].to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tokens_used
|
|
16
|
+
payload["tokens_used"].to_i
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def chat_id
|
|
20
|
+
payload["chat_id"].to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_payload(payload)
|
|
24
|
+
new(payload)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sveda
|
|
4
|
+
class StreamEvent
|
|
5
|
+
attr_reader :type, :payload
|
|
6
|
+
|
|
7
|
+
def initialize(type, payload)
|
|
8
|
+
@type = type
|
|
9
|
+
@payload = payload
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
payload[key] || payload[key.to_s] || payload[key.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
payload
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_missing(name, *args, &block)
|
|
21
|
+
key = name.to_s
|
|
22
|
+
return payload[key] if payload.key?(key)
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def respond_to_missing?(name, include_private = false)
|
|
28
|
+
payload.key?(name.to_s) || super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Sveda
|
|
6
|
+
class StreamParser
|
|
7
|
+
SSE_DONE_LINE = "data: [DONE]"
|
|
8
|
+
|
|
9
|
+
STREAM_EVENTS = [
|
|
10
|
+
"message.start",
|
|
11
|
+
"text.delta",
|
|
12
|
+
"reasoning.delta",
|
|
13
|
+
"tool.call",
|
|
14
|
+
"tool.result",
|
|
15
|
+
"tool.progress",
|
|
16
|
+
"context.usage",
|
|
17
|
+
"chat.title",
|
|
18
|
+
"max_steps",
|
|
19
|
+
"message.end",
|
|
20
|
+
"error"
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def each(body)
|
|
24
|
+
return enum_for(:each, body) unless block_given?
|
|
25
|
+
|
|
26
|
+
if body.is_a?(String)
|
|
27
|
+
body.split(/\r\n|\n|\r/).each do |line|
|
|
28
|
+
event = parse_line(line)
|
|
29
|
+
yield event if event
|
|
30
|
+
end
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
buffer = +""
|
|
35
|
+
body.each do |chunk|
|
|
36
|
+
buffer << chunk.to_s
|
|
37
|
+
while (newline_index = buffer.index("\n"))
|
|
38
|
+
line = buffer.slice!(0, newline_index + 1)
|
|
39
|
+
event = parse_line(line)
|
|
40
|
+
yield event if event
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
tail = buffer.strip
|
|
45
|
+
return if tail.empty?
|
|
46
|
+
|
|
47
|
+
event = parse_line(tail)
|
|
48
|
+
yield event if event
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_line(line)
|
|
52
|
+
trimmed = line.to_s.strip
|
|
53
|
+
return unless trimmed.start_with?("data:")
|
|
54
|
+
|
|
55
|
+
payload = trimmed[5..].to_s.strip
|
|
56
|
+
return if payload.empty? || payload == "[DONE]"
|
|
57
|
+
|
|
58
|
+
decoded = JSON.parse(payload)
|
|
59
|
+
return unless decoded.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
type = decoded["type"]
|
|
62
|
+
return unless type.is_a?(String) && STREAM_EVENTS.include?(type)
|
|
63
|
+
|
|
64
|
+
StreamEvent.new(type, decoded)
|
|
65
|
+
rescue JSON::ParserError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/sveda-ai.rb
ADDED
data/lib/sveda.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sveda/version"
|
|
4
|
+
require_relative "sveda/errors"
|
|
5
|
+
require_relative "sveda/stream_event"
|
|
6
|
+
require_relative "sveda/stream_parser"
|
|
7
|
+
require_relative "sveda/embed_token"
|
|
8
|
+
require_relative "sveda/message"
|
|
9
|
+
require_relative "sveda/http"
|
|
10
|
+
require_relative "sveda/embed"
|
|
11
|
+
require_relative "sveda/chat"
|
|
12
|
+
require_relative "sveda/histories"
|
|
13
|
+
require_relative "sveda/client"
|
|
14
|
+
|
|
15
|
+
module Sveda
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sveda-ruby-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Neresson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby SDK for the Sveda AI sidecar HTTP API (embed tokens, streaming chat,
|
|
14
|
+
histories).
|
|
15
|
+
email:
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/sveda-ai.rb
|
|
23
|
+
- lib/sveda-ruby-sdk.rb
|
|
24
|
+
- lib/sveda.rb
|
|
25
|
+
- lib/sveda/chat.rb
|
|
26
|
+
- lib/sveda/client.rb
|
|
27
|
+
- lib/sveda/embed.rb
|
|
28
|
+
- lib/sveda/embed_token.rb
|
|
29
|
+
- lib/sveda/errors.rb
|
|
30
|
+
- lib/sveda/histories.rb
|
|
31
|
+
- lib/sveda/http.rb
|
|
32
|
+
- lib/sveda/message.rb
|
|
33
|
+
- lib/sveda/stream_event.rb
|
|
34
|
+
- lib/sveda/stream_parser.rb
|
|
35
|
+
- lib/sveda/version.rb
|
|
36
|
+
homepage: https://github.com/neresson/sveda-ruby-sdk
|
|
37
|
+
licenses:
|
|
38
|
+
- MIT
|
|
39
|
+
metadata:
|
|
40
|
+
homepage_uri: https://github.com/neresson/sveda-ruby-sdk
|
|
41
|
+
source_code_uri: https://github.com/neresson/sveda-ruby-sdk
|
|
42
|
+
changelog_uri: https://github.com/neresson/sveda-ruby-sdk/blob/main/README.md
|
|
43
|
+
rubygems_mfa_required: 'true'
|
|
44
|
+
post_install_message:
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 3.2.0
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.5.22
|
|
60
|
+
signing_key:
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Ruby SDK for the Sveda AI sidecar HTTP API
|
|
63
|
+
test_files: []
|