llmed 0.1.6 → 0.1.10

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/exe/llmed +5 -1
  3. data/lib/llm.rb +66 -0
  4. data/lib/llmed.rb +86 -59
  5. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d712d49a56c4e6c1a8da617cf220acc7b4b6c91ffeebb36f6ca4f1fd57ec765
4
- data.tar.gz: f2a071cd093cb592f6165f60bce6c7c9760a796a1cd3ee72bfea1eb1c5675587
3
+ metadata.gz: 44c211eda3efc57a664ab947ebd9f8a89d7b1baf5fd320d78f54eee9f742dbcc
4
+ data.tar.gz: c5659d561c6e5e6fa2a62e8a96bfb959b9b8ab017de8c5f36a482461e16b4434
5
5
  SHA512:
6
- metadata.gz: c9bb361202bfdf31ef240b336cbfaa8eeb33093ac3e67803b1b0c17610415c1799d6a877175df176a7bdf0e171b8bebd0f753c510ab70923408df7a07bfa8eb3
7
- data.tar.gz: 07bf9ee01e90793146ed776edfb6ed29b71120ba5ea84d4853d59a38b9c324f777e3b0a5f187d8aef6b1b40a22e3162182ec4568f8c868e9db08252208603f7a
6
+ metadata.gz: f095496d76a624e17f8fa32c055bff45202c4f62a67acf2529a93873d6c03c1c6a7345ea6c3323dcf065c93ce4d056b5e2e4821ef165267ed9074526b383d3f9
7
+ data.tar.gz: 146a2b89ff296bf65deb7f837ed1348fe26543d709e0a1fb892cb7743ac5f22f8932fa254e38448576838de8b31de29d1accd5b44f9e4da7ec719819a6859e21
data/exe/llmed CHANGED
@@ -4,7 +4,7 @@
4
4
  require 'optparse'
5
5
  require 'llmed'
6
6
 
7
- logger = Logger.new(STDOUT)
7
+ logger = Logger.new(STDERR)
8
8
  output_dir = './llmed-out'
9
9
  release_dir = output_dir
10
10
  template = <<-TMP
@@ -41,6 +41,10 @@ OptionParser.new do |parser|
41
41
  parser.on('--release-dir DIR', String) do |path|
42
42
  release_dir = path
43
43
  end
44
+
45
+ parser.on('-q', '--quiet') do
46
+ logger.level = :error
47
+ end
44
48
  end.parse!
45
49
 
46
50
  source_code = ARGF.read
data/lib/llm.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'openai'
2
+ require 'langchain'
3
+
4
+ Langchain.logger.level = Logger::ERROR
5
+
6
+ class LLMed
7
+ module LLM
8
+ module Message
9
+ System = Struct.new(:content)
10
+ User = Struct.new(:content)
11
+ end
12
+
13
+ module Template
14
+ def self.build(template:, input_variables:)
15
+ Langchain::Prompt::PromptTemplate.new(template: template, input_variables: input_variables)
16
+ end
17
+ end
18
+
19
+ class Response
20
+ def initialize(response, tokens)
21
+ @response = response
22
+ @tokens = tokens
23
+ end
24
+
25
+ def source_code
26
+ @response
27
+ end
28
+
29
+ def total_tokens
30
+ @tokens
31
+ end
32
+ end
33
+
34
+ class OpenAI
35
+ def initialize(**args)
36
+ @llm = Langchain::LLM::OpenAI.new(**args)
37
+ end
38
+
39
+ def chat(messages: [])
40
+ messages = messages.map do |m|
41
+ case m
42
+ when Message::System
43
+ { role: 'system', content: m.content }
44
+ when Message::User
45
+ { role: 'user', content: m.content }
46
+ end
47
+ end
48
+
49
+ llm_response = @llm.chat(messages: messages)
50
+ Response.new(llm_response.chat_completion, llm_response.total_tokens)
51
+ end
52
+ end
53
+
54
+ class Test
55
+ def initialize
56
+ @output = ''
57
+ end
58
+
59
+ def chat(messages: [])
60
+ @output = messages.map { |m| m[:content] }.join("\n")
61
+
62
+ Response.new(@output, 0)
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/llmed.rb CHANGED
@@ -2,14 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pp'
5
- require 'openai'
6
- require 'langchain'
5
+ require 'csv'
7
6
  require 'pathname'
8
7
  require 'fileutils'
9
8
  require 'forwardable'
10
9
 
11
- Langchain.logger.level = Logger::ERROR
12
-
13
10
  class LLMed
14
11
  extend Forwardable
15
12
 
@@ -34,7 +31,13 @@ class LLMed
34
31
  end
35
32
 
36
33
  def message?
37
- not (@message.nil? || @message.empty?)
34
+ !(@message.nil? || @message.empty?)
35
+ end
36
+
37
+ # Example:
38
+ # context("files") { sh "ls /etc" }
39
+ def sh(cmd)
40
+ `#{cmd}`
38
41
  end
39
42
 
40
43
  # Example:
@@ -53,7 +56,7 @@ class LLMed
53
56
 
54
57
  class Configuration
55
58
  def initialize
56
- @prompt = Langchain::Prompt::PromptTemplate.new(template: "
59
+ @prompt = LLMed::LLM::Template.build(template: "
57
60
  You are a software developer and only have knowledge of the programming language {language}.
58
61
  Your response must contain only the generated source code, with no additional text.
59
62
  All source code must be written in a single file, and you must ensure it runs correctly on the first attempt.
@@ -62,7 +65,7 @@ Always include the properly escaped comment: LLMED-COMPILED.
62
65
  You must only modify the following source code:
63
66
  {source_code}
64
67
 
65
- ", input_variables: ["language", "source_code"])
68
+ ", input_variables: %w[language source_code])
66
69
  end
67
70
 
68
71
  def prompt(language:, source_code:)
@@ -72,8 +75,11 @@ You must only modify the following source code:
72
75
  # Change the default prompt, input variables: language, source_code
73
76
  # Example:
74
77
  # set_prompt "my new prompt"
75
- def set_prompt(prompt)
76
- @prompt = Langchain::Prompt::PromptTemplate.new(template: prompt, input_variables: ["language", "source_code"])
78
+ def set_prompt(*arg, input_variables: %w[language source_code], **args)
79
+ input_variables = {} if args[:file]
80
+ prompt = File.read(args[:file]) if args[:file]
81
+ prompt ||= arg.first
82
+ @prompt = LLMed::LLM::Template.build(template: prompt, input_variables: input_variables)
77
83
  end
78
84
 
79
85
  # Set default language used for all applications.
@@ -91,19 +97,22 @@ You must only modify the following source code:
91
97
 
92
98
  def language(main)
93
99
  lang = main || @language
94
- raise "Please assign a language to the application or general with the function set_languag" if lang.nil?
100
+ raise 'Please assign a language to the application or general with the function set_languag' if lang.nil?
101
+
95
102
  lang
96
103
  end
97
104
 
98
- def llm()
105
+ def llm
99
106
  case @provider
100
107
  when :openai
101
- Langchain::LLM::OpenAI.new(
108
+ LLMed::LLM::OpenAI.new(
102
109
  api_key: @provider_api_key,
103
- default_options: { temperature: 0.7, chat_model: @provider_model}
110
+ default_options: { temperature: 0.7, chat_model: @provider_model }
104
111
  )
112
+ when :test
113
+ LLMed::LLM::Test.new
105
114
  when nil
106
- raise "Please set the provider with `set_llm(provider, api_key, model)`"
115
+ raise 'Please set the provider with `set_llm(provider, api_key, model)`'
107
116
  else
108
117
  raise "not implemented provider #{@provider}"
109
118
  end
@@ -114,7 +123,7 @@ You must only modify the following source code:
114
123
  attr_reader :contexts, :name, :language
115
124
 
116
125
  def initialize(name:, language:, output_file:, block:, logger:, release:)
117
- raise "required language" if language.nil?
126
+ raise 'required language' if language.nil?
118
127
 
119
128
  @name = name
120
129
  @output_file = output_file
@@ -128,30 +137,32 @@ You must only modify the following source code:
128
137
  def context(name, **opts, &block)
129
138
  ctx = Context.new(name: name, options: opts)
130
139
  output = ctx.instance_eval(&block)
131
- unless ctx.message?
132
- ctx.llm(output)
133
- end
140
+ ctx.llm(output) unless ctx.message?
134
141
 
135
142
  @contexts << ctx
136
143
  end
137
144
 
138
145
  def evaluate
139
- self.instance_eval(&@block)
146
+ instance_eval(&@block)
140
147
  end
141
148
 
142
149
  def source_code(output_dir, release_dir)
150
+ return unless @output_file.is_a?(String)
143
151
  return unless @release
144
- release_source_code = Pathname.new(release_dir) + "#{@output_file}.r#{@release}#{@language}"
152
+
153
+ release_source_code = Pathname.new(release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
154
+ release_main_source_code = Pathname.new(release_dir) + "#{@output_file}.release"
145
155
  output_file = Pathname.new(output_dir) + @output_file
146
- if @release and not File.exist?(release_source_code)
156
+ if @release && !File.exist?(release_source_code)
147
157
  FileUtils.cp(output_file, release_source_code)
158
+ FileUtils.cp(output_file, release_main_source_code)
148
159
  @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
149
160
  end
150
-
151
- return File.read(release_source_code)
161
+ @logger.info("APPLICATION #{@name} INPUT RELEASE FILE #{release_main_source_code}")
162
+ File.read(release_main_source_code)
152
163
  end
153
164
 
154
- def output_file(output_dir, mode = 'w')
165
+ def output_file(output_dir, mode = 'w', &block)
155
166
  if @output_file.respond_to? :write
156
167
  yield @output_file
157
168
  else
@@ -160,21 +171,31 @@ You must only modify the following source code:
160
171
 
161
172
  @logger.info("APPLICATION #{@name} OUTPUT FILE #{path}")
162
173
 
163
- File.open(path, mode) do |file|
164
- yield file
165
- end
174
+ File.open(path, mode, &block)
166
175
  end
167
176
  end
177
+
178
+ def write_statistics(release_dir, total_tokens)
179
+ return unless @output_file.is_a?(String)
180
+
181
+ statistics_file = Pathname.new(release_dir) + "#{@output_file}.statistics"
182
+
183
+ File.open(statistics_file, 'a') do |file|
184
+ csv = CSV.new(file)
185
+ csv << [Time.now.to_i, @name, @release, total_tokens]
186
+ end
187
+ @logger.info("APPLICATION #{@name} WROTE STATISTICS FILE #{statistics_file}")
188
+ end
168
189
  end
169
190
 
170
191
  def initialize(logger:)
171
192
  @logger = logger
172
193
  @applications = []
173
- @configuration = Configuration.new()
194
+ @configuration = Configuration.new
174
195
  end
175
196
 
176
197
  def eval_source(code)
177
- self.instance_eval(code)
198
+ instance_eval(code)
178
199
  end
179
200
 
180
201
  # changes default language
@@ -184,38 +205,50 @@ You must only modify the following source code:
184
205
  # changes default prompt
185
206
  def_delegator :@configuration, :set_prompt, :set_prompt
186
207
 
187
- def application(name, language: nil, release: nil, output_file:, &block)
188
- @app = Application.new(name: name, language: @configuration.language(language), output_file: output_file, block: block, logger: @logger, release: release)
208
+ def application(name, output_file:, language: nil, release: nil, &block)
209
+ @app = Application.new(name: name, language: @configuration.language(language), output_file: output_file,
210
+ block: block, logger: @logger, release: release)
189
211
  @applications << @app
190
212
  end
191
213
 
192
214
  def compile(output_dir:, release_dir: nil)
193
- release_dir = output_dir unless release_dir
194
- @applications.each do |app|
195
- @logger.info("APPLICATION #{app.name} COMPILING")
196
-
197
- llm = @configuration.llm()
198
- system_content = @configuration.prompt(language: app.language, source_code: app.source_code(output_dir, release_dir))
199
- messages = [
200
- {role: "system", content: system_content},
201
- ]
202
- app.evaluate
203
- app.contexts.each do |ctx|
204
- next if ctx.skip?
205
- messages << {role: "user", content: ctx.message}
206
- end
215
+ @applications.each { |app| compile_application(app, output_dir, release_dir) }
216
+ end
217
+
218
+ private
219
+
220
+ def compile_application(app, output_dir, release_dir)
221
+ release_dir ||= output_dir
207
222
 
208
- llm_response = llm.chat(messages: messages)
209
- response = llm_response.chat_completion
210
- @logger.info("APPLICATION #{app.name} TOTAL TOKENS #{llm_response.total_tokens}")
211
- write_output(app, output_dir, source_code(response))
223
+ @logger.info("APPLICATION #{app.name} COMPILING")
224
+
225
+ llm = @configuration.llm
226
+ system_content = @configuration.prompt(language: app.language,
227
+ source_code: app.source_code(
228
+ output_dir, release_dir
229
+ ))
230
+ messages = [LLMed::LLM::Message::System.new(system_content)]
231
+ app.evaluate
232
+ app.contexts.each do |ctx|
233
+ next if ctx.skip?
234
+
235
+ messages << LLMed::LLM::Message::User.new(ctx.message)
212
236
  end
237
+
238
+ llm_response = llm.chat(messages: messages)
239
+ response = llm_response.source_code
240
+ @logger.info("APPLICATION #{app.name} TOTAL TOKENS #{llm_response.total_tokens}")
241
+ write_output(app, output_dir, source_code(response))
242
+ write_statistics(app, release_dir, llm_response)
213
243
  end
214
244
 
215
- private
216
245
  def source_code(content)
217
246
  # TODO: by provider?
218
- content.gsub('```', '').sub(/^(ruby|python(\d*)|elixir|c(pp)?|perl|bash)/, '')
247
+ content.gsub('```', '').sub(/^(node(js)?|javascript|ruby|python(\d*)|elixir|perl|bash|c(pp)?)/, '')
248
+ end
249
+
250
+ def write_statistics(app, release_dir, response)
251
+ app.write_statistics(release_dir, response.total_tokens)
219
252
  end
220
253
 
221
254
  def write_output(app, output_dir, output)
@@ -223,12 +256,6 @@ You must only modify the following source code:
223
256
  file.write(output)
224
257
  end
225
258
  end
226
-
227
- def edit_same_source_code(app, output_dir, messages)
228
- content = ""
229
- app.output_file(output_dir, 'r') do |file|
230
- content = "Codigo fuente a modificar: #{file.read()}"
231
- end
232
- messages << {role: "user", content: content}
233
- end
234
259
  end
260
+
261
+ require_relative 'llm'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jovany Leandro G.C
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-04 00:00:00.000000000 Z
11
+ date: 2025-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: langchainrb
@@ -76,6 +76,7 @@ extensions: []
76
76
  extra_rdoc_files: []
77
77
  files:
78
78
  - exe/llmed
79
+ - lib/llm.rb
79
80
  - lib/llmed.rb
80
81
  homepage: https://github.com/bit4bit/llm-labs/tree/main/llmed
81
82
  licenses: