llmed 0.2.3 → 0.2.5

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.
@@ -0,0 +1,62 @@
1
+ # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
+ class LLMed
3
+ class Context
4
+ attr_reader :name
5
+
6
+ def initialize(name:, options: {})
7
+ @name = name
8
+ @skip = options[:skip] || false
9
+ @release_dir = options[:release_dir]
10
+ end
11
+
12
+ def skip?
13
+ @skip
14
+ end
15
+
16
+ def same_digest?(val)
17
+ digest == val
18
+ end
19
+
20
+ def digest
21
+ Digest::SHA256.hexdigest "#{@name}.#{@message}"
22
+ end
23
+
24
+ def message
25
+ "# Context: #{@name} Digest: #{digest}\n\n#{@message}"
26
+ end
27
+
28
+ def llm(message)
29
+ @message = message
30
+ end
31
+
32
+ def message?
33
+ !(@message.nil? || @message.empty?)
34
+ end
35
+
36
+ # Example:
37
+ # context("files") { sh "ls /etc" }
38
+ def sh(cmd)
39
+ `#{cmd}`
40
+ end
41
+
42
+ # Example:
43
+ # context("application") { from_file("application.cllmed") }
44
+ def from_file(path)
45
+ File.read(path)
46
+ end
47
+
48
+ # Example:
49
+ # context("source") { from_source_code("sourcepathtoinclude") }
50
+ def from_source_code(path)
51
+ code = File.read(path)
52
+ " Given the following source code: #{code}\n\n\n"
53
+ end
54
+
55
+ # Example:
56
+ # context("source") { from_release("file in release dir") }
57
+ def from_release(path)
58
+ code = File.read(Pathname.new(@release_dir) + path)
59
+ " Given the following source code: #{code}\n\n\n"
60
+ end
61
+ end
62
+ end
data/lib/llmed.rb CHANGED
@@ -13,300 +13,12 @@ require 'notify'
13
13
  class LLMed
14
14
  extend Forwardable
15
15
 
16
- class Context
17
- attr_reader :name
18
-
19
- def initialize(name:, options: {})
20
- @name = name
21
- @skip = options[:skip] || false
22
- end
23
-
24
- def skip?
25
- @skip
26
- end
27
-
28
- def same_digest?(val)
29
- digest == val
30
- end
31
-
32
- def digest
33
- Digest::SHA256.hexdigest "#{@name}.#{@message}"
34
- end
35
-
36
- def message
37
- "# Context: #{@name} Digest: #{digest}\n\n#{@message}"
38
- end
39
-
40
- def llm(message)
41
- @message = message
42
- end
43
-
44
- def message?
45
- !(@message.nil? || @message.empty?)
46
- end
47
-
48
- # Example:
49
- # context("files") { sh "ls /etc" }
50
- def sh(cmd)
51
- `#{cmd}`
52
- end
53
-
54
- # Example:
55
- # context("application") { from_file("application.cllmed") }
56
- def from_file(path)
57
- File.read(path)
58
- end
59
-
60
- # Example:
61
- # context("source") { from_source_code("sourcepathtoinclude") }
62
- def from_source_code(path)
63
- code = File.read(path)
64
- " Given the following source code: #{code}\n\n\n"
65
- end
66
- end
67
-
68
- class Configuration
69
- def initialize
70
- @prompt = LLMed::LLM::Template.build(template: "
71
- You are a software developer with knowledge only of the programming language {language}. Follow the SOLID principles strictly, use only imperative and functional programming, and design highly isolated components.
72
- Your response must contain only the generated source code, with no additional text.
73
- All source code must be written in a single file, and you must ensure it runs correctly on the first attempt.
74
- Always include the properly escaped comment: LLMED-COMPILED.
75
-
76
- You must only modify the following source code:
77
- {source_code}
78
-
79
- Only generate source code of the context who digest belongs to {update_context_digests}.
80
-
81
- Wrap with comment every code that belongs to the indicated context, example in ruby:
82
- #<llmed-code context='context name' digest='....'>
83
- ...
84
- #</llmed-code>
85
-
86
- ", input_variables: %w[language source_code update_context_digests])
87
- end
88
-
89
- def prompt(language:, source_code:, update_context_digests: [])
90
- @prompt.format(language: language, source_code: source_code,
91
- update_context_digests: update_context_digests.join(','))
92
- end
93
-
94
- # Change the default prompt, input variables: language, source_code
95
- # Example:
96
- # set_prompt "my new prompt"
97
- def set_prompt(*arg, input_variables: %w[language source_code], **args)
98
- input_variables = {} if args[:file]
99
- prompt = File.read(args[:file]) if args[:file]
100
- prompt ||= arg.first
101
- @prompt = LLMed::LLM::Template.build(template: prompt, input_variables: input_variables)
102
- end
103
-
104
- # Set default language used for all applications.
105
- # Example:
106
- # set_langugage :ruby
107
- def set_language(language)
108
- @language = language
109
- end
110
-
111
- def set_llm(provider:, api_key:, model:)
112
- @provider = provider
113
- @provider_api_key = api_key
114
- @provider_model = model
115
- end
116
-
117
- def language(main)
118
- lang = main || @language
119
- raise 'Please assign a language to the application or general with the function set_languag' if lang.nil?
120
-
121
- lang
122
- end
123
-
124
- def llm
125
- case @provider
126
- when :openai
127
- LLMed::LLM::OpenAI.new(
128
- api_key: @provider_api_key,
129
- default_options: { temperature: 0.7, chat_model: @provider_model }
130
- )
131
- when :test
132
- LLMed::LLM::Test.new
133
- when nil
134
- raise 'Please set the provider with `set_llm(provider, api_key, model)`'
135
- else
136
- raise "not implemented provider #{@provider}"
137
- end
138
- end
139
- end
140
-
141
- class Application
142
- attr_reader :contexts, :name, :language
143
-
144
- def initialize(name:, language:, output_file:, block:, logger:, release:)
145
- raise 'required language' if language.nil?
146
-
147
- @name = name
148
- @output_file = output_file
149
- @language = language
150
- @block = block
151
- @contexts = []
152
- @logger = logger
153
- @release = release
154
- end
155
-
156
- def context(name, **opts, &block)
157
- ctx = Context.new(name: name, options: opts)
158
- output = ctx.instance_eval(&block)
159
- ctx.llm(output) unless ctx.message?
160
-
161
- @contexts << ctx
162
- end
163
-
164
- def evaluate
165
- instance_eval(&@block)
166
- end
167
-
168
- def source_code(output_dir, release_dir)
169
- return unless @output_file.is_a?(String)
170
- return unless @release
171
-
172
- release_source_code = Pathname.new(release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
173
- release_main_source_code = Pathname.new(release_dir) + "#{@output_file}.release"
174
- output_file = Pathname.new(output_dir) + @output_file
175
- if @release && !File.exist?(release_source_code)
176
- FileUtils.cp(output_file, release_source_code)
177
- FileUtils.cp(output_file, release_main_source_code)
178
- @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
179
- end
180
- @logger.info("APPLICATION #{@name} INPUT RELEASE FILE #{release_main_source_code}")
181
- File.read(release_source_code)
182
- end
183
-
184
- def release_contexts(_output_dir, release_dir)
185
- return {} unless @release
186
-
187
- release_source_code = Pathname.new(release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
188
- return {} unless File.exist?(release_source_code)
189
-
190
- File.read(release_source_code).scan(/context='(.+)' digest='(.+)'/).to_h
191
- end
192
-
193
- def output_file(output_dir, mode = 'w', &block)
194
- if @output_file.respond_to? :write
195
- yield @output_file
196
- else
197
- path = Pathname.new(output_dir) + @output_file
198
- FileUtils.mkdir_p(File.dirname(path))
199
-
200
- @logger.info("APPLICATION #{@name} OUTPUT FILE #{path}")
201
-
202
- File.open(path, mode, &block)
203
- end
204
- end
205
-
206
- def patch_or_create(output_dir, release_dir, output)
207
- release_source_code_path = Pathname.new(release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
208
-
209
- if @release && File.exist?(release_source_code_path)
210
- release_source_code = File.read(release_source_code_path)
211
- output_contexts = output.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'>(.+?)</llmed-code>}im)
212
-
213
- output_contexts.each do |match|
214
- name, digest, new_code = match
215
- new_digest = digest
216
- @contexts.each do |ctx|
217
- if ctx.name == name
218
- new_digest = ctx.digest
219
- break
220
- end
221
- end
222
-
223
- @logger.info("APPLICATION #{@name} PATCHING CONTEXT #{name} \n\tFROM #{digest}\n\tTO DIGEST #{new_digest}")
224
- release_source_code = release_source_code.sub(%r{(.*?)(<llmed-code context='#{name}' digest='.*?'>)(.+?)(</llmed-code>)(.*?)}m) do
225
- "#{::Regexp.last_match(1)}<llmed-code context='#{name}' digest='#{new_digest}'>#{new_code}#{::Regexp.last_match(4)}#{::Regexp.last_match(5)}"
226
- end
227
- end
228
-
229
- output_file(output_dir) do |file|
230
- file.write(release_source_code)
231
- end
232
- else
233
- output_file(output_dir) do |file|
234
- file.write(output)
235
- end
236
- end
237
- end
238
-
239
- def digests_of_context_to_update(output_dir, release_dir)
240
- update_context_digest = []
241
- release_contexts = release_contexts(output_dir, release_dir)
242
-
243
- unless release_contexts.empty?
244
- # rebuild context from top to down
245
- # we are expecting:
246
- # - top the most stable concepts
247
- # - buttom the most inestable concepts
248
- update_rest = false
249
- @contexts.each do |ctx|
250
- release_context_digest = release_contexts[ctx.name]
251
- # maybe the context is not connected to the source code
252
- next if release_context_digest.nil?
253
-
254
- if update_rest
255
- update_context_digest << release_context_digest
256
- next
257
- end
258
- next if ctx.same_digest?(release_context_digest)
259
-
260
- update_rest = true
261
- update_context_digest << release_context_digest
262
- end
263
- end
264
-
265
- update_context_digest
266
- end
267
-
268
- def rebuild?(output_dir, release_dir)
269
- return true unless @release
270
-
271
- update_context_digest = digests_of_context_to_update(output_dir, release_dir)
272
- release_contexts = release_contexts(output_dir, release_dir)
273
- update_context_digest.each do |digest|
274
- context_by_digest = release_contexts.invert
275
- @logger.info("APPLICATION #{@name} REBUILDING CONTEXT #{context_by_digest[digest]}")
276
- end
277
-
278
- !update_context_digest.empty?
279
- end
280
-
281
- def write_statistics(release_dir, response)
282
- return unless @output_file.is_a?(String)
283
-
284
- statistics_file = Pathname.new(release_dir) + "#{@output_file}.statistics"
285
-
286
- File.open(statistics_file, 'a') do |file|
287
- stat = {
288
- inserted_at: Time.now.to_i,
289
- name: @name,
290
- provider: response.provider,
291
- model: response.model,
292
- release: @release,
293
- total_tokens: response.total_tokens,
294
- duration_seconds: response.duration_seconds
295
- }
296
- file.puts stat.to_json
297
- end
298
- @logger.info("APPLICATION #{@name} WROTE STATISTICS FILE #{statistics_file}")
299
- end
300
-
301
- def notify(message)
302
- Notify.notify("APPLICATION #{@name}", message)
303
- end
304
- end
305
-
306
- def initialize(logger:)
16
+ def initialize(logger:, output_dir:, release_dir:)
307
17
  @logger = logger
308
18
  @applications = []
309
19
  @configuration = Configuration.new
20
+ @release_dir = release_dir || output_dir
21
+ @output_dir = output_dir
310
22
  end
311
23
 
312
24
  def eval_source(code)
@@ -321,59 +33,54 @@ Wrap with comment every code that belongs to the indicated context, example in r
321
33
  def_delegator :@configuration, :set_prompt, :set_prompt
322
34
 
323
35
  def application(name, output_file:, language: nil, release: nil, &block)
324
- @app = Application.new(name: name, language: @configuration.language(language), output_file: output_file,
325
- block: block, logger: @logger, release: release)
36
+ @app = Application.new(
37
+ name: name,
38
+ language: @configuration.language(language),
39
+ output_file: output_file,
40
+ block: block,
41
+ logger: @logger,
42
+ release: release,
43
+ release_dir: @release_dir,
44
+ output_dir: @output_dir
45
+ )
326
46
  @applications << @app
327
47
  end
328
48
 
329
- def compile(output_dir:, release_dir: nil)
49
+ def compile
330
50
  @applications.each do |app|
331
- compile_application(app, output_dir, release_dir)
51
+ compile_application(app)
332
52
  end
333
53
  end
334
54
 
335
55
  private
336
56
 
337
- def compile_application(app, output_dir, release_dir)
338
- release_dir ||= output_dir
339
-
57
+ def compile_application(app)
340
58
  app.notify('COMPILE START')
341
59
  @logger.info("APPLICATION #{app.name} COMPILING")
342
60
 
343
- llm = @configuration.llm
344
-
61
+ app.prepare
345
62
  app.evaluate
63
+ if app.rebuild?
64
+ llm = @configuration.llm
65
+ messages = [LLMed::LLM::Message::System.new(app.system_prompt(@configuration))]
66
+ app.contexts.each do |ctx|
67
+ next if ctx.skip?
346
68
 
347
- system_content = @configuration.prompt(language: app.language,
348
- source_code: app.source_code(
349
- output_dir, release_dir
350
- ),
351
- update_context_digests: app.digests_of_context_to_update(output_dir,
352
- release_dir))
353
- messages = [LLMed::LLM::Message::System.new(system_content)]
354
- app.contexts.each do |ctx|
355
- next if ctx.skip?
69
+ messages << LLMed::LLM::Message::User.new(ctx.message)
70
+ end
356
71
 
357
- messages << LLMed::LLM::Message::User.new(ctx.message)
358
- end
359
- if app.rebuild?(output_dir, release_dir)
360
72
  llm_response = llm.chat(messages: messages)
361
73
  @logger.info("APPLICATION #{app.name} TOTAL TOKENS #{llm_response.total_tokens}")
362
- write_output(app, output_dir, release_dir, llm_response.source_code)
363
- write_statistics(app, release_dir, llm_response)
74
+ app.patch_or_create(llm_response.source_code)
75
+ app.write_statistics(llm_response)
364
76
  app.notify("COMPILE DONE #{llm_response.duration_seconds}")
365
77
  else
366
78
  @logger.info("APPLICATION #{app.name} NOT CHANGES DETECTED")
367
79
  end
368
80
  end
369
-
370
- def write_statistics(app, release_dir, response)
371
- app.write_statistics(release_dir, response)
372
- end
373
-
374
- def write_output(app, output_dir, release_dir, output)
375
- app.patch_or_create(output_dir, release_dir, output)
376
- end
377
81
  end
378
82
 
379
83
  require_relative 'llm'
84
+ require_relative 'llmed/configuration'
85
+ require_relative 'llmed/context'
86
+ require_relative 'llmed/application'
data/lib/llmed.rb~ ADDED
@@ -0,0 +1,190 @@
1
+ require 'pp'
2
+ require 'langchain'
3
+ require 'pathname'
4
+ require 'fileutils'
5
+ require 'forwardable'
6
+
7
+ Langchain.logger.level = Logger::ERROR
8
+
9
+ class LLMed
10
+ extend Forwardable
11
+
12
+ class Context
13
+ attr_reader :message, :name
14
+
15
+ def initialize(name:, options: {})
16
+ @name = name
17
+ @skip = options[:skip] || false
18
+ end
19
+
20
+ def skip?
21
+ @skip
22
+ end
23
+
24
+ def llm(message)
25
+ @message = message
26
+ end
27
+
28
+ def message?
29
+ not (@message.nil? || @message.empty?)
30
+ end
31
+
32
+
33
+ def from_file(path)
34
+ File.read(path)
35
+ end
36
+
37
+ def from_source_code(path)
38
+ code = File.read(path)
39
+ "Dado el codigo fuente: #{code}\n\n\n"
40
+ end
41
+ end
42
+
43
+ class Configuration
44
+ def initialize
45
+ @prompt = Langchain::Prompt::PromptTemplate.new(template: "
46
+ Eres desarrollador de software y solo conoces del lenguage de programacion {language}.
47
+ La respuesta no debe contener texto adicional al codigo fuente generado.
48
+ Todo el codigo fuente se genera en un unico archivo.
49
+ Siempre adicionas el comentario de codigo correctamente escapado LLMED-COMPILED.
50
+
51
+ ", input_variables: ["language"])
52
+ end
53
+
54
+ def prompt(language:)
55
+ @prompt.format(language: language)
56
+ end
57
+
58
+ def set_prompt(prompt)
59
+ @prompt = Langchain::Prompt::PromptTemplate.new(template: prompt, input_variables: ["language"])
60
+ end
61
+
62
+ def set_language(language)
63
+ @language = language
64
+ end
65
+
66
+ def set_llm(provider:, api_key:, model:)
67
+ @provider = provider
68
+ @provider_api_key = api_key
69
+ @provider_model = model
70
+ end
71
+
72
+ def language(main)
73
+ lang = main || @language
74
+ raise "Please assign a language to the application or general with the function set_languag" if lang.nil?
75
+ lang
76
+ end
77
+
78
+ def llm()
79
+ case @provider
80
+ when :openai
81
+ Langchain::LLM::OpenAI.new(
82
+ api_key: @provider_api_key,
83
+ default_options: { temperature: 0.7, chat_model: @provider_model}
84
+ )
85
+ when nil
86
+ raise "Please set the provider with `set_llm(provider, api_key, model)`"
87
+ else
88
+ raise "not implemented provider #{@provider}"
89
+ end
90
+ end
91
+ end
92
+
93
+ class Application
94
+ attr_reader :contexts, :name, :language
95
+
96
+ def initialize(name:, language:, output_file:, block:, logger:)
97
+ raise "required language" if language.nil?
98
+
99
+ @name = name
100
+ @output_file = output_file
101
+ @language = language
102
+ @block = block
103
+ @contexts = []
104
+ @logger = logger
105
+ end
106
+
107
+ def context(name, **opts, &block)
108
+ ctx = Context.new(name: name, options: opts)
109
+ output = ctx.instance_eval(&block)
110
+ unless ctx.message?
111
+ ctx.llm(output)
112
+ end
113
+
114
+ @contexts << ctx
115
+ end
116
+
117
+ def evaluate
118
+ self.instance_eval(&@block)
119
+ end
120
+
121
+ def output_file(output_dir)
122
+ if @output_file.respond_to? :write
123
+ yield @output_file
124
+ else
125
+ path = Pathname.new(output_dir) + @output_file
126
+ FileUtils.mkdir_p(File.dirname(path))
127
+
128
+ @logger.info("APPLICATION #{@name} OUTPUT FILE #{@output_file}")
129
+
130
+ File.open(path, 'w') do |file|
131
+ yield file
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def initialize(logger:)
138
+ @logger = logger
139
+ @applications = []
140
+ @configuration = Configuration.new()
141
+ end
142
+
143
+ def eval_source(code)
144
+ self.instance_eval(code)
145
+ end
146
+
147
+ # changes default language
148
+ def_delegator :@configuration, :set_language, :set_language
149
+ # changes default llm
150
+ def_delegator :@configuration, :set_llm, :set_llm
151
+ # changes default prompt
152
+ def_delegator :@configuration, :set_prompt, :set_prompt
153
+
154
+ def application(name, language: nil, output_file:, &block)
155
+ @app = Application.new(name: name, language: @configuration.language(language), output_file: output_file, block: block, logger: @logger)
156
+ @applications << @app
157
+ end
158
+
159
+ def compile(output_dir:)
160
+ @applications.each do |app|
161
+ llm = @configuration.llm()
162
+
163
+ messages = [
164
+ {role: "system", content: @configuration.prompt(language: app.language)},
165
+ ]
166
+ app.evaluate
167
+ app.contexts.each do |ctx|
168
+ next if ctx.skip?
169
+ messages << {role: "user", content: ctx.message}
170
+ end
171
+
172
+ llm_response = llm.chat(messages: messages)
173
+ response = llm_response.chat_completion
174
+ @logger.info("APPLICATION #{app.name} TOTAL TOKENS #{llm_response.total_tokens}")
175
+ write_output(app, output_dir, source_code(response))
176
+ end
177
+ end
178
+
179
+ private
180
+ def source_code(content)
181
+ # TODO: by provider?
182
+ content.gsub('```', '').sub(/^(ruby|python(\d*)|elixir|c(pp)?)/, '')
183
+ end
184
+
185
+ def write_output(app, output_dir, output)
186
+ app.output_file(output_dir) do |file|
187
+ file.write(output)
188
+ end
189
+ end
190
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jovany Leandro G.C
@@ -89,14 +89,25 @@ executables:
89
89
  extensions: []
90
90
  extra_rdoc_files: []
91
91
  files:
92
+ - LICENSE
93
+ - README.md
92
94
  - exe/llmed
93
95
  - lib/llm.rb
96
+ - lib/llm.rb~
94
97
  - lib/llmed.rb
98
+ - lib/llmed.rb~
99
+ - lib/llmed/application.rb
100
+ - lib/llmed/application.rb~
101
+ - lib/llmed/configuration.rb
102
+ - lib/llmed/configuration.rb~
103
+ - lib/llmed/context.rb
104
+ - lib/llmed/context.rb~
95
105
  homepage: https://github.com/bit4bit/llmed
96
106
  licenses:
97
107
  - GPL-3.0
98
108
  metadata:
99
109
  source_code_uri: https://github.com/bit4bit/llmed
110
+ allowed_push_host: https://rubygems.org
100
111
  post_install_message:
101
112
  rdoc_options: []
102
113
  require_paths:
@@ -105,12 +116,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
116
  requirements:
106
117
  - - ">="
107
118
  - !ruby/object:Gem::Version
108
- version: '0'
119
+ version: 3.0.0
109
120
  required_rubygems_version: !ruby/object:Gem::Requirement
110
121
  requirements:
111
122
  - - ">="
112
123
  - !ruby/object:Gem::Version
113
- version: '0'
124
+ version: 1.3.7
114
125
  requirements: []
115
126
  rubygems_version: 3.3.15
116
127
  signing_key: