glim_ai 0.2.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +25 -0
  3. data/Gemfile.lock +49 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +125 -0
  6. data/Rakefile +31 -0
  7. data/examples/autocode/autocode.rb +166 -0
  8. data/examples/autocode/solargraph_test.rb +59 -0
  9. data/examples/autocode/templates/changed_files_now_evaluate_output.erb +29 -0
  10. data/examples/autocode/templates/task.erb +16 -0
  11. data/examples/calc/calc.rb +50 -0
  12. data/examples/code_competition/code_competition.rb +78 -0
  13. data/examples/code_competition/output/python_claude-2.rb +33 -0
  14. data/examples/code_competition/output/python_claude-instant-1.rb +18 -0
  15. data/examples/code_competition/output/python_gpt-3.5-turbo-16k.rb +69 -0
  16. data/examples/code_competition/output/python_gpt-3.5-turbo.rb +43 -0
  17. data/examples/code_competition/output/python_gpt-4.rb +34 -0
  18. data/examples/code_competition/output/ruby_claude-2.rb +22 -0
  19. data/examples/code_competition/output/ruby_claude-instant-1.rb +20 -0
  20. data/examples/code_competition/output/ruby_gpt-3.5-turbo-16k.rb +27 -0
  21. data/examples/code_competition/output/ruby_gpt-3.5-turbo.rb +30 -0
  22. data/examples/code_competition/output/ruby_gpt-4.rb +31 -0
  23. data/examples/code_competition/output/ruby_human.rb +41 -0
  24. data/examples/code_competition/templates/analyze_code.erb +33 -0
  25. data/examples/code_competition/templates/write_code.erb +26 -0
  26. data/examples/glim_demo/ask_all.rb +35 -0
  27. data/examples/glim_demo/templates/rate_all.erb +24 -0
  28. data/examples/improve_prompt/improve_prompt.rb +62 -0
  29. data/examples/improve_prompt/templates/stashed/prompt_attempt_explicit_steps.erb +15 -0
  30. data/examples/improve_prompt/templates/stashed/prompt_attempt_explicit_steps_user_message.erb +15 -0
  31. data/examples/improve_prompt/templates/stashed/prompt_attempt_initial.erb +8 -0
  32. data/examples/improve_prompt/templates/stashed/prompt_attempt_nothing.erb +19 -0
  33. data/examples/improve_prompt/templates/try_code_first.erb +13 -0
  34. data/examples/improve_prompt/templates/try_code_first_system.erb +22 -0
  35. data/examples/old/econ/discounting.rb +27 -0
  36. data/examples/old/econ/templates/discounting.erb +10 -0
  37. data/examples/old/generate_glim_code/generate_glim_code.rb +34 -0
  38. data/examples/old/generate_glim_code/templates/generate_glim_code.erb +17 -0
  39. data/examples/old/generate_glim_code/templates/improve_code.erb +27 -0
  40. data/examples/old/glim_dev_tools/ask_code_question.rb +38 -0
  41. data/examples/old/glim_dev_tools/templates/ask_code_question.erb +12 -0
  42. data/examples/old/glim_dev_tools/templates/write_globals_test.erb +28 -0
  43. data/examples/old/glim_dev_tools/write_globals_test.rb +20 -0
  44. data/examples/old/linguistics/nine.rb +0 -0
  45. data/examples/old/rewrite_code/input/hello.py +1 -0
  46. data/examples/old/rewrite_code/input/subdir/hello.py +1 -0
  47. data/examples/old/rewrite_code/input/world.py +1 -0
  48. data/examples/old/rewrite_code/rewrite_code.rb +18 -0
  49. data/examples/old/rewrite_code/templates/rewrite_code.erb +32 -0
  50. data/examples/window_check/data.rb +1260 -0
  51. data/examples/window_check/fruits.rb +118 -0
  52. data/examples/window_check/tools.rb +56 -0
  53. data/examples/window_check/window_check.rb +214 -0
  54. data/glim_generated_tests/make_special_code_with_fixed_length_test.rb +44 -0
  55. data/glim_generated_tests/old-20230831120513-make_special_code_with_fixed_length_test.rb +1 -0
  56. data/glim_generated_tests/old-20230831121222-make_special_code_with_fixed_length_test.rb +55 -0
  57. data/glim_generated_tests/old-20230831124501-make_special_code_with_fixed_length_test.rb +33 -0
  58. data/glim_generated_tests/test/make_special_code_with_fixed_length_test.rb +58 -0
  59. data/lib/anthropic_request_details.rb +37 -0
  60. data/lib/anthropic_response.rb +101 -0
  61. data/lib/chat_request_details.rb +140 -0
  62. data/lib/chat_response.rb +303 -0
  63. data/lib/glim_ai/version.rb +5 -0
  64. data/lib/glim_ai.rb +8 -0
  65. data/lib/glim_ai_callable.rb +151 -0
  66. data/lib/glim_context.rb +62 -0
  67. data/lib/glim_helpers.rb +54 -0
  68. data/lib/glim_request.rb +266 -0
  69. data/lib/glim_response.rb +155 -0
  70. data/lib/globals.rb +255 -0
  71. data/lib/html_templates/chat_request.erb +86 -0
  72. data/sample.env +9 -0
  73. metadata +131 -0
@@ -0,0 +1,151 @@
1
+
2
+
3
+
4
+ module AICallable
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def ai_callable_as(method_name, &block)
12
+ method_signature_builder = MethodSignatureBuilder.new
13
+ method_signature_builder.instance_eval(&block)
14
+ ai_method_signatures << method_signature_builder.signature.merge({ name: method_name.to_s })
15
+ end
16
+
17
+ def ai_method_signatures
18
+ @ai_method_signatures ||= []
19
+ end
20
+
21
+ def ai_method_signatures_clean
22
+ return @ai_method_signatures_clean if @ai_method_signatures_clean
23
+ def remove_local_name(data)
24
+ result = data.map do |item|
25
+ {
26
+ 'name' => item[:name],
27
+ 'description' => item[:description],
28
+ 'parameters' => {
29
+ 'type' => item[:parameters][:type],
30
+ 'properties' => item[:parameters][:properties].transform_values { |property| property.reject { |k, _| k == :local_name } },
31
+ 'required' => item[:parameters][:required]
32
+ }
33
+ }
34
+ rescue => e
35
+ putt :warning, "Issue with #{item}: #{e.message}"
36
+ raise e
37
+ end
38
+ return result
39
+ end
40
+ sigs = ai_method_signatures # this is an array
41
+ @ai_method_signatures_clean = remove_local_name(sigs)
42
+ end
43
+
44
+ end # ClassMethods
45
+
46
+ def _perform_ai_call(eval_function_name, eval_function_arguments)
47
+ # begin
48
+ sigs = self.class.ai_method_signatures
49
+ putt(:functions, "eval_function_name = #{eval_function_name}, sigs: #{sigs}")
50
+
51
+ sig = sigs.select { |x| x[:name].to_s == eval_function_name.to_s }.first
52
+ props = sig[:parameters][:properties]
53
+ # props looks like this: {v1=>{...}, v2: {...}
54
+ local_function_arguments = {}
55
+ for ai_name in props.keys
56
+ v = props[ai_name]
57
+ # v looks like this: {:type=>:string, :description=>"The expression, as a string, in correct ruby syntax", :local_name=>:exp}}
58
+ local_param_name = v[:local_name]
59
+ local_function_arguments[local_param_name] = eval_function_arguments[ai_name]
60
+ end
61
+ putt(:functions, "eval_function_arguments: #{eval_function_arguments}")
62
+ putt(:functions, "local_function_arguments: #{local_function_arguments}")
63
+
64
+ required_params = sig[:parameters][:required]
65
+ for required_param in required_params
66
+ raise "Missing required parameter: #{required_param}" unless eval_function_arguments[required_param]
67
+ end
68
+ # eval_function_result = eval_functions_object.send(eval_function_name, **local_function_arguments)
69
+ eval_function_result = send(eval_function_name, **local_function_arguments)
70
+ # rescue => e
71
+ # putt :warning, "FAILED Function call to #{self}.#{eval_function_name}(#{local_function_arguments}): #{e}"
72
+ # eval_function_result = e.message
73
+ # end
74
+ return eval_function_result
75
+ end
76
+
77
+
78
+ class MethodSignatureBuilder
79
+ def initialize
80
+ @signature = {
81
+ parameters: {
82
+ type: "object",
83
+ properties: {},
84
+ required: []
85
+ }
86
+ }
87
+ end
88
+
89
+ attr_reader :signature
90
+
91
+ def describe(text)
92
+ @signature[:description] = text
93
+ end
94
+
95
+ # 4.2.1. Instance Data Model
96
+ # JSON Schema interprets documents according to a data model. A JSON value interpreted according to this data model is called an "instance".
97
+
98
+ # An instance has one of six primitive types, and a range of possible values depending on the type:
99
+
100
+ # null:
101
+ # A JSON "null" value
102
+ # boolean:
103
+ # A "true" or "false" value, from the JSON "true" or "false" value
104
+ # object:
105
+ # An unordered set of properties mapping a string to an instance, from the JSON "object" value
106
+ # array:
107
+ # An ordered list of instances, from the JSON "array" value
108
+ # number:
109
+ # An arbitrary-precision, base-10 decimal number value, from the JSON "number" value
110
+ # string:
111
+ # A string of Unicode code points, from the JSON "string" value
112
+
113
+ def number(name, description, opts = {})
114
+ # we want to use ai_name for everything here, but then when we invoke,
115
+ # we will want to look up the local name
116
+ ai_name = opts[:ai_name] || name
117
+ @signature[:parameters][:properties][ai_name] = {
118
+ type: :number,
119
+ description: description,
120
+ local_name: name
121
+ }
122
+ if opts[:required]
123
+ raise "Required muat be boolean if set" unless opts[:required].is_a?(TrueClass) || opts[:required].is_a?(FalseClass)
124
+ @signature[:parameters][:required] << ai_name if opts[:required]
125
+ end
126
+ end
127
+
128
+ def string(name, description, opts = {})
129
+ # we want to use ai_name for everything here, but then when we invoke,
130
+ # we will want to look up the local name
131
+ ai_name = opts[:ai_name] || name
132
+ @signature[:parameters][:properties][ai_name] = {
133
+ type: :string,
134
+ description: description,
135
+ local_name: name
136
+ }
137
+ if opts[:enum]
138
+ rng = opts[:enum]
139
+ for s in rng
140
+ raise "Invalid enum value: #{s}" unless s.is_a?(String)
141
+ end
142
+ @signature[:parameters][:properties][ai_name][:enum] = rng
143
+ end
144
+ if opts[:required]
145
+ raise "Required muat be boolean if set" unless opts[:required].is_a?(TrueClass) || opts[:required].is_a?(FalseClass)
146
+ @signature[:parameters][:required] << ai_name if opts[:required]
147
+ end
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,62 @@
1
+ require_relative 'globals'
2
+
3
+ class GlimContext
4
+
5
+ attr_reader :log_name, :template_subdir
6
+ attr_reader :start_time
7
+
8
+ def initialize(log_name: nil, template_subdir: "templates")
9
+ #["test/test_glim.rb:71:in `new'",
10
+ if !log_name
11
+ @log_name = caller[0].split(':').first.split('/').last
12
+ else
13
+ @log_name = log_name
14
+ end
15
+ @log_name += Time.now.strftime('%Y-%m-%d-%H-%M-%S')
16
+ @template_subdir = template_subdir.must_be_a String
17
+ putt :log, "GlimContext template_subdir=#{@template_subdir}, log_name: #{log_name.inspect}"
18
+ @start_time = Time.now
19
+ end
20
+
21
+ def request(args)
22
+ args_with_context = args.merge(context: self)
23
+ GlimRequest.new(**args_with_context)
24
+ end
25
+
26
+ def request_from_template(template_name, **template_args)
27
+ req = GlimRequest.new(context: self)
28
+ req.process_template(template_name, **template_args)
29
+ req.context = self
30
+ req
31
+ end
32
+
33
+ # just for convenience
34
+ def response_from_template(template_name, **template_args)
35
+ template_name.must_be_a String
36
+ template_args.must_be_a Hash
37
+ # puts("response_from_spec: #{template_args.inspect}")
38
+ req = request_from_template(template_name, **template_args)
39
+ req.response
40
+ end
41
+
42
+ def log_base_glim
43
+ ENV['GLIM_LOG_DIRECTORY']
44
+ end
45
+
46
+ def log_base
47
+ File.join(log_base_glim,log_name)
48
+ end
49
+
50
+ def log_line_to_summary(line)
51
+ log_summary_file = File.join(log_base, "llm_log.csv")
52
+ seconds_since_start = Time.now - start_time
53
+ s = "#{seconds_since_start.round(3)}, #{line}"
54
+ File.open(log_summary_file, 'a') do |f|
55
+ f.puts s
56
+ end
57
+ end
58
+
59
+
60
+ end
61
+
62
+
@@ -0,0 +1,54 @@
1
+ module GlimHelpers
2
+
3
+ # TODO: modify this so that you can also include a single file, list of files, etc
4
+ def include_files(path, prefix='')
5
+ putt :include_files, "include_files(path: #{path}, prefix: #{prefix})"
6
+ result = ""
7
+ Dir.foreach(path) do |entry|
8
+ next if entry.start_with?('.')
9
+ entry_path = File.join(path, entry)
10
+ relative_path = File.join(prefix, entry)
11
+ if File.directory?(entry_path)
12
+ result += include_files(entry_path, relative_path)
13
+ else
14
+ # elsif File.file?(entry_path)
15
+ # result += "\n```\n# File: #{relative_path}\n"
16
+ # result += File.read(entry_path)
17
+ # result += "\n```\n"
18
+ result += include_file(entry_path, relative_path)
19
+ end
20
+ end
21
+ result
22
+ end
23
+
24
+ def include_file(entry_path, relative_path = nil)
25
+ relative_path ||= entry_path
26
+ raise("File not found: #{entry_path}") if !File.file?(entry_path)
27
+ result = "\n<file pathname=\"#{relative_path}\">"
28
+ result += File.read(entry_path)
29
+ result + "</file>\n"
30
+ end
31
+
32
+
33
+ def prompt_output_files
34
+ <<-GLIM_PROMPT
35
+
36
+
37
+ TODO
38
+
39
+ fix this for xml
40
+
41
+ SYSTEM MESSAGE: ALWAYS, when asked to generate source code or other text files, use the following format:
42
+ <file pathname="path_to_file/hello.rb">
43
+ puts "Hello from Line 1"
44
+ puts "hello from Line 2"
45
+ </file>
46
+ So, the example above shows how you would include a file called "hello.rb" that belongs in the subdirectory "path_to_file" of the current directory.
47
+ The file would contain two "puts" statements.
48
+ Use this for all text files you generate, not just source code.
49
+
50
+ GLIM_PROMPT
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,266 @@
1
+ require 'must_be'
2
+ require 'json-schema'
3
+ require 'erb'
4
+
5
+ require_relative 'globals'
6
+ require_relative 'glim_helpers'
7
+
8
+
9
+ require_relative 'chat_request_details'
10
+ require_relative 'chat_response'
11
+
12
+ module GenericParams
13
+ def generic_params(*attr_names)
14
+ attr_names.each do |attr_name|
15
+ define_method("#{attr_name}=") do |value|
16
+ instance_variable_set("@#{attr_name}", value)
17
+ request_details.update_request_hash
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+
24
+ class GlimRequest
25
+
26
+ extend GenericParams
27
+
28
+ GENERIC_PARAMS = %i[temperature top_p max_tokens stop]
29
+
30
+ generic_params(*GENERIC_PARAMS)
31
+ attr_reader(*GENERIC_PARAMS)
32
+
33
+ attr_reader :llm_name, :prompt
34
+
35
+ attr_reader :request_hash
36
+
37
+ # this is the data structure the response class will send over the network
38
+ # the cache key is generated from this.
39
+ def initialize(**args)
40
+ @use_cached_response = true
41
+ @logged_something = false
42
+ args.each do |k, v|
43
+ if (GENERIC_PARAMS + %i[template_name template_text context llm_name ]).include?(k)
44
+ instance_variable_set("@#{k}", v)
45
+ else
46
+ raise "Unknown parameter #{k}"
47
+ end
48
+ # puts "GlimRequest.initialize: #{args_with_req.inspect}"
49
+ end
50
+ @request_hash = {}
51
+ request_details.llm_class_changed
52
+ end
53
+
54
+ def method_missing(method_name, *args, &block)
55
+ if request_details.respond_to?(method_name)
56
+ request_details.send(method_name, *args, &block)
57
+ else
58
+ raise "No method #{method_name} in #{request_details.class}."
59
+ end
60
+ end
61
+
62
+ def generic_params_hash
63
+ h = {}
64
+ GENERIC_PARAMS.each do |attr_name|
65
+ h[attr_name] = instance_variable_get("@#{attr_name}")
66
+ end
67
+ h
68
+ end
69
+
70
+ def self.openai_llms
71
+ %w[
72
+ gpt-4 gpt-4-0613 gpt-4-32k gpt-4-32k-0613
73
+ gpt-3.5-turbo gpt-3.5-turbo-0613 gpt-3.5-turbo-16k gpt-3.5-turbo-16k-0613]
74
+ end
75
+
76
+ def self.aviary_llms
77
+ llama2_llms + codellama_llms
78
+ end
79
+
80
+ def self.llama2_llms(size = nil)
81
+ sizes = [7,13,70]
82
+ if !size
83
+ return sizes.map { |n| "meta-llama/Llama-2-#{n}b-chat-hf" }
84
+ end
85
+ if sizes.include?(size)
86
+ return "meta-llama/Llama-2-#{size}b-chat-hf"
87
+ else
88
+ raise "Unknown llama size #{size}"
89
+ end
90
+ end
91
+
92
+ def self.codellama_llms
93
+ return ["codellama/CodeLlama-34b-Instruct-hf"]
94
+ end
95
+
96
+ def request_details
97
+ @request_details ||= details_class_for_llm_name.new(self) # raise("Set llm_name first!")
98
+ end
99
+
100
+ def details_class_for_llm_name
101
+ llm_name&.start_with?('claude') ? AnthropicRequestDetails : ChatRequestDetails
102
+ end
103
+
104
+ def llm_name=(llm_name)
105
+ @llm_name = llm_name
106
+ klass = details_class_for_llm_name
107
+ if @request_details&.class != klass
108
+ @request_details = klass.new(self)
109
+ putt :warning, "Setting llm_name to #{llm_name} caused deletion of request_hash, parameters might get lost, TODO"
110
+ @request_hash = {}
111
+ @request_details.llm_class_changed
112
+ end
113
+ request_details.update_request_hash
114
+ end
115
+
116
+ def prompt=(p)
117
+ @prompt = p
118
+ request_details.update_request_hash
119
+ save_log_file("prompt.txt", prompt)
120
+ end
121
+
122
+ def response_class
123
+ @request_details.response_class
124
+ end
125
+
126
+ attr_accessor :template_name, :template_text
127
+ attr_accessor :no_cache, :use_cached_response
128
+ attr_accessor :context
129
+
130
+ def log_base_this_request
131
+ @log_base_this_request ||= begin
132
+ timestamp = Time.now.strftime('%a-%H:%M:%S.%3N')
133
+ template_name_sanitized = (template_name || "no_template").gsub(/[^0-9A-Za-z.\-]/, '_')
134
+ subdir = File.join(context.log_base, "#{timestamp}-#{template_name_sanitized}")
135
+ FileUtils.mkdir_p(subdir) unless Dir.exist?(subdir)
136
+ putt :log, "Log path: #{@log_base_this_request}"
137
+ subdir
138
+ end
139
+ @log_base_this_request
140
+ end
141
+
142
+ def process_template(template_name, **template_args)
143
+ # TODO - think through how to handle paths
144
+ # basedir = File.dirname(File.expand_path($PROGRAM_NAME))
145
+
146
+ for c in caller
147
+ calling_file = c.split(':').first
148
+ break unless calling_file && calling_file.include?("/lib")
149
+ end
150
+ dir_path = File.dirname(calling_file)
151
+ template_path = File.join(dir_path, 'templates', "#{template_name}.erb")
152
+
153
+ unless File.exist?(template_path)
154
+ raise "Template #{template_name} not found: #{template_path}"
155
+ end
156
+
157
+ putt :config, template_path
158
+
159
+ #template_path = File.join("#{basedir}","templates","#{template_name}.erb")
160
+
161
+ template_text = File.read(template_path)
162
+ template = ERB.new(template_text)
163
+
164
+ wrapper = Object.new
165
+ wrapper.extend(GlimHelpers)
166
+ template_args.each do |key, value|
167
+ wrapper.define_singleton_method(key) { value }
168
+ end
169
+ req_instance = self # this way, we can access it in the define_method below
170
+ wrapper.define_singleton_method(:req) { req_instance } # caution: can't use self directly here, otherwise self == wrapper
171
+ @prompt = template.result(wrapper.instance_eval { binding })
172
+ @template_name = template_name
173
+ @template_text = template_text
174
+ request_details.update_request_hash
175
+ save_log_file("template_text.txt", template_text)
176
+ save_log_file("prompt.txt", prompt)
177
+ return nil
178
+ end
179
+
180
+ # def _generic_params
181
+ # h = {}
182
+ # for key in GENERIC_PARAMS
183
+ # h[key] = instance_variable_get("@#{key}")
184
+ # end
185
+ # h
186
+ # end
187
+
188
+ def count_tokens(s)
189
+ response_class._count_tokens(llm_name, s)
190
+ end
191
+
192
+ def llm_info
193
+ response_class._llm_info(llm_name)
194
+ end
195
+
196
+ def cost_per_prompt_token
197
+ llm_info[:cost_per_prompt_token]
198
+ end
199
+
200
+ def cost_per_completion_token
201
+ llm_info[:cost_per_completion_token]
202
+ end
203
+
204
+ def context_length
205
+ llm_info[:context_length]
206
+ end
207
+
208
+ def prompt_token_count
209
+ # careful; for open_ai, this needs prepare() first and we want to look at messages[]
210
+ count_tokens(prompt)
211
+ end
212
+
213
+ def min_cost
214
+ return prompt_token_count * cost_per_prompt_token
215
+ end
216
+
217
+ def max_cost
218
+ # TODO this doesn't work yet.
219
+ return cost_per_prompt_token * prompt_token_count + cost_per_completion_token * max_tokens
220
+ end
221
+
222
+ def cache_key
223
+ putt :cache, "Computing cache key based on:"
224
+ # putt :cache, JSON.pretty_generate(deep_copy_with_mods(request_hash, 88,88))
225
+ key = Digest::SHA1.hexdigest(request_hash.to_json)
226
+ putt :cache, "Cache key was: #{key}"
227
+ return key
228
+ end
229
+
230
+ # this will create a response and, unless it's cached, send off the request to the API
231
+ def response
232
+ response = response_class.new(self)
233
+ return response
234
+ end
235
+
236
+ def to_s
237
+ s = "Req to #{llm_name}"
238
+ s += " from #{template_name}" if template_name
239
+ s += request_details.to_s if request_details
240
+ end
241
+
242
+ def inspect
243
+ "#<GlimRequest: prompt_size=#{@prompt ? @prompt.size : 'nil'}, template_name=#{@template_name}>"
244
+ end
245
+
246
+
247
+ def save_log_file(section_name, content)
248
+ file_path = File.join(log_base_this_request, section_name)
249
+ putt(:log, "Saving to: #{file_path}")
250
+ File.write(file_path, content)
251
+
252
+ log_dir = ENV['GLIM_LOG_DIRECTORY']
253
+ if !@logged_something
254
+ @logged_something = true
255
+ last_all_files = File.join(log_dir,"_last","*")
256
+ #puts "deleting #{last_all_files}"
257
+ Dir.glob(last_all_files).each do |file|
258
+ File.delete(file) if File.file?(file)
259
+ end
260
+ end
261
+ last_file = File.join(log_dir,"_last",section_name)
262
+ FileUtils.mkdir_p(File.dirname(last_file)) unless Dir.exist?(File.dirname(last_file))
263
+ File.write(last_file, content)
264
+ end
265
+
266
+ end
@@ -0,0 +1,155 @@
1
+ require_relative 'globals'
2
+
3
+ class GlimResponseError < StandardError
4
+ def initialize(raw_response, message = "Unexpected GlimResponse")
5
+ puts("\n---GlimResponseError---")
6
+ puts(JSON.pretty_generate(raw_response) || "GlimResponse was Nil!")
7
+ super(message)
8
+ end
9
+ end
10
+
11
+ # A response is always either available or a request has been sent
12
+ class GlimResponse
13
+ attr_reader :req, :params
14
+
15
+ def initialize(req)
16
+ @req = req
17
+ log_request_hash
18
+ @time_request_sent = Time.now
19
+ @cached_response = _look_for_cached_response
20
+ if req.use_cached_response && @cached_response
21
+ putt :cache, "Using cached response for key: #{req.cache_key}"
22
+ @raw_response = cached_response.with_indifferent_access
23
+ log_raw_response
24
+ process_response_from_api
25
+ log_summary_append
26
+ else
27
+ if @cached_reaponse
28
+ putt :cache, "Making API call because req.use_cached_response was #{req.use_cached_response}, even though cached response was found for key: #{req.cache_key}"
29
+ else
30
+ putt :cache, "Making API call because no cached response was found for key: #{req.cache_key}"
31
+ end
32
+ @cached_response = false
33
+ putt :rpc, "RPC to #{req}"
34
+ async_send_request_to_api
35
+ # now the request has been send. the user's thread can do more stuff until the
36
+ # user eventually looks at this response, which will block if we haven't heard back.
37
+ end
38
+ end
39
+
40
+ def log_request_hash
41
+ s = "# request_hash generated by #{req.class} with #{req.request_details.class}\n"
42
+ s += JSON.pretty_generate(req.request_hash)
43
+ req.save_log_file("request_hash.json", s)
44
+
45
+
46
+ # template_text = File.read('lib/chat_request.erb')
47
+ # template = ERB.new(template_text)
48
+ #s = template.result_with_hash(request_hash: req.request_hash)
49
+ #req.save_log_file("request_hash.html",s)
50
+
51
+
52
+
53
+ end
54
+
55
+ def log_raw_response
56
+ req.save_log_file("raw_response.json", JSON.pretty_generate(raw_response))
57
+ end
58
+
59
+ def log_completion
60
+ req.save_log_file("completion.txt", completion)
61
+ end
62
+
63
+ def context
64
+ req.context
65
+ end
66
+
67
+ # caching
68
+ def save_raw_response_to_cache
69
+ putt :cache, "Saving response to cache for key: #{req.cache_key}"
70
+ cache_file = File.join(CACHE_PATH, "#{req.cache_key}.json")
71
+ File.write(cache_file, raw_response.to_json)
72
+ end
73
+
74
+ def _look_for_cached_response
75
+ cache_file = File.join(CACHE_PATH, "#{req.cache_key}.json")
76
+ me = self.class.name
77
+ if File.exist?(cache_file)
78
+ putt :cache, "Cached #{me} found for key: #{req.cache_key}"
79
+ return (JSON.parse(File.read(cache_file)).with_indifferent_access)
80
+ else
81
+ putt :cache, "No cached #{me} found for key: #{req.cache_key}"
82
+ return nil
83
+ end
84
+ end
85
+
86
+ attr_reader :cached_response
87
+
88
+ def response_available?
89
+ @thread.status == false
90
+ end
91
+
92
+ def responding_llm_name
93
+ raise "implement in subclass!"
94
+ end
95
+
96
+ def total_tokens
97
+ prompt_tokens + completion_tokens
98
+ end
99
+
100
+ def total_cost
101
+ prompt_tokens * req.cost_per_prompt_token + completion_tokens * req.cost_per_completion_token
102
+ end
103
+
104
+ def log_summary_append
105
+ templ = req.template_name ? req.template_name+".erb" : "no_template"
106
+ time_spent = Time.now - @time_request_sent
107
+ tps = completion_tokens / time_spent
108
+ s = "#{responding_llm_name}, #{templ}, #{prompt_tokens}, prompt_tokens + , #{completion_tokens}, completion_tokens = $"
109
+ if cached_response
110
+ s += ",0,0,cached, , , "
111
+ else
112
+ s += ",#{total_cost}, #{time_spent.round(3)}, seconds, #{tps}, tokens/s"
113
+ end
114
+ s += ", #{req.log_base_this_request}"
115
+ context.log_line_to_summary(s)
116
+ end
117
+
118
+
119
+ def wait_for_response
120
+ return if @raw_response
121
+ raise "No Thread! Call run() first!" unless @thread
122
+ if response_available?
123
+ putt :rpc, "Response already received from #{req.to_s}"
124
+ else
125
+ putt :rpc, "Will now block until we have a for #{req.to_s}"
126
+ end
127
+ @raw_response ||= (@thread.value.must_be_a Hash).with_indifferent_access
128
+ save_raw_response_to_cache unless req.no_cache
129
+ log_raw_response
130
+ process_response_from_api
131
+ log_summary_append
132
+ end
133
+
134
+ def err(msg)
135
+ raise GlimResponseError.new(raw_response, msg)
136
+ end
137
+
138
+ # the raw response as received from OpenAI
139
+ def raw_response
140
+ wait_for_response
141
+ @raw_response
142
+ end
143
+
144
+ def [](key)
145
+ wait_for_response
146
+ @raw_response[key]
147
+ end
148
+
149
+ def completion
150
+ wait_for_response
151
+ @completion
152
+ end
153
+
154
+ end
155
+