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.
- checksums.yaml +7 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +31 -0
- data/examples/autocode/autocode.rb +166 -0
- data/examples/autocode/solargraph_test.rb +59 -0
- data/examples/autocode/templates/changed_files_now_evaluate_output.erb +29 -0
- data/examples/autocode/templates/task.erb +16 -0
- data/examples/calc/calc.rb +50 -0
- data/examples/code_competition/code_competition.rb +78 -0
- data/examples/code_competition/output/python_claude-2.rb +33 -0
- data/examples/code_competition/output/python_claude-instant-1.rb +18 -0
- data/examples/code_competition/output/python_gpt-3.5-turbo-16k.rb +69 -0
- data/examples/code_competition/output/python_gpt-3.5-turbo.rb +43 -0
- data/examples/code_competition/output/python_gpt-4.rb +34 -0
- data/examples/code_competition/output/ruby_claude-2.rb +22 -0
- data/examples/code_competition/output/ruby_claude-instant-1.rb +20 -0
- data/examples/code_competition/output/ruby_gpt-3.5-turbo-16k.rb +27 -0
- data/examples/code_competition/output/ruby_gpt-3.5-turbo.rb +30 -0
- data/examples/code_competition/output/ruby_gpt-4.rb +31 -0
- data/examples/code_competition/output/ruby_human.rb +41 -0
- data/examples/code_competition/templates/analyze_code.erb +33 -0
- data/examples/code_competition/templates/write_code.erb +26 -0
- data/examples/glim_demo/ask_all.rb +35 -0
- data/examples/glim_demo/templates/rate_all.erb +24 -0
- data/examples/improve_prompt/improve_prompt.rb +62 -0
- data/examples/improve_prompt/templates/stashed/prompt_attempt_explicit_steps.erb +15 -0
- data/examples/improve_prompt/templates/stashed/prompt_attempt_explicit_steps_user_message.erb +15 -0
- data/examples/improve_prompt/templates/stashed/prompt_attempt_initial.erb +8 -0
- data/examples/improve_prompt/templates/stashed/prompt_attempt_nothing.erb +19 -0
- data/examples/improve_prompt/templates/try_code_first.erb +13 -0
- data/examples/improve_prompt/templates/try_code_first_system.erb +22 -0
- data/examples/old/econ/discounting.rb +27 -0
- data/examples/old/econ/templates/discounting.erb +10 -0
- data/examples/old/generate_glim_code/generate_glim_code.rb +34 -0
- data/examples/old/generate_glim_code/templates/generate_glim_code.erb +17 -0
- data/examples/old/generate_glim_code/templates/improve_code.erb +27 -0
- data/examples/old/glim_dev_tools/ask_code_question.rb +38 -0
- data/examples/old/glim_dev_tools/templates/ask_code_question.erb +12 -0
- data/examples/old/glim_dev_tools/templates/write_globals_test.erb +28 -0
- data/examples/old/glim_dev_tools/write_globals_test.rb +20 -0
- data/examples/old/linguistics/nine.rb +0 -0
- data/examples/old/rewrite_code/input/hello.py +1 -0
- data/examples/old/rewrite_code/input/subdir/hello.py +1 -0
- data/examples/old/rewrite_code/input/world.py +1 -0
- data/examples/old/rewrite_code/rewrite_code.rb +18 -0
- data/examples/old/rewrite_code/templates/rewrite_code.erb +32 -0
- data/examples/window_check/data.rb +1260 -0
- data/examples/window_check/fruits.rb +118 -0
- data/examples/window_check/tools.rb +56 -0
- data/examples/window_check/window_check.rb +214 -0
- data/glim_generated_tests/make_special_code_with_fixed_length_test.rb +44 -0
- data/glim_generated_tests/old-20230831120513-make_special_code_with_fixed_length_test.rb +1 -0
- data/glim_generated_tests/old-20230831121222-make_special_code_with_fixed_length_test.rb +55 -0
- data/glim_generated_tests/old-20230831124501-make_special_code_with_fixed_length_test.rb +33 -0
- data/glim_generated_tests/test/make_special_code_with_fixed_length_test.rb +58 -0
- data/lib/anthropic_request_details.rb +37 -0
- data/lib/anthropic_response.rb +101 -0
- data/lib/chat_request_details.rb +140 -0
- data/lib/chat_response.rb +303 -0
- data/lib/glim_ai/version.rb +5 -0
- data/lib/glim_ai.rb +8 -0
- data/lib/glim_ai_callable.rb +151 -0
- data/lib/glim_context.rb +62 -0
- data/lib/glim_helpers.rb +54 -0
- data/lib/glim_request.rb +266 -0
- data/lib/glim_response.rb +155 -0
- data/lib/globals.rb +255 -0
- data/lib/html_templates/chat_request.erb +86 -0
- data/sample.env +9 -0
- 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
|
data/lib/glim_context.rb
ADDED
@@ -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
|
+
|
data/lib/glim_helpers.rb
ADDED
@@ -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
|
+
|
data/lib/glim_request.rb
ADDED
@@ -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
|
+
|