llmed 0.2.2 → 0.2.4

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.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # LLMED
2
+
3
+ LLM Execution Development.
4
+
5
+ Concepts:
6
+ * Source Code = This (there is not name yet)
7
+ * Application = Legacy Source Code
8
+ * Compiler = LLM
9
+
10
+ What would happen if:
11
+ * Source code becomes just an opaque resource for being executed.
12
+ * If we express the context of the solution (compile the idea).
13
+
14
+ In classic terms the LLM is the Compiler, Source Code is the Binary, the Programming language is Context Description.
15
+
16
+ ```ruby
17
+ set_llm provider: :openai, api_key: ENV['OPENAI_API_KEY'], model: 'gpt-4o-mini'
18
+
19
+ application "hola mundo ruby", language: :ruby, output_file: "holamundo-ruby.ollmed" do
20
+ # More stable context: if this changes, all subsequent context will be recompiled.
21
+ context "variables" do
22
+ <<-LLM
23
+ Variable saludos de valor 'hola mundo'.
24
+ LLM
25
+ end
26
+
27
+ # More inestable context: if this changes, only this context will be recompiled.
28
+ context "main" do
29
+ <<-LLM
30
+ Mostrar al usuario la variable saludos.
31
+ LLM
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Programming flow
37
+
38
+ * Cycle
39
+ * Edit application.
40
+ * Once you agree with the current state of the application, increase the value of the `release` attribute
41
+ * Commit the release file (.release) and the source code (.llmed).
42
+
43
+ # Usage
44
+
45
+ * `gem install llmed`
46
+ * or local user
47
+ * `gem install --user-install llmed`
48
+ * add to `PATH` the path `~/.local/share/gem/ruby/<RUBY VERSION example 3.0.1>/bin/`
49
+ * `llmed -t /tmp/demo.llmed`
50
+ * edit
51
+ * compile to legacy source code `llmed /tmp/demo.llmed`
52
+ * execute or compile the legacy source code.
53
+
54
+ # Usage Development
55
+
56
+ * `bundle3.1 install --path vendor/`
57
+ * `OPENAI_API_KEY=xxx rake llmed[examples/tictactoe.rb]`
58
+
59
+ # Interesting
60
+
61
+ * The same prompt and the same source code produce exactly the same source code, but if we change the prompt a little bit, the source code also changes a little bit. So we have almost a one-to-one relationship. Can the prompt be the source of truth?
62
+
63
+ # History
64
+
65
+ After doing a small project in OpenAI i just deleted the chat,
66
+ later i decide to add more features but it was not possible
67
+ because i did not have the "source code", so some questions hit me:
68
+ Why i need to spend time of my life fixing LLM trash?
69
+ What if i just compile the idea?
70
+ How can i study the idea of others?
71
+
72
+ So this project is for exploring this questions
data/exe/llmed CHANGED
@@ -52,6 +52,6 @@ if ARGF.respond_to?(:path)
52
52
  release_dir = File.dirname(ARGF.path)
53
53
  end
54
54
 
55
- llmed = LLMed.new(logger: logger)
55
+ llmed = LLMed.new(logger: logger, output_dir: output_dir, release_dir: release_dir)
56
56
  llmed.eval_source source_code
57
- llmed.compile(output_dir: output_dir, release_dir: release_dir)
57
+ llmed.compile()
data/lib/llm.rb CHANGED
@@ -46,7 +46,7 @@ class LLMed
46
46
  private
47
47
 
48
48
  def source_code(content)
49
- content.gsub('```', '').sub(/^(node(js)?|javascript|ruby|python(\d*)|elixir|perl|bash|c(pp)?)/, '')
49
+ content.gsub('```', '').sub(/^(node(js)?|javascript|ruby|python(\d*)|elixir|perl|bash|html|c(pp)?)/, '')
50
50
  end
51
51
  end
52
52
 
data/lib/llm.rb~ ADDED
File without changes
@@ -0,0 +1,187 @@
1
+ # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
+ # frozen_string_literal: true
3
+
4
+ class LLMed
5
+ class Application
6
+ attr_reader :contexts, :name, :language
7
+
8
+ def initialize(name:, language:, output_file:, block:, logger:, release:, release_dir:, output_dir:)
9
+ raise 'required language' if language.nil?
10
+
11
+ @name = name
12
+ @output_file = output_file
13
+ @language = language
14
+ @block = block
15
+ @contexts = []
16
+ @logger = logger
17
+ @release = release
18
+ @release_dir = release_dir
19
+ @output_dir = output_dir
20
+ end
21
+
22
+ # Example:
23
+ # application { context "demo" { "content" } }
24
+ def context(name, **opts, &block)
25
+ opts[:release_dir] = @release_dir
26
+ ctx = Context.new(name: name, options: opts)
27
+ output = ctx.instance_eval(&block)
28
+ ctx.llm(output) unless ctx.message?
29
+
30
+ @contexts << ctx
31
+ end
32
+
33
+ def evaluate
34
+ instance_eval(&@block)
35
+ end
36
+
37
+ def prepare
38
+ return unless @output_file.is_a?(String)
39
+ return unless @release
40
+
41
+ output_file = Pathname.new(@output_dir) + @output_file
42
+ if @release && !File.exist?(release_source_code)
43
+ FileUtils.cp(output_file, release_source_code)
44
+ FileUtils.cp(output_file, release_main_source_code)
45
+ @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
46
+ end
47
+ @logger.info("APPLICATION #{@name} INPUT RELEASE FILE #{release_main_source_code}")
48
+ end
49
+
50
+ def source_code
51
+ return unless File.exist?(release_source_code)
52
+
53
+ File.read(release_source_code)
54
+ end
55
+
56
+ def output_file(output_dir, mode = 'w', &block)
57
+ if @output_file.respond_to? :write
58
+ yield @output_file
59
+ else
60
+ path = Pathname.new(output_dir) + @output_file
61
+ FileUtils.mkdir_p(File.dirname(path))
62
+
63
+ @logger.info("APPLICATION #{@name} OUTPUT FILE #{path}")
64
+
65
+ File.open(path, mode, &block)
66
+ end
67
+ end
68
+
69
+ def patch_or_create(output)
70
+ output_content = output
71
+
72
+ if @release && File.exist?(release_source_code)
73
+ release_source_code_content = File.read(release_source_code)
74
+ output_contexts = output.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'>(.+?)</llmed-code>}im)
75
+
76
+ output_contexts.each do |match|
77
+ name, digest, new_code = match
78
+ new_digest = digest
79
+ @contexts.each do |ctx|
80
+ if ctx.name == name
81
+ new_digest = ctx.digest
82
+ break
83
+ end
84
+ end
85
+
86
+ @logger.info("APPLICATION #{@name} PATCHING CONTEXT #{name} \n\tFROM #{digest}\n\tTO DIGEST #{new_digest}")
87
+ release_source_code_content = release_source_code_content.sub(%r{(.*?)(<llmed-code context='#{name}' digest='.*?'>)(.+?)(</llmed-code>)(.*?)}m) do
88
+ "#{::Regexp.last_match(1)}<llmed-code context='#{name}' digest='#{new_digest}'>#{new_code}#{::Regexp.last_match(4)}#{::Regexp.last_match(5)}"
89
+ end
90
+ end
91
+
92
+ output_content = release_source_code_content
93
+ end
94
+
95
+ output_file(@output_dir) do |file|
96
+ file.write(output_content)
97
+ end
98
+ end
99
+
100
+ def system_prompt(configuration)
101
+ configuration.prompt(language: language,
102
+ source_code: source_code,
103
+ update_context_digests: digests_of_context_to_update)
104
+ end
105
+
106
+ def rebuild?
107
+ return true unless @release
108
+
109
+ !digests_of_context_to_update.tap do |digests|
110
+ digests.each do |digest|
111
+ context_by_digest = release_contexts.invert
112
+ @logger.info("APPLICATION #{@name} REBUILDING CONTEXT #{context_by_digest[digest]}")
113
+ end
114
+ end.empty?
115
+ end
116
+
117
+ def write_statistics(response)
118
+ return unless @output_file.is_a?(String)
119
+
120
+ statistics_file = Pathname.new(@release_dir) + "#{@output_file}.statistics"
121
+
122
+ File.open(statistics_file, 'a') do |file|
123
+ stat = {
124
+ inserted_at: Time.now.to_i,
125
+ name: @name,
126
+ provider: response.provider,
127
+ model: response.model,
128
+ release: @release,
129
+ total_tokens: response.total_tokens,
130
+ duration_seconds: response.duration_seconds
131
+ }
132
+ file.puts stat.to_json
133
+ end
134
+ @logger.info("APPLICATION #{@name} WROTE STATISTICS FILE #{statistics_file}")
135
+ end
136
+
137
+ def notify(message)
138
+ Notify.notify("APPLICATION #{@name}", message)
139
+ end
140
+
141
+ private
142
+
143
+ def digests_of_context_to_update
144
+ update_context_digest = []
145
+
146
+ unless release_contexts.empty?
147
+ # rebuild context from top to down
148
+ # we are expecting:
149
+ # - top the most stable concepts
150
+ # - buttom the most inestable concepts
151
+ update_rest = false
152
+ @contexts.each do |ctx|
153
+ release_context_digest = release_contexts[ctx.name]
154
+ # maybe the context is not connected to the source code
155
+ next if release_context_digest.nil?
156
+
157
+ if update_rest
158
+ update_context_digest << release_context_digest
159
+ next
160
+ end
161
+ next if ctx.same_digest?(release_context_digest)
162
+
163
+ update_rest = true
164
+ update_context_digest << release_context_digest
165
+ end
166
+ end
167
+
168
+ update_context_digest
169
+ end
170
+
171
+ def release_source_code
172
+ Pathname.new(@release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
173
+ end
174
+
175
+ def release_main_source_code
176
+ Pathname.new(@release_dir) + "#{@output_file}.release"
177
+ end
178
+
179
+ def release_contexts
180
+ return {} unless @release
181
+
182
+ return {} unless File.exist?(release_source_code)
183
+
184
+ File.read(release_source_code).scan(/context='(.+)' digest='(.+)'/).to_h
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,182 @@
1
+ # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
+ # frozen_string_literal: true
3
+
4
+ class LLMed
5
+ class Application
6
+ attr_reader :contexts, :name, :language
7
+
8
+ def initialize(name:, language:, output_file:, block:, logger:, release:, release_dir:, output_dir:)
9
+ raise 'required language' if language.nil?
10
+
11
+ @name = name
12
+ @output_file = output_file
13
+ @language = language
14
+ @block = block
15
+ @contexts = []
16
+ @logger = logger
17
+ @release = release
18
+ @release_dir = release_dir
19
+ @output_dir = output_dir
20
+ end
21
+
22
+ # Example:
23
+ # application { context "demo" { "content" } }
24
+ def context(name, **opts, &block)
25
+ opts[:release_dir] = @release_dir
26
+ ctx = Context.new(name: name, options: opts)
27
+ output = ctx.instance_eval(&block)
28
+ ctx.llm(output) unless ctx.message?
29
+
30
+ @contexts << ctx
31
+ end
32
+
33
+ def evaluate
34
+ instance_eval(&@block)
35
+ end
36
+
37
+ def source_code
38
+ return unless @output_file.is_a?(String)
39
+ return unless @release
40
+
41
+ output_file = Pathname.new(@output_dir) + @output_file
42
+ if @release && !File.exist?(release_source_code)
43
+ FileUtils.cp(output_file, release_source_code)
44
+ FileUtils.cp(output_file, release_main_source_code)
45
+ @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
46
+ end
47
+ @logger.info("APPLICATION #{@name} INPUT RELEASE FILE #{release_main_source_code}")
48
+ File.read(release_source_code)
49
+ end
50
+
51
+ def output_file(output_dir, mode = 'w', &block)
52
+ if @output_file.respond_to? :write
53
+ yield @output_file
54
+ else
55
+ path = Pathname.new(output_dir) + @output_file
56
+ FileUtils.mkdir_p(File.dirname(path))
57
+
58
+ @logger.info("APPLICATION #{@name} OUTPUT FILE #{path}")
59
+
60
+ File.open(path, mode, &block)
61
+ end
62
+ end
63
+
64
+ def patch_or_create(output)
65
+ release_source_code_path = Pathname.new(@release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
66
+ output_content = output
67
+ if @release && File.exist?(release_source_code_path)
68
+ release_source_code = File.read(release_source_code_path)
69
+ output_contexts = output.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'>(.+?)</llmed-code>}im)
70
+
71
+ output_contexts.each do |match|
72
+ name, digest, new_code = match
73
+ new_digest = digest
74
+ @contexts.each do |ctx|
75
+ if ctx.name == name
76
+ new_digest = ctx.digest
77
+ break
78
+ end
79
+ end
80
+
81
+ @logger.info("APPLICATION #{@name} PATCHING CONTEXT #{name} \n\tFROM #{digest}\n\tTO DIGEST #{new_digest}")
82
+ release_source_code = release_source_code.sub(%r{(.*?)(<llmed-code context='#{name}' digest='.*?'>)(.+?)(</llmed-code>)(.*?)}m) do
83
+ "#{::Regexp.last_match(1)}<llmed-code context='#{name}' digest='#{new_digest}'>#{new_code}#{::Regexp.last_match(4)}#{::Regexp.last_match(5)}"
84
+ end
85
+ end
86
+
87
+ output_content = release_source_code
88
+ end
89
+
90
+ output_file(@output_dir) do |file|
91
+ file.write(output_content)
92
+ end
93
+ end
94
+
95
+ def system_prompt(configuration)
96
+ configuration.prompt(language: language,
97
+ source_code: source_code,
98
+ update_context_digests: digests_of_context_to_update)
99
+ end
100
+
101
+ def rebuild?
102
+ return true unless @release
103
+
104
+ !digests_of_context_to_update.tap do |digests|
105
+ digests.each do |digest|
106
+ context_by_digest = release_contexts.invert
107
+ @logger.info("APPLICATION #{@name} REBUILDING CONTEXT #{context_by_digest[digest]}")
108
+ end
109
+ end.empty?
110
+ end
111
+
112
+ def write_statistics(response)
113
+ return unless @output_file.is_a?(String)
114
+
115
+ statistics_file = Pathname.new(@release_dir) + "#{@output_file}.statistics"
116
+
117
+ File.open(statistics_file, 'a') do |file|
118
+ stat = {
119
+ inserted_at: Time.now.to_i,
120
+ name: @name,
121
+ provider: response.provider,
122
+ model: response.model,
123
+ release: @release,
124
+ total_tokens: response.total_tokens,
125
+ duration_seconds: response.duration_seconds
126
+ }
127
+ file.puts stat.to_json
128
+ end
129
+ @logger.info("APPLICATION #{@name} WROTE STATISTICS FILE #{statistics_file}")
130
+ end
131
+
132
+ def notify(message)
133
+ Notify.notify("APPLICATION #{@name}", message)
134
+ end
135
+
136
+ private
137
+
138
+ def digests_of_context_to_update
139
+ update_context_digest = []
140
+
141
+ unless release_contexts.empty?
142
+ # rebuild context from top to down
143
+ # we are expecting:
144
+ # - top the most stable concepts
145
+ # - buttom the most inestable concepts
146
+ update_rest = false
147
+ @contexts.each do |ctx|
148
+ release_context_digest = release_contexts[ctx.name]
149
+ # maybe the context is not connected to the source code
150
+ next if release_context_digest.nil?
151
+
152
+ if update_rest
153
+ update_context_digest << release_context_digest
154
+ next
155
+ end
156
+ next if ctx.same_digest?(release_context_digest)
157
+
158
+ update_rest = true
159
+ update_context_digest << release_context_digest
160
+ end
161
+ end
162
+
163
+ update_context_digest
164
+ end
165
+
166
+ def release_source_code
167
+ Pathname.new(@release_dir) + "#{@output_file}.r#{@release}#{@language}.cache"
168
+ end
169
+
170
+ def release_main_source_code
171
+ Pathname.new(@release_dir) + "#{@output_file}.release"
172
+ end
173
+
174
+ def release_contexts
175
+ return {} unless @release
176
+
177
+ return {} unless File.exist?(release_source_code)
178
+
179
+ File.read(release_source_code).scan(/context='(.+)' digest='(.+)'/).to_h
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,77 @@
1
+ # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
+ # frozen_string_literal: true
3
+
4
+ class LLMed
5
+ class Configuration
6
+ def initialize
7
+ @prompt = LLMed::LLM::Template.build(template: "
8
+ You are a software developer with knowledge only of the programming language {language}. Follow the SOLID principles strictly, you must use only imperative and functional programming, and design highly isolated components.
9
+ Your response must contain only the generated source code, with no additional text.
10
+ All source code must be written in a single file, and you must ensure it runs correctly on the first attempt.
11
+ Always include the properly escaped comment: LLMED-COMPILED.
12
+
13
+ You must only modify the following source code:
14
+ {source_code}
15
+
16
+ Only generate source code of the context who digest belongs to {update_context_digests}.
17
+
18
+ Wrap with comment every code that belongs to the indicated context, example in ruby:
19
+ #<llmed-code context='context name' digest='....'>
20
+ ...
21
+ #</llmed-code>
22
+
23
+ ", input_variables: %w[language source_code update_context_digests])
24
+ end
25
+
26
+ def prompt(language:, source_code:, update_context_digests: [])
27
+ @prompt.format(language: language, source_code: source_code,
28
+ update_context_digests: update_context_digests.join(','))
29
+ end
30
+
31
+ # Change the default prompt, input variables: language, source_code
32
+ # Example:
33
+ # set_prompt "my new prompt"
34
+ def set_prompt(*arg, input_variables: %w[language source_code], **args)
35
+ input_variables = {} if args[:file]
36
+ prompt = File.read(args[:file]) if args[:file]
37
+ prompt ||= arg.first
38
+ @prompt = LLMed::LLM::Template.build(template: prompt, input_variables: input_variables)
39
+ end
40
+
41
+ # Set default language used for all applications.
42
+ # Example:
43
+ # set_langugage :ruby
44
+ def set_language(language)
45
+ @language = language
46
+ end
47
+
48
+ def set_llm(provider:, api_key:, model:)
49
+ @provider = provider
50
+ @provider_api_key = api_key
51
+ @provider_model = model
52
+ end
53
+
54
+ def language(main)
55
+ lang = main || @language
56
+ raise 'Please assign a language to the application or general with the function set_languag' if lang.nil?
57
+
58
+ lang
59
+ end
60
+
61
+ def llm
62
+ case @provider
63
+ when :openai
64
+ LLMed::LLM::OpenAI.new(
65
+ api_key: @provider_api_key,
66
+ default_options: { temperature: 0.7, chat_model: @provider_model }
67
+ )
68
+ when :test
69
+ LLMed::LLM::Test.new
70
+ when nil
71
+ raise 'Please set the provider with `set_llm(provider, api_key, model)`'
72
+ else
73
+ raise "not implemented provider #{@provider}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,76 @@
1
+ class LLMed
2
+ module LLM
3
+ class Configuration
4
+ def initialize
5
+ @prompt = LLMed::LLM::Template.build(template: "
6
+ You are a software developer with knowledge only of the programming language {language}. Follow the SOLID principles strictly, you must use only imperative and functional programming, and design highly isolated components.
7
+ Your response must contain only the generated source code, with no additional text.
8
+ All source code must be written in a single file, and you must ensure it runs correctly on the first attempt.
9
+ Always include the properly escaped comment: LLMED-COMPILED.
10
+
11
+ You must only modify the following source code:
12
+ {source_code}
13
+
14
+ Only generate source code of the context who digest belongs to {update_context_digests}.
15
+
16
+ Wrap with comment every code that belongs to the indicated context, example in ruby:
17
+ #<llmed-code context='context name' digest='....'>
18
+ ...
19
+ #</llmed-code>
20
+
21
+ ", input_variables: %w[language source_code update_context_digests])
22
+ end
23
+
24
+ def prompt(language:, source_code:, update_context_digests: [])
25
+ @prompt.format(language: language, source_code: source_code,
26
+ update_context_digests: update_context_digests.join(','))
27
+ end
28
+
29
+ # Change the default prompt, input variables: language, source_code
30
+ # Example:
31
+ # set_prompt "my new prompt"
32
+ def set_prompt(*arg, input_variables: %w[language source_code], **args)
33
+ input_variables = {} if args[:file]
34
+ prompt = File.read(args[:file]) if args[:file]
35
+ prompt ||= arg.first
36
+ @prompt = LLMed::LLM::Template.build(template: prompt, input_variables: input_variables)
37
+ end
38
+
39
+ # Set default language used for all applications.
40
+ # Example:
41
+ # set_langugage :ruby
42
+ def set_language(language)
43
+ @language = language
44
+ end
45
+
46
+ def set_llm(provider:, api_key:, model:)
47
+ @provider = provider
48
+ @provider_api_key = api_key
49
+ @provider_model = model
50
+ end
51
+
52
+ def language(main)
53
+ lang = main || @language
54
+ raise 'Please assign a language to the application or general with the function set_languag' if lang.nil?
55
+
56
+ lang
57
+ end
58
+
59
+ def llm
60
+ case @provider
61
+ when :openai
62
+ LLMed::LLM::OpenAI.new(
63
+ api_key: @provider_api_key,
64
+ default_options: { temperature: 0.7, chat_model: @provider_model }
65
+ )
66
+ when :test
67
+ LLMed::LLM::Test.new
68
+ when nil
69
+ raise 'Please set the provider with `set_llm(provider, api_key, model)`'
70
+ else
71
+ raise "not implemented provider #{@provider}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,64 @@
1
+ # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
+ # frozen_string_literal: true
3
+
4
+ class LLMed
5
+ class Context
6
+ attr_reader :name
7
+
8
+ def initialize(name:, options: {})
9
+ @name = name
10
+ @skip = options[:skip] || false
11
+ @release_dir = options[:release_dir]
12
+ end
13
+
14
+ def skip?
15
+ @skip
16
+ end
17
+
18
+ def same_digest?(val)
19
+ digest == val
20
+ end
21
+
22
+ def digest
23
+ Digest::SHA256.hexdigest "#{@name}.#{@message}"
24
+ end
25
+
26
+ def message
27
+ "# Context: #{@name} Digest: #{digest}\n\n#{@message}"
28
+ end
29
+
30
+ def llm(message)
31
+ @message = message
32
+ end
33
+
34
+ def message?
35
+ !(@message.nil? || @message.empty?)
36
+ end
37
+
38
+ # Example:
39
+ # context("files") { sh "ls /etc" }
40
+ def sh(cmd)
41
+ `#{cmd}`
42
+ end
43
+
44
+ # Example:
45
+ # context("application") { from_file("application.cllmed") }
46
+ def from_file(path)
47
+ File.read(path)
48
+ end
49
+
50
+ # Example:
51
+ # context("source") { from_source_code("sourcepathtoinclude") }
52
+ def from_source_code(path)
53
+ code = File.read(path)
54
+ " Given the following source code: #{code}\n\n\n"
55
+ end
56
+
57
+ # Example:
58
+ # context("source") { from_release("file in release dir") }
59
+ def from_release(path)
60
+ code = File.read(Pathname.new(@release_dir) + path)
61
+ " Given the following source code: #{code}\n\n\n"
62
+ end
63
+ end
64
+ end