coolhand 0.1.2
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/.idea/coolhand-ruby.iml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +167 -0
- data/.simplecov +7 -0
- data/CHANGELOG.md +45 -0
- data/LICENSE +201 -0
- data/README.md +327 -0
- data/Rakefile +12 -0
- data/coolhand-ruby.gemspec +42 -0
- data/lib/coolhand/ruby/api_service.rb +136 -0
- data/lib/coolhand/ruby/configuration.rb +38 -0
- data/lib/coolhand/ruby/feedback_service.rb +15 -0
- data/lib/coolhand/ruby/interceptor.rb +118 -0
- data/lib/coolhand/ruby/logger_service.rb +17 -0
- data/lib/coolhand/ruby/version.rb +7 -0
- data/lib/coolhand/ruby.rb +81 -0
- data/sig/coolhand/ruby.rbs +6 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 193fc72a2e4d3c485584250f6f846ba97dbd02f55c357e8bf6f3f95641b669d5
|
|
4
|
+
data.tar.gz: 4523f4af59e1a81a7763edd5424e86bec7bd35eb40799af7ad253446d9c86bbe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6d65bfde6461c4f9676a2e0c64de863204e075abae9b762666119bad7676d21ff962e9aeafb3d0420e86ee9d00e0dcda9e266f99dbfc751b289e5e3c91e028cf
|
|
7
|
+
data.tar.gz: 48aa86e01741e1a93faec921e65e13a3d434740f91c0c564d22406279d15eab56f60f1aea1bebb5d9c5eb1095200c459b8e728cbd41cc118ee359edfb9eccec1
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# This is the configuration used to check the rubocop source code.
|
|
2
|
+
|
|
3
|
+
require:
|
|
4
|
+
- rubocop-performance
|
|
5
|
+
- rubocop-rspec
|
|
6
|
+
inherit_gem:
|
|
7
|
+
test-prof: config/rubocop-rspec.yml
|
|
8
|
+
|
|
9
|
+
AllCops:
|
|
10
|
+
NewCops: enable
|
|
11
|
+
Exclude:
|
|
12
|
+
- 'vendor/**/*'
|
|
13
|
+
- 'spec/fixtures/**/*'
|
|
14
|
+
- 'tmp/**/*'
|
|
15
|
+
- '.git/**/*'
|
|
16
|
+
- 'bin/*'
|
|
17
|
+
TargetRubyVersion: 3.0
|
|
18
|
+
SuggestExtensions: false
|
|
19
|
+
ActiveSupportExtensionsEnabled: true
|
|
20
|
+
DocumentationBaseURL: https://docs.rubocop.org/rubocop
|
|
21
|
+
|
|
22
|
+
# Metrics rules
|
|
23
|
+
Metrics/ModuleLength:
|
|
24
|
+
CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
|
|
25
|
+
Max: 200
|
|
26
|
+
|
|
27
|
+
Metrics/ClassLength:
|
|
28
|
+
CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
|
|
29
|
+
Max: 200
|
|
30
|
+
|
|
31
|
+
Metrics/BlockLength:
|
|
32
|
+
Max: 50
|
|
33
|
+
AllowedMethods:
|
|
34
|
+
- included
|
|
35
|
+
Exclude:
|
|
36
|
+
- '**/*.gemspec'
|
|
37
|
+
- 'spec/**/*'
|
|
38
|
+
|
|
39
|
+
Metrics/AbcSize:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
Metrics/CyclomaticComplexity:
|
|
43
|
+
Enabled: false
|
|
44
|
+
|
|
45
|
+
Metrics/PerceivedComplexity:
|
|
46
|
+
Enabled: false
|
|
47
|
+
|
|
48
|
+
Metrics/ParameterLists:
|
|
49
|
+
CountKeywordArgs: false
|
|
50
|
+
|
|
51
|
+
Metrics/CollectionLiteralLength:
|
|
52
|
+
LengthThreshold: 500
|
|
53
|
+
|
|
54
|
+
Metrics/MethodLength:
|
|
55
|
+
CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
|
|
56
|
+
Max: 50
|
|
57
|
+
|
|
58
|
+
# Bundle rules
|
|
59
|
+
Bundler/OrderedGems:
|
|
60
|
+
Enabled: false
|
|
61
|
+
|
|
62
|
+
# Style rules
|
|
63
|
+
Style/WordArray:
|
|
64
|
+
MinSize: 5
|
|
65
|
+
|
|
66
|
+
Style/StringLiterals:
|
|
67
|
+
EnforcedStyle: double_quotes
|
|
68
|
+
|
|
69
|
+
Style/GuardClause:
|
|
70
|
+
Enabled: false
|
|
71
|
+
|
|
72
|
+
Style/SoleNestedConditional:
|
|
73
|
+
Enabled: false
|
|
74
|
+
|
|
75
|
+
Style/TrailingCommaInArrayLiteral:
|
|
76
|
+
Enabled: false
|
|
77
|
+
|
|
78
|
+
Style/TrailingCommaInHashLiteral:
|
|
79
|
+
Enabled: false
|
|
80
|
+
|
|
81
|
+
Style/Documentation:
|
|
82
|
+
Enabled: false
|
|
83
|
+
|
|
84
|
+
Style/ClassAndModuleChildren:
|
|
85
|
+
Enabled: false
|
|
86
|
+
|
|
87
|
+
Style/Alias:
|
|
88
|
+
EnforcedStyle: prefer_alias_method
|
|
89
|
+
|
|
90
|
+
Style/DoubleNegation:
|
|
91
|
+
Enabled: false
|
|
92
|
+
|
|
93
|
+
Style/InverseMethods:
|
|
94
|
+
Enabled: false
|
|
95
|
+
|
|
96
|
+
# Layout rules
|
|
97
|
+
Layout/LineLength:
|
|
98
|
+
Max: 120
|
|
99
|
+
|
|
100
|
+
Layout/ArgumentAlignment:
|
|
101
|
+
EnforcedStyle: with_fixed_indentation
|
|
102
|
+
|
|
103
|
+
Layout/ParameterAlignment:
|
|
104
|
+
EnforcedStyle: with_fixed_indentation
|
|
105
|
+
|
|
106
|
+
Layout/HashAlignment:
|
|
107
|
+
EnforcedLastArgumentHashStyle: ignore_implicit
|
|
108
|
+
|
|
109
|
+
Layout/FirstArrayElementIndentation:
|
|
110
|
+
EnforcedStyle: consistent
|
|
111
|
+
|
|
112
|
+
Layout/FirstHashElementIndentation:
|
|
113
|
+
EnforcedStyle: consistent
|
|
114
|
+
|
|
115
|
+
Layout/MultilineMethodCallIndentation:
|
|
116
|
+
EnforcedStyle: indented
|
|
117
|
+
|
|
118
|
+
Layout/MultilineAssignmentLayout:
|
|
119
|
+
EnforcedStyle: same_line
|
|
120
|
+
|
|
121
|
+
Layout/EndAlignment:
|
|
122
|
+
EnforcedStyleAlignWith: variable
|
|
123
|
+
|
|
124
|
+
Naming/VariableNumber:
|
|
125
|
+
Enabled: false
|
|
126
|
+
|
|
127
|
+
# Lint rules
|
|
128
|
+
Lint/EmptyClass:
|
|
129
|
+
Enabled: false
|
|
130
|
+
|
|
131
|
+
Lint/MissingSuper:
|
|
132
|
+
Enabled: false
|
|
133
|
+
|
|
134
|
+
# RSpec rules
|
|
135
|
+
RSpec/HookArgument:
|
|
136
|
+
Enabled: false
|
|
137
|
+
|
|
138
|
+
RSpec/MultipleExpectations:
|
|
139
|
+
Enabled: false
|
|
140
|
+
|
|
141
|
+
RSpec/ExampleLength:
|
|
142
|
+
Enabled: false
|
|
143
|
+
|
|
144
|
+
RSpec/LetSetup:
|
|
145
|
+
Enabled: false
|
|
146
|
+
|
|
147
|
+
RSpec/InstanceVariable:
|
|
148
|
+
Enabled: false
|
|
149
|
+
|
|
150
|
+
RSpec/MultipleMemoizedHelpers:
|
|
151
|
+
Enabled: false
|
|
152
|
+
|
|
153
|
+
RSpec/NestedGroups:
|
|
154
|
+
Max: 6
|
|
155
|
+
|
|
156
|
+
RSpec/MessageSpies:
|
|
157
|
+
Enabled: false
|
|
158
|
+
|
|
159
|
+
RSpec/StubbedMock:
|
|
160
|
+
Enabled: false
|
|
161
|
+
|
|
162
|
+
RSpec/DescribedClass:
|
|
163
|
+
Enabled: false
|
|
164
|
+
|
|
165
|
+
# Performance rules
|
|
166
|
+
Performance/MethodObjectAsBlock:
|
|
167
|
+
Enabled: true
|
data/.simplecov
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.2] - 2024-10-22
|
|
9
|
+
|
|
10
|
+
### 🔧 Configuration Improvements
|
|
11
|
+
- **Removed environment variable dependency** - Configuration now only via Ruby config block
|
|
12
|
+
- **Added smart defaults** - Automatically monitors OpenAI and Anthropic APIs by default
|
|
13
|
+
|
|
14
|
+
### 📚 Documentation
|
|
15
|
+
- **Improved examples** - Added Rails credentials best practices
|
|
16
|
+
- **Clearer configuration** - Removed confusing ENV references
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug Fixes
|
|
19
|
+
- **Fixed test isolation** - Added configuration reset between tests
|
|
20
|
+
- **Fixed intercept_addresses format** - Corrected to use array instead of string
|
|
21
|
+
|
|
22
|
+
## [0.1.1] - 2024-10-21
|
|
23
|
+
|
|
24
|
+
### ✨ New Features
|
|
25
|
+
- **Feedback API Support** - Users can now submit feedback (likes/dislikes, explanations, revised outputs) for LLM responses
|
|
26
|
+
- **Public create_feedback method** - Exposed in FeedbackService for direct feedback submission
|
|
27
|
+
|
|
28
|
+
### 🏗️ DRYer Architecture
|
|
29
|
+
- **Introduced ApiService base class** - Extracted common API functionality into a shared parent class, reducing code duplication
|
|
30
|
+
- **Renamed Logger to LoggerService** - Better naming consistency and inheritance from ApiService
|
|
31
|
+
- **Added FeedbackService** - New service for submitting LLM request feedback through the API
|
|
32
|
+
|
|
33
|
+
### 🔧 Development Dependencies
|
|
34
|
+
- **Added webmock gem** - Required for HTTP stubbing in tests
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Updated gem name from "coolhand-ruby" to "coolhand"
|
|
38
|
+
|
|
39
|
+
## [0.1.0] - 2024-10-21
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- Initial release of coolhand gem
|
|
43
|
+
- Automatic interception and logging of LLM API calls
|
|
44
|
+
- Net::HTTP patching to capture request and response data
|
|
45
|
+
- Support for Ruby 3.0 and higher
|
data/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
data/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Coolhand Ruby Monitor
|
|
2
|
+
|
|
3
|
+
Monitor and log LLM API calls from multiple providers (OpenAI, Anthropic, Google AI, Cohere, and more) to the Coolhand analytics platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'coolhand'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
1. **Get API Key**: Visit [coolhand.io](https://coolhand.io/) to create a free account
|
|
14
|
+
2. **Install**: `gem install coolhand`
|
|
15
|
+
3. **Initialize**: Add configuration to your Ruby application
|
|
16
|
+
4. **Configure**: Set your API key in the configuration block
|
|
17
|
+
5. **Deploy**: Your AI calls are now automatically monitored!
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Automatic Global Monitoring
|
|
22
|
+
|
|
23
|
+
🔥 **Set it and forget it! Monitor ALL AI API calls across your entire application with minimal configuration.**
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# Add this configuration at the start of your application
|
|
27
|
+
require 'coolhand'
|
|
28
|
+
|
|
29
|
+
Coolhand.configure do |config|
|
|
30
|
+
config.api_key = 'your_api_key_here'
|
|
31
|
+
config.silent = true # Set to false for debug output
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# That's it! ALL AI API calls are now automatically monitored:
|
|
35
|
+
# ✅ OpenAI SDK calls
|
|
36
|
+
# ✅ Anthropic API calls
|
|
37
|
+
# ✅ Direct HTTP requests to AI APIs
|
|
38
|
+
# ✅ ANY library making AI API calls via Faraday
|
|
39
|
+
|
|
40
|
+
# NO code changes needed in your existing services!
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**✨ Why Automatic Monitoring:**
|
|
44
|
+
- 🚫 **Zero refactoring** - No code changes to existing services
|
|
45
|
+
- 📊 **Complete coverage** - Monitors ALL AI libraries using Faraday automatically
|
|
46
|
+
- 🔒 **Security built-in** - Automatic credential sanitization
|
|
47
|
+
- ⚡ **Performance optimized** - Negligible overhead via async logging
|
|
48
|
+
- 🛡️ **Future-proof** - Automatically captures new AI calls added by your team
|
|
49
|
+
|
|
50
|
+
## Feedback API
|
|
51
|
+
|
|
52
|
+
Collect feedback on LLM responses to improve model performance:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
require 'coolhand'
|
|
56
|
+
|
|
57
|
+
# Create feedback for an LLM response
|
|
58
|
+
feedback_service = Coolhand::Ruby::FeedbackService.new(Coolhand.configuration)
|
|
59
|
+
|
|
60
|
+
feedback = feedback_service.create_feedback(
|
|
61
|
+
llm_request_log_id: 123,
|
|
62
|
+
llm_provider_unique_id: 'req_xxxxxxx',
|
|
63
|
+
client_unique_id: 'workorder-chat-456',
|
|
64
|
+
creator_unique_id: 'user-789',
|
|
65
|
+
original_output: 'Here is the original LLM response!',
|
|
66
|
+
revised_output: 'Here is the human edit of the original LLM response.',
|
|
67
|
+
explanation: 'Tone of the original response read like AI-generated open source README docs',
|
|
68
|
+
like: true
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Field Guide:** All fields are optional, but here's how to get the best results:
|
|
73
|
+
|
|
74
|
+
### Matching Fields
|
|
75
|
+
- **`llm_request_log_id`** 🎯 *Exact Match* - ID from the Coolhand API response when the original LLM request was logged. Provides exact matching.
|
|
76
|
+
- **`llm_provider_unique_id`** 🎯 *Exact Match* - The x-request-id from the LLM API response (e.g., "req_xxxxxxx")
|
|
77
|
+
- **`original_output`** 🔍 *Fuzzy Match* - The original LLM response text. Provides fuzzy matching but isn't 100% reliable.
|
|
78
|
+
- **`client_unique_id`** 🔗 *Your Internal Matcher* - Connect to an identifier from your system for internal matching
|
|
79
|
+
|
|
80
|
+
### Quality Data
|
|
81
|
+
- **`revised_output`** ⭐ *Best Signal* - End user revision of the LLM response. The highest value data for improving quality scores.
|
|
82
|
+
- **`explanation`** 💬 *Medium Signal* - End user explanation of why the response was good or bad. Valuable qualitative data.
|
|
83
|
+
- **`like`** 👍 *Low Signal* - Boolean like/dislike. Lower quality signal but easy for users to provide.
|
|
84
|
+
- **`creator_unique_id`** 👤 *User Tracking* - Unique ID to match feedback to the end user who created it
|
|
85
|
+
|
|
86
|
+
## Rails Integration
|
|
87
|
+
|
|
88
|
+
### Configuration
|
|
89
|
+
|
|
90
|
+
Create an initializer file at `config/initializers/coolhand.rb`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# config/initializers/coolhand.rb
|
|
94
|
+
Coolhand.configure do |config|
|
|
95
|
+
# Your Coolhand API Key (Required)
|
|
96
|
+
# Best practice: Use Rails credentials or environment-specific configuration
|
|
97
|
+
config.api_key = Rails.application.credentials.coolhand_api_key
|
|
98
|
+
|
|
99
|
+
# Set to true to suppress console output
|
|
100
|
+
config.silent = Rails.env.production?
|
|
101
|
+
|
|
102
|
+
# Specify which LLM endpoints to intercept (array of strings)
|
|
103
|
+
# Optional - defaults to ["api.openai.com", "api.anthropic.com"]
|
|
104
|
+
# config.intercept_addresses = ["api.openai.com", "api.anthropic.com", "api.cohere.ai"]
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Rails Controller Example
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class ChatController < ApplicationController
|
|
112
|
+
def create_feedback
|
|
113
|
+
feedback_service = Coolhand::Ruby::FeedbackService.new(Coolhand.configuration)
|
|
114
|
+
|
|
115
|
+
feedback = feedback_service.create_feedback(
|
|
116
|
+
llm_request_log_id: params[:log_id],
|
|
117
|
+
creator_unique_id: current_user.id,
|
|
118
|
+
original_output: params[:original_response],
|
|
119
|
+
revised_output: params[:edited_response],
|
|
120
|
+
explanation: params[:feedback_text],
|
|
121
|
+
like: params[:thumbs_up]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if feedback
|
|
125
|
+
render json: { success: true, message: 'Feedback recorded' }
|
|
126
|
+
else
|
|
127
|
+
render json: { success: false, message: 'Failed to record feedback' }, status: 422
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Background Job Example
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class FeedbackCollectionJob < ApplicationJob
|
|
137
|
+
def perform(feedback_data)
|
|
138
|
+
feedback_service = Coolhand::Ruby::FeedbackService.new(Coolhand.configuration)
|
|
139
|
+
|
|
140
|
+
feedback_service.create_feedback(
|
|
141
|
+
llm_provider_unique_id: feedback_data[:request_id],
|
|
142
|
+
creator_unique_id: feedback_data[:user_id],
|
|
143
|
+
original_output: feedback_data[:original],
|
|
144
|
+
explanation: feedback_data[:reason],
|
|
145
|
+
like: feedback_data[:positive]
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Configuration Options
|
|
152
|
+
|
|
153
|
+
### Configuration Parameters
|
|
154
|
+
|
|
155
|
+
| Option | Type | Default | Description |
|
|
156
|
+
|--------|------|---------|-------------|
|
|
157
|
+
| `api_key` | String | *required* | Your Coolhand API key for authentication |
|
|
158
|
+
| `silent` | Boolean | `false` | Whether to suppress console output |
|
|
159
|
+
| `intercept_addresses` | Array | `["api.openai.com", "api.anthropic.com"]` | Array of API endpoint strings to monitor |
|
|
160
|
+
|
|
161
|
+
## Usage Examples
|
|
162
|
+
|
|
163
|
+
### With OpenAI Ruby Client
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
require 'openai'
|
|
167
|
+
require 'coolhand'
|
|
168
|
+
|
|
169
|
+
# Configure Coolhand
|
|
170
|
+
Coolhand.configure do |config|
|
|
171
|
+
config.api_key = 'your_api_key_here'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Use OpenAI normally - requests are automatically logged
|
|
175
|
+
client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
|
|
176
|
+
|
|
177
|
+
response = client.chat(
|
|
178
|
+
parameters: {
|
|
179
|
+
model: "gpt-3.5-turbo",
|
|
180
|
+
messages: [{ role: "user", content: "These pretzels are making me thirsty!"}],
|
|
181
|
+
temperature: 0.7
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
puts response.dig("choices", 0, "message", "content")
|
|
186
|
+
# The request and response have been automatically logged to Coolhand!
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### With Anthropic Ruby Client
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
require 'anthropic'
|
|
193
|
+
require 'coolhand'
|
|
194
|
+
|
|
195
|
+
# Configure Coolhand
|
|
196
|
+
Coolhand.configure do |config|
|
|
197
|
+
config.api_key = 'your_api_key_here'
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Use Anthropic normally - requests are automatically logged
|
|
201
|
+
anthropic = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
|
|
202
|
+
|
|
203
|
+
response = anthropic.messages(
|
|
204
|
+
model: "claude-3-opus",
|
|
205
|
+
max_tokens: 1024,
|
|
206
|
+
messages: [{ role: "user", content: "Hello, Claude!" }]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
puts response["content"]
|
|
210
|
+
# Automatically logged to Coolhand!
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## What Gets Logged
|
|
214
|
+
|
|
215
|
+
The monitor captures:
|
|
216
|
+
|
|
217
|
+
- **Request Data**: Method, URL, headers, request body
|
|
218
|
+
- **Response Data**: Status code, headers, response body
|
|
219
|
+
- **Metadata**: Timestamp, protocol used
|
|
220
|
+
- **LLM-Specific**: Model used, token counts, temperature settings
|
|
221
|
+
|
|
222
|
+
Headers containing API keys are automatically sanitized for security.
|
|
223
|
+
|
|
224
|
+
## Supported Libraries
|
|
225
|
+
|
|
226
|
+
The monitor works with any Ruby library that uses Faraday for HTTP(S) requests to LLM APIs, including:
|
|
227
|
+
|
|
228
|
+
- OpenAI Ruby SDK
|
|
229
|
+
- Anthropic Ruby SDK
|
|
230
|
+
- ruby-openai gem
|
|
231
|
+
- LangChain.rb
|
|
232
|
+
- Direct Faraday requests
|
|
233
|
+
- Any other Faraday-based HTTP client
|
|
234
|
+
|
|
235
|
+
## How It Works
|
|
236
|
+
|
|
237
|
+
The gem patches Faraday connections to intercept HTTP requests. When a request matches the configured LLM endpoints:
|
|
238
|
+
|
|
239
|
+
1. The original request executes normally
|
|
240
|
+
2. Request and response data (body, headers, status) are captured
|
|
241
|
+
3. Data is sent to the Coolhand API asynchronously in a background thread
|
|
242
|
+
4. Your application continues without any performance impact
|
|
243
|
+
|
|
244
|
+
For non-matching endpoints, requests pass through unchanged.
|
|
245
|
+
|
|
246
|
+
## Troubleshooting
|
|
247
|
+
|
|
248
|
+
### Debugging Output
|
|
249
|
+
|
|
250
|
+
Enable verbose logging to see what's being intercepted:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
Coolhand.configure do |config|
|
|
254
|
+
config.api_key = 'your_api_key_here'
|
|
255
|
+
config.silent = false # Enable console output
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Testing
|
|
260
|
+
|
|
261
|
+
In test environments, you may want to configure differently:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# config/initializers/coolhand.rb
|
|
265
|
+
if Rails.env.test?
|
|
266
|
+
Coolhand.configure do |config|
|
|
267
|
+
config.api_key = 'test_key'
|
|
268
|
+
config.silent = true
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Non-Rails Applications
|
|
274
|
+
|
|
275
|
+
For standard Ruby scripts or non-Rails applications:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
#!/usr/bin/env ruby
|
|
279
|
+
require 'coolhand'
|
|
280
|
+
|
|
281
|
+
Coolhand.configure do |config|
|
|
282
|
+
config.api_key = 'your_api_key_here' # Store securely, don't commit to git
|
|
283
|
+
config.environment = 'production'
|
|
284
|
+
config.silent = false
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Your application code here...
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## API Key
|
|
291
|
+
|
|
292
|
+
🆓 **Sign up for free** at [coolhand.io](https://coolhand.io/) to get your API key and start monitoring your LLM usage.
|
|
293
|
+
|
|
294
|
+
**What you get:**
|
|
295
|
+
- Complete LLM request and response logging
|
|
296
|
+
- Usage analytics and insights
|
|
297
|
+
- Feedback collection and quality scoring
|
|
298
|
+
- No credit card required to start
|
|
299
|
+
|
|
300
|
+
## Error Handling
|
|
301
|
+
|
|
302
|
+
The monitor handles errors gracefully:
|
|
303
|
+
|
|
304
|
+
- Failed API logging attempts are logged to console but don't interrupt your application
|
|
305
|
+
- Invalid API keys will be reported but won't crash your app
|
|
306
|
+
- Network issues are handled with appropriate error messages
|
|
307
|
+
|
|
308
|
+
## Security
|
|
309
|
+
|
|
310
|
+
- API keys in request headers are automatically redacted
|
|
311
|
+
- No sensitive data is exposed in logs
|
|
312
|
+
- All data is sent via HTTPS to Coolhand servers
|
|
313
|
+
|
|
314
|
+
## Other Languages
|
|
315
|
+
|
|
316
|
+
- **Node.js**: [coolhand-node package](https://github.com/coolhand-io/coolhand-node) - Coolhand monitoring for Node.js applications
|
|
317
|
+
- **API Docs**: [API Documentation](https://coolhand.io/docs) - Direct API integration documentation
|
|
318
|
+
|
|
319
|
+
## Community
|
|
320
|
+
|
|
321
|
+
- **Questions?** [Create an issue](https://github.com/Coolhand-Labs/coolhand-ruby/issues)
|
|
322
|
+
- **Contribute?** [Submit a pull request](https://github.com/Coolhand-Labs/coolhand-ruby/pulls)
|
|
323
|
+
- **Support?** Visit [coolhandlabs.com](https://coolhandlabs.com)
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
Apache-2.0
|
data/Rakefile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/coolhand/ruby/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "coolhand"
|
|
7
|
+
spec.version = Coolhand::Ruby::VERSION
|
|
8
|
+
spec.authors = ["Michael Carroll", "Yaroslav Malyk"]
|
|
9
|
+
spec.email = ["mc@coolhandlabs.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Intercepts and logs OpenAI API calls from a Ruby application."
|
|
12
|
+
spec.description = "A Ruby gem to automatically monitor and log external LLM requests. It patches Net::HTTP " \
|
|
13
|
+
"to capture request and response data."
|
|
14
|
+
spec.homepage = "https://coolhandlabs.com/"
|
|
15
|
+
spec.license = "Apache-2.0"
|
|
16
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
17
|
+
|
|
18
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
19
|
+
|
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/Coolhand-Labs/coolhand-ruby"
|
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/Coolhand-Labs/coolhand-ruby"
|
|
23
|
+
|
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
26
|
+
spec.files = Dir.chdir(__dir__) do
|
|
27
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
28
|
+
(File.expand_path(f) == __FILE__) ||
|
|
29
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
spec.bindir = "exe"
|
|
33
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
34
|
+
spec.require_paths = ["lib"]
|
|
35
|
+
|
|
36
|
+
# Uncomment to register a new dependency of your gem
|
|
37
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
|
38
|
+
|
|
39
|
+
# For more information and examples about making a new gem, check out our
|
|
40
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
41
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
42
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Coolhand
|
|
8
|
+
module Ruby
|
|
9
|
+
class ApiService
|
|
10
|
+
BASE_URI = "https://coolhand.io/api"
|
|
11
|
+
|
|
12
|
+
attr_reader :api_endpoint
|
|
13
|
+
|
|
14
|
+
def initialize(endpoint_path)
|
|
15
|
+
@api_endpoint = "#{BASE_URI}/#{endpoint_path}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configuration
|
|
19
|
+
Coolhand.configuration
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def api_key
|
|
23
|
+
configuration.api_key
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def silent
|
|
27
|
+
configuration.silent
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def create_request_options(_payload)
|
|
33
|
+
{
|
|
34
|
+
"Content-Type" => "application/json",
|
|
35
|
+
"X-API-Key" => api_key
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_request(payload, success_message)
|
|
40
|
+
uri = URI.parse(@api_endpoint)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = (uri.scheme == "https")
|
|
43
|
+
|
|
44
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
45
|
+
headers = create_request_options(payload)
|
|
46
|
+
headers.each { |key, value| request[key] = value }
|
|
47
|
+
request.body = JSON.generate(payload)
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
response = http.request(request)
|
|
51
|
+
|
|
52
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
53
|
+
result = JSON.parse(response.body, symbolize_names: true)
|
|
54
|
+
log success_message
|
|
55
|
+
result
|
|
56
|
+
else
|
|
57
|
+
puts "❌ Request failed: #{response.code} - #{response.body}"
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
puts "❌ Request error: #{e.message}"
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def log(*args)
|
|
67
|
+
puts args.join(" ") unless silent
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def log_separator
|
|
71
|
+
log("═" * 60) unless silent
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def create_feedback(feedback)
|
|
75
|
+
payload = {
|
|
76
|
+
llm_request_log_feedback: feedback
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
log_feedback_info(feedback)
|
|
80
|
+
|
|
81
|
+
result = send_request(
|
|
82
|
+
payload,
|
|
83
|
+
"✅ Successfully created feedback with ID: #{feedback[:llm_request_log_id] || 'N/A'}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
log_separator
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def create_log(captured_data)
|
|
91
|
+
payload = {
|
|
92
|
+
llm_request_log: {
|
|
93
|
+
raw_request: captured_data
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
log_request_info(captured_data)
|
|
98
|
+
|
|
99
|
+
result = send_request(
|
|
100
|
+
payload,
|
|
101
|
+
"✅ Successfully logged to API"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
puts "✅ Successfully logged to API with ID: #{result[:id]}" if result && !silent
|
|
105
|
+
|
|
106
|
+
log_separator
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def log_feedback_info(feedback)
|
|
113
|
+
return if silent
|
|
114
|
+
|
|
115
|
+
puts "\n📝 CREATING FEEDBACK for LLM Request Log ID: #{feedback[:llm_request_log_id]}"
|
|
116
|
+
puts "👍/👎 Like: #{feedback[:like]}"
|
|
117
|
+
|
|
118
|
+
if feedback[:explanation]
|
|
119
|
+
explanation = feedback[:explanation]
|
|
120
|
+
truncated = explanation.length > 100 ? "#{explanation[0..99]}..." : explanation
|
|
121
|
+
puts "💭 Explanation: #{truncated}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts "📤 Sending to: #{@api_endpoint}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def log_request_info(captured_data)
|
|
128
|
+
return if silent
|
|
129
|
+
|
|
130
|
+
puts "\n🎉 LOGGING OpenAI API Call #{@api_endpoint}"
|
|
131
|
+
puts captured_data
|
|
132
|
+
puts "📤 Sending to: #{@api_endpoint}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coolhand
|
|
4
|
+
# Handles all configuration settings for the gem.
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :api_key, :environment, :silent
|
|
7
|
+
attr_reader :intercept_addresses
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
# Set defaults
|
|
11
|
+
@environment = "production"
|
|
12
|
+
@api_key = nil
|
|
13
|
+
@silent = false
|
|
14
|
+
@intercept_addresses = ["api.openai.com", "api.anthropic.com"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Custom setter that preserves defaults when nil/empty array is provided
|
|
18
|
+
def intercept_addresses=(value)
|
|
19
|
+
return if value.nil? || (value.is_a?(Array) && value.empty?)
|
|
20
|
+
|
|
21
|
+
@intercept_addresses = value.is_a?(Array) ? value : [value]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate!
|
|
25
|
+
# Validate API Key after configuration
|
|
26
|
+
if api_key.nil?
|
|
27
|
+
Coolhand.log "❌ Coolhand Error: API Key is required. Please set it in the configuration."
|
|
28
|
+
raise Error, "API Key is required"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Validate intercept_addresses after configuration
|
|
32
|
+
if intercept_addresses.nil? || intercept_addresses.empty?
|
|
33
|
+
Coolhand.log "❌ Coolhand Error: Intercept addresses cannot be empty. Please set it in the configuration."
|
|
34
|
+
raise Error, "Intercept addresses cannot be empty"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coolhand
|
|
4
|
+
class Interceptor < Faraday::Middleware
|
|
5
|
+
ORIGINAL_METHOD_ALIAS = :coolhand_original_initialize
|
|
6
|
+
|
|
7
|
+
def self.patch!
|
|
8
|
+
return if Faraday::Connection.private_method_defined?(ORIGINAL_METHOD_ALIAS)
|
|
9
|
+
|
|
10
|
+
Coolhand.log "📡 Monitoring outbound requests ..."
|
|
11
|
+
|
|
12
|
+
Faraday::Connection.class_eval do
|
|
13
|
+
alias_method ORIGINAL_METHOD_ALIAS, :initialize
|
|
14
|
+
|
|
15
|
+
def initialize(url = nil, options = nil, &block)
|
|
16
|
+
send(ORIGINAL_METHOD_ALIAS, url, options, &block)
|
|
17
|
+
|
|
18
|
+
use Interceptor
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Coolhand.log "🔧 Setting up monitoring for Faraday ..."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.unpatch!
|
|
26
|
+
return unless Faraday::Connection.private_method_defined?(ORIGINAL_METHOD_ALIAS)
|
|
27
|
+
|
|
28
|
+
Faraday::Connection.class_eval do
|
|
29
|
+
alias_method :initialize, ORIGINAL_METHOD_ALIAS
|
|
30
|
+
remove_method ORIGINAL_METHOD_ALIAS
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Coolhand.log "🔌 Faraday unpatched ..."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(env)
|
|
37
|
+
return super unless llm_api_request?(env)
|
|
38
|
+
|
|
39
|
+
Coolhand.log "🎯 INTERCEPTING OpenAI call #{env.url}"
|
|
40
|
+
|
|
41
|
+
call_data = build_call_data(env)
|
|
42
|
+
buffer = override_on_data(env)
|
|
43
|
+
|
|
44
|
+
process_complete_callback(env, buffer, call_data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def llm_api_request?(env)
|
|
50
|
+
Coolhand.configuration.intercept_addresses.any? do |address|
|
|
51
|
+
env.url.to_s.include?(address)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_call_data(env)
|
|
56
|
+
{
|
|
57
|
+
id: SecureRandom.uuid,
|
|
58
|
+
timestamp: DateTime.now,
|
|
59
|
+
method: env.method,
|
|
60
|
+
url: env.url.to_s,
|
|
61
|
+
headers: sanitize_headers(env.request_headers),
|
|
62
|
+
request_body: parse_json(env.request_body),
|
|
63
|
+
response_body: nil,
|
|
64
|
+
response_headers: nil,
|
|
65
|
+
status_code: nil
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def override_on_data(env)
|
|
70
|
+
buffer = +""
|
|
71
|
+
original_on_data = env.request.on_data
|
|
72
|
+
env.request.on_data = proc do |chunk, overall_received_bytes|
|
|
73
|
+
buffer << chunk
|
|
74
|
+
|
|
75
|
+
original_on_data&.call(chunk, overall_received_bytes)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
buffer
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def process_complete_callback(env, buffer, call_data)
|
|
82
|
+
@app.call(env).on_complete do |response_env|
|
|
83
|
+
if buffer.empty?
|
|
84
|
+
body = response_env.body
|
|
85
|
+
else
|
|
86
|
+
body = buffer
|
|
87
|
+
response_env.body = body
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
call_data[:response_body] = parse_json(body)
|
|
91
|
+
call_data[:response_headers] = sanitize_headers(response_env.request_headers)
|
|
92
|
+
call_data[:status_code] = response_env.status
|
|
93
|
+
|
|
94
|
+
Thread.new { Coolhand.logger_service.log_to_api(call_data) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_json(string)
|
|
99
|
+
JSON.parse(string)
|
|
100
|
+
rescue JSON::ParserError, TypeError
|
|
101
|
+
string
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sanitize_headers(headers)
|
|
105
|
+
sanitized = headers.transform_keys(&:to_s).dup
|
|
106
|
+
|
|
107
|
+
if sanitized["Authorization"]
|
|
108
|
+
sanitized["Authorization"] = sanitized["Authorization"].gsub(/Bearer .+/, "Bearer [REDACTED]")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
%w[openai-api-key api-key].each do |key|
|
|
112
|
+
sanitized[key] = "[REDACTED]" if sanitized[key]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sanitized
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "api_service"
|
|
4
|
+
|
|
5
|
+
module Coolhand
|
|
6
|
+
module Ruby
|
|
7
|
+
class LoggerService < ApiService
|
|
8
|
+
def initialize
|
|
9
|
+
super("v2/llm_request_logs")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def log_to_api(captured_data)
|
|
13
|
+
create_log(captured_data)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "faraday"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
require_relative "ruby/version"
|
|
9
|
+
require_relative "ruby/configuration"
|
|
10
|
+
require_relative "ruby/interceptor"
|
|
11
|
+
require_relative "ruby/api_service"
|
|
12
|
+
require_relative "ruby/logger_service"
|
|
13
|
+
require_relative "ruby/feedback_service"
|
|
14
|
+
|
|
15
|
+
# The main module for the Coolhand gem.
|
|
16
|
+
# It provides the configuration interface and initializes the patching.
|
|
17
|
+
module Coolhand
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
# Class-level instance variables to hold the configuration
|
|
21
|
+
@configuration = Configuration.new
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
attr_reader :configuration
|
|
25
|
+
|
|
26
|
+
# Reset configuration to defaults (mainly for testing)
|
|
27
|
+
def reset_configuration!
|
|
28
|
+
@configuration = Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Provides a block to configure the gem.
|
|
32
|
+
#
|
|
33
|
+
# Example:
|
|
34
|
+
# Coolhand.configure do |config|
|
|
35
|
+
# config.environment = 'development'
|
|
36
|
+
# config.silent = false
|
|
37
|
+
# config.api_key = "xxx-yyy-zzz"
|
|
38
|
+
# config.intercept_addresses = ["openai.com"]
|
|
39
|
+
# end
|
|
40
|
+
def configure
|
|
41
|
+
yield(configuration)
|
|
42
|
+
|
|
43
|
+
configuration.validate!
|
|
44
|
+
|
|
45
|
+
# Apply the patch after configuration is set
|
|
46
|
+
Interceptor.patch!
|
|
47
|
+
|
|
48
|
+
log "✅ Coolhand ready - will log OpenAI calls"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def capture
|
|
52
|
+
unless block_given?
|
|
53
|
+
log "❌ Coolhand Error: Method .capture requires block."
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Interceptor.patch!
|
|
58
|
+
|
|
59
|
+
yield
|
|
60
|
+
ensure
|
|
61
|
+
Interceptor.unpatch!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A simple logger that respects the 'silent' configuration option.
|
|
65
|
+
def log(message)
|
|
66
|
+
return if configuration.silent
|
|
67
|
+
|
|
68
|
+
puts "COOLHAND: #{message}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Creates a new FeedbackService instance
|
|
72
|
+
def feedback_service
|
|
73
|
+
Ruby::FeedbackService.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Creates a new LoggerService instance
|
|
77
|
+
def logger_service
|
|
78
|
+
Ruby::LoggerService.new
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: coolhand
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael Carroll
|
|
8
|
+
- Yaroslav Malyk
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: exe
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2025-10-22 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: A Ruby gem to automatically monitor and log external LLM requests. It
|
|
15
|
+
patches Net::HTTP to capture request and response data.
|
|
16
|
+
email:
|
|
17
|
+
- mc@coolhandlabs.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- ".idea/coolhand-ruby.iml"
|
|
23
|
+
- ".rspec"
|
|
24
|
+
- ".rubocop.yml"
|
|
25
|
+
- ".simplecov"
|
|
26
|
+
- CHANGELOG.md
|
|
27
|
+
- LICENSE
|
|
28
|
+
- README.md
|
|
29
|
+
- Rakefile
|
|
30
|
+
- coolhand-ruby.gemspec
|
|
31
|
+
- lib/coolhand/ruby.rb
|
|
32
|
+
- lib/coolhand/ruby/api_service.rb
|
|
33
|
+
- lib/coolhand/ruby/configuration.rb
|
|
34
|
+
- lib/coolhand/ruby/feedback_service.rb
|
|
35
|
+
- lib/coolhand/ruby/interceptor.rb
|
|
36
|
+
- lib/coolhand/ruby/logger_service.rb
|
|
37
|
+
- lib/coolhand/ruby/version.rb
|
|
38
|
+
- sig/coolhand/ruby.rbs
|
|
39
|
+
homepage: https://coolhandlabs.com/
|
|
40
|
+
licenses:
|
|
41
|
+
- Apache-2.0
|
|
42
|
+
metadata:
|
|
43
|
+
allowed_push_host: https://rubygems.org
|
|
44
|
+
homepage_uri: https://coolhandlabs.com/
|
|
45
|
+
source_code_uri: https://github.com/Coolhand-Labs/coolhand-ruby
|
|
46
|
+
changelog_uri: https://github.com/Coolhand-Labs/coolhand-ruby
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
post_install_message:
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: 3.0.0
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.3.26
|
|
64
|
+
signing_key:
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: Intercepts and logs OpenAI API calls from a Ruby application.
|
|
67
|
+
test_files: []
|