znyx-sdk 1.2.1
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/lib/znyx_sdk/client.rb +113 -0
- data/lib/znyx_sdk/models.rb +65 -0
- data/lib/znyx_sdk/telemetry.rb +156 -0
- data/lib/znyx_sdk.rb +7 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 816193b73770d4949028adf8359e4e402aa8cdd2fc9a2f39235ea9bc47acc946
|
|
4
|
+
data.tar.gz: 4cff940a92b4b7905422405a09a70e6f08864d08f318adb8e20c58e2ce5e3a8f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 71e56c056f15328e7f7cb1a83177906755f6fbb8e5279700b593673f09a49b04ab7754a611fc5f660e47bde51975491f35155377270f51da190942f3286ea3ad
|
|
7
|
+
data.tar.gz: fb7938b04ed59c4b2b3d51c6245b25f1a0ca58b0b5329fff2096a96b732d2f31c76f48dee14cfa3f72958791114e7af6b5bbb709b30b519e3c40474429324d53
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module ZnyxSdk
|
|
7
|
+
# Official Ruby SDK for the ZNYX Runtime guardrails API.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# client = ZnyxSdk::Client.new("http://localhost:8080")
|
|
11
|
+
#
|
|
12
|
+
# result = client.evaluate_input(
|
|
13
|
+
# tenant_id: "my-org",
|
|
14
|
+
# app_id: "my-app",
|
|
15
|
+
# text: user_message
|
|
16
|
+
# )
|
|
17
|
+
# raise result.user_message if result.blocked?
|
|
18
|
+
#
|
|
19
|
+
class Client
|
|
20
|
+
# @param base_url [String] URL of the running ZNYX Runtime (e.g. "http://localhost:8080")
|
|
21
|
+
# @param api_key [String, nil] Optional API key sent as Authorization: Bearer
|
|
22
|
+
# @param timeout [Integer] HTTP read/open timeout in seconds (default: 10)
|
|
23
|
+
def initialize(base_url, api_key: nil, timeout: 10)
|
|
24
|
+
@uri = URI.parse(base_url.chomp("/"))
|
|
25
|
+
@api_key = api_key
|
|
26
|
+
@timeout = timeout
|
|
27
|
+
|
|
28
|
+
# Anonymous, opt-out install telemetry (ZNYX_TELEMETRY=false to disable).
|
|
29
|
+
begin
|
|
30
|
+
Telemetry.maybe_send_install_ping
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# telemetry must never break client construction
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Evaluates a user prompt before sending it to the LLM.
|
|
37
|
+
#
|
|
38
|
+
# @param text [String] The text to evaluate
|
|
39
|
+
# @param tenant_id [String]
|
|
40
|
+
# @param app_id [String]
|
|
41
|
+
# @param kwargs [Hash] Optional: agent_id, env, trace_id, session_id, metadata
|
|
42
|
+
# @return [EvaluationResponse]
|
|
43
|
+
def evaluate_input(text:, tenant_id: "default", app_id: "default", **kwargs)
|
|
44
|
+
post("/v1/evaluate/input", build_payload(text: text, tenant_id: tenant_id, app_id: app_id, **kwargs))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Evaluates an LLM response before returning it to the user.
|
|
48
|
+
def evaluate_output(text:, tenant_id: "default", app_id: "default", **kwargs)
|
|
49
|
+
post("/v1/evaluate/output", build_payload(text: text, tenant_id: tenant_id, app_id: app_id, **kwargs))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Evaluates a tool call before executing it.
|
|
53
|
+
#
|
|
54
|
+
# @param tool_name [String]
|
|
55
|
+
# @param tool_args [Hash]
|
|
56
|
+
def evaluate_tool(tool_name:, tool_args:, tenant_id: "default", app_id: "default", **kwargs)
|
|
57
|
+
payload = {
|
|
58
|
+
request_id: new_request_id,
|
|
59
|
+
tenant_id: tenant_id,
|
|
60
|
+
app_id: app_id,
|
|
61
|
+
agent_id: kwargs.delete(:agent_id) || "default",
|
|
62
|
+
env: kwargs.delete(:env) || "prod",
|
|
63
|
+
tool_name: tool_name,
|
|
64
|
+
tool_args: tool_args,
|
|
65
|
+
}.merge(kwargs.compact)
|
|
66
|
+
post("/v1/evaluate/tool", payload)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_payload(text:, tenant_id:, app_id:, **kwargs)
|
|
72
|
+
{
|
|
73
|
+
request_id: new_request_id,
|
|
74
|
+
tenant_id: tenant_id,
|
|
75
|
+
app_id: app_id,
|
|
76
|
+
agent_id: kwargs.delete(:agent_id) || "default",
|
|
77
|
+
env: kwargs.delete(:env) || "prod",
|
|
78
|
+
text: text,
|
|
79
|
+
}.merge(kwargs.compact)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def post(path, payload)
|
|
83
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
84
|
+
http.use_ssl = @uri.scheme == "https"
|
|
85
|
+
http.read_timeout = @timeout
|
|
86
|
+
http.open_timeout = @timeout
|
|
87
|
+
|
|
88
|
+
request = Net::HTTP::Post.new(path)
|
|
89
|
+
request["Content-Type"] = "application/json"
|
|
90
|
+
request["Authorization"] = "Bearer #{@api_key}" if @api_key
|
|
91
|
+
request.body = JSON.generate(payload)
|
|
92
|
+
|
|
93
|
+
response = http.request(request)
|
|
94
|
+
|
|
95
|
+
if response.code.to_i >= 400
|
|
96
|
+
raise ZnyxError.new(
|
|
97
|
+
"ZNYX Runtime returned #{response.code}: #{response.body}",
|
|
98
|
+
status_code: response.code.to_i
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
EvaluationResponse.new(JSON.parse(response.body))
|
|
103
|
+
rescue ZnyxError
|
|
104
|
+
raise
|
|
105
|
+
rescue => e
|
|
106
|
+
raise ZnyxError, "HTTP request failed: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def new_request_id
|
|
110
|
+
"req_#{SecureRandom.hex(4)}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module ZnyxSdk
|
|
2
|
+
DECISIONS = %w[ALLOW BLOCK REDACT WARN TRANSFORM].freeze
|
|
3
|
+
|
|
4
|
+
# Immutable value object returned by all evaluate methods.
|
|
5
|
+
class EvaluationResponse
|
|
6
|
+
attr_reader :request_id, :decision, :risk_score, :policy_version,
|
|
7
|
+
:rule_hits, :sanitized_text, :user_message,
|
|
8
|
+
:developer_message, :latency_ms, :trace_id, :session_id,
|
|
9
|
+
:detector_results
|
|
10
|
+
|
|
11
|
+
def initialize(data)
|
|
12
|
+
@request_id = data["request_id"]
|
|
13
|
+
@decision = data["decision"]
|
|
14
|
+
@risk_score = data["risk_score"].to_i
|
|
15
|
+
@policy_version = data["policy_version"]
|
|
16
|
+
@rule_hits = Array(data["rule_hits"]).map { |h| RuleHit.new(h) }
|
|
17
|
+
@sanitized_text = data["sanitized_text"]
|
|
18
|
+
@user_message = data["user_message"]
|
|
19
|
+
@developer_message = data["developer_message"]
|
|
20
|
+
@latency_ms = data["latency_ms"].to_i
|
|
21
|
+
@trace_id = data["trace_id"]
|
|
22
|
+
@session_id = data["session_id"]
|
|
23
|
+
@detector_results = Array(data["detector_results"]).map { |d| DetectorResult.new(d) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def blocked? = @decision == "BLOCK"
|
|
27
|
+
def redacted? = @decision == "REDACT"
|
|
28
|
+
|
|
29
|
+
# Returns sanitized_text if present, otherwise the original string.
|
|
30
|
+
def safe_text(original)
|
|
31
|
+
sanitized_text.nil? || sanitized_text.empty? ? original : sanitized_text
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class RuleHit
|
|
36
|
+
attr_reader :rule_id, :severity, :message
|
|
37
|
+
|
|
38
|
+
def initialize(data)
|
|
39
|
+
@rule_id = data["rule_id"]
|
|
40
|
+
@severity = data["severity"]
|
|
41
|
+
@message = data["message"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class DetectorResult
|
|
46
|
+
attr_reader :detector_name, :decision, :risk_score, :latency_ms, :rule_hits
|
|
47
|
+
|
|
48
|
+
def initialize(data)
|
|
49
|
+
@detector_name = data["detector_name"]
|
|
50
|
+
@decision = data["decision"]
|
|
51
|
+
@risk_score = data["risk_score"].to_i
|
|
52
|
+
@latency_ms = data["latency_ms"].to_i
|
|
53
|
+
@rule_hits = Array(data["rule_hits"]).map { |h| RuleHit.new(h) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ZnyxError < StandardError
|
|
58
|
+
attr_reader :status_code
|
|
59
|
+
|
|
60
|
+
def initialize(message, status_code: nil)
|
|
61
|
+
super(message)
|
|
62
|
+
@status_code = status_code
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "time"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
|
|
11
|
+
module ZnyxSdk
|
|
12
|
+
# Anonymous install telemetry for the ZNYX Ruby SDK.
|
|
13
|
+
#
|
|
14
|
+
# Sends a single fire-and-forget ping when a client is first constructed, then
|
|
15
|
+
# at most one "heartbeat" ping per 24h. Non-sensitive metadata only:
|
|
16
|
+
# install_id (random UUID, persisted to ~/.znyx/sdk-state.json), SDK version,
|
|
17
|
+
# source ("ruby-sdk"), OS / arch / Ruby version, run_count.
|
|
18
|
+
#
|
|
19
|
+
# No PII, no request content, no tenant data. Opt out with
|
|
20
|
+
# ZNYX_TELEMETRY=false (on by default). Disclosed once to stderr on the first
|
|
21
|
+
# run. Every operation is best-effort — telemetry must never slow down, block,
|
|
22
|
+
# or break the calling application. See TELEMETRY.md.
|
|
23
|
+
module Telemetry
|
|
24
|
+
# Telemetry endpoint. Defaults to the ZNYX production receiver so anonymous
|
|
25
|
+
# install telemetry is on out of the box (opt-out, fully transparent).
|
|
26
|
+
# Override with ZNYX_TELEMETRY_URL (or ZNYX_HEARTBEAT_URL), or opt out with
|
|
27
|
+
# ZNYX_TELEMETRY=false.
|
|
28
|
+
ENDPOINT = (
|
|
29
|
+
(ENV["ZNYX_TELEMETRY_URL"] || "").empty? ? nil : ENV["ZNYX_TELEMETRY_URL"]
|
|
30
|
+
) || (
|
|
31
|
+
(ENV["ZNYX_HEARTBEAT_URL"] || "").empty? ? nil : ENV["ZNYX_HEARTBEAT_URL"]
|
|
32
|
+
) || "https://cp.znyx.ai/v1/install-telemetry"
|
|
33
|
+
|
|
34
|
+
STATE_FILE = File.join(Dir.home, ".znyx", "sdk-state.json")
|
|
35
|
+
HEARTBEAT_INTERVAL = 86_400 # seconds (24h)
|
|
36
|
+
SOURCE = "ruby-sdk"
|
|
37
|
+
|
|
38
|
+
DISCLOSURE =
|
|
39
|
+
"[znyx-sdk] Anonymous usage telemetry is on (install id, SDK version, OS - " \
|
|
40
|
+
"no PII, no request content). Opt out with ZNYX_TELEMETRY=false."
|
|
41
|
+
|
|
42
|
+
@attempted = false
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# Send an anonymous install ping, throttled to once per 24h. Fire-and-forget.
|
|
46
|
+
def maybe_send_install_ping
|
|
47
|
+
return if @attempted
|
|
48
|
+
|
|
49
|
+
@attempted = true
|
|
50
|
+
|
|
51
|
+
return if ENDPOINT.to_s.empty? || !enabled?
|
|
52
|
+
|
|
53
|
+
state = load_state
|
|
54
|
+
last_ping = state["last_ping_at"]
|
|
55
|
+
|
|
56
|
+
if last_ping.nil?
|
|
57
|
+
event_type = "first_run"
|
|
58
|
+
else
|
|
59
|
+
begin
|
|
60
|
+
elapsed = Time.now - Time.iso8601(last_ping)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
elapsed = HEARTBEAT_INTERVAL # unparseable -> treat as due
|
|
63
|
+
end
|
|
64
|
+
return if elapsed < HEARTBEAT_INTERVAL
|
|
65
|
+
|
|
66
|
+
event_type = "heartbeat"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
install_id = state["install_id"] || SecureRandom.uuid
|
|
70
|
+
run_count = (state["run_count"] || 0).to_i + 1
|
|
71
|
+
now_iso = Time.now.utc.iso8601
|
|
72
|
+
|
|
73
|
+
state.merge!(
|
|
74
|
+
"install_id" => install_id,
|
|
75
|
+
"first_seen_at" => state["first_seen_at"] || now_iso,
|
|
76
|
+
"last_ping_at" => now_iso,
|
|
77
|
+
"run_count" => run_count
|
|
78
|
+
)
|
|
79
|
+
save_state(state)
|
|
80
|
+
|
|
81
|
+
if event_type == "first_run"
|
|
82
|
+
begin
|
|
83
|
+
warn DISCLOSURE
|
|
84
|
+
rescue StandardError
|
|
85
|
+
# ignore
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
payload = {
|
|
90
|
+
install_id: install_id,
|
|
91
|
+
version: version,
|
|
92
|
+
event_type: event_type,
|
|
93
|
+
source: SOURCE,
|
|
94
|
+
os: RbConfig::CONFIG["host_os"],
|
|
95
|
+
os_version: host_os_version,
|
|
96
|
+
arch: RbConfig::CONFIG["host_cpu"],
|
|
97
|
+
ruby_version: RUBY_VERSION,
|
|
98
|
+
run_count: run_count,
|
|
99
|
+
timestamp: now_iso
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Thread.new { post(payload) }
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# Telemetry must never break client construction.
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def enabled?
|
|
110
|
+
val = (ENV["ZNYX_TELEMETRY"] || ENV["GUARDRAILS_TELEMETRY"] || "true").downcase
|
|
111
|
+
!%w[false 0 no].include?(val)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def version
|
|
115
|
+
defined?(ZnyxSdk::VERSION) ? ZnyxSdk::VERSION : "unknown"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def host_os_version
|
|
119
|
+
`uname -r`.strip
|
|
120
|
+
rescue StandardError
|
|
121
|
+
"unknown"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def load_state
|
|
125
|
+
return {} unless File.exist?(STATE_FILE)
|
|
126
|
+
|
|
127
|
+
JSON.parse(File.read(STATE_FILE))
|
|
128
|
+
rescue StandardError
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def save_state(state)
|
|
133
|
+
FileUtils.mkdir_p(File.dirname(STATE_FILE))
|
|
134
|
+
File.write(STATE_FILE, JSON.pretty_generate(state))
|
|
135
|
+
rescue StandardError
|
|
136
|
+
# best-effort; never raise
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def post(payload)
|
|
140
|
+
uri = URI.parse(ENDPOINT)
|
|
141
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
142
|
+
http.use_ssl = uri.scheme == "https"
|
|
143
|
+
http.open_timeout = 3
|
|
144
|
+
http.read_timeout = 3
|
|
145
|
+
|
|
146
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
147
|
+
request["Content-Type"] = "application/json"
|
|
148
|
+
request.body = JSON.generate(payload)
|
|
149
|
+
|
|
150
|
+
http.request(request)
|
|
151
|
+
rescue StandardError
|
|
152
|
+
# never fail the app because of telemetry
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/znyx_sdk.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: znyx-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zitrino
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
- community@zitrino.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/znyx_sdk.rb
|
|
21
|
+
- lib/znyx_sdk/client.rb
|
|
22
|
+
- lib/znyx_sdk/models.rb
|
|
23
|
+
- lib/znyx_sdk/telemetry.rb
|
|
24
|
+
homepage: https://znyx.ai
|
|
25
|
+
licenses:
|
|
26
|
+
- Apache-2.0
|
|
27
|
+
metadata:
|
|
28
|
+
source_code_uri: https://github.com/zitrino-oss/znyx-sdk
|
|
29
|
+
bug_tracker_uri: https://github.com/zitrino-oss/znyx-sdk/issues
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.0'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.4.19
|
|
46
|
+
signing_key:
|
|
47
|
+
specification_version: 4
|
|
48
|
+
summary: Official Ruby SDK for the ZNYX Runtime guardrails API
|
|
49
|
+
test_files: []
|