llmed 0.4.3 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5733bac9f8a6d8c5465f487bda233c8997491d838c778b45194e3c15f9c14734
4
- data.tar.gz: e606c3e377bc6d0bf75eed59dd6456773c02650e98118a0135814808195e2fb8
3
+ metadata.gz: c47ebb44059bae3a1ffc2d27dd064098036ebd390cb677a488761845e13d4b6b
4
+ data.tar.gz: 1442dec8d3c594b8610af95d7edfbdbdee8881ac00a046001cc6fd4eb9c2e68c
5
5
  SHA512:
6
- metadata.gz: 3fc77a1b7b7ca6ba6c8bcb528e1ef702277588195c47e538cfa6a8432aef4ae7cb876b10537605866bfa7ea0a61ceacf1308cd52d3b5f0f2a7a4f51ef5b552ed
7
- data.tar.gz: 94f66750f04e2c97d8e78125ac920c52037fd6bc5e5759cbd2e349402be599619fb5cb2dc92af0c519a0cc557333be022925abb556f09995460a0b0d936386c8
6
+ metadata.gz: 80a7c93eed1106305b848bae99b0fcce744c55bc933538465041e19329ed1e6fe01f398c38ec849f2ae4981ec260024d3a9ded96f1465224a0b9f60cc8924b53
7
+ data.tar.gz: 7c3df54bec3ee2e2f02bd7108c92004e6969bb8019ab5209bda32327cc916492ac2946948b2ce835325b93d33238292187870e68bc35bfa50feefb0b38164263
data/lib/llm.rb CHANGED
@@ -56,7 +56,7 @@ class LLMed
56
56
  @logger.warn("POSSIBLE INCONSISTENCY COMPLETED TOKENS REACHED MAX TOKENS #{MAX_TOKENS}")
57
57
  end
58
58
  end
59
-
59
+
60
60
  def llm_arguments(args)
61
61
  args
62
62
  end
@@ -1,6 +1,8 @@
1
1
  # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'set'
5
+
4
6
  class LLMed
5
7
  class Application
6
8
  attr_reader :contexts, :name, :language
@@ -29,6 +31,8 @@ class LLMed
29
31
  end
30
32
 
31
33
  def initialize(name:, language:, output_file:, block:, logger:, release:, release_dir:, output_dir:)
34
+ snapshot_file = Pathname.new(release_dir) + "#{output_file}.snapshot"
35
+
32
36
  @name = name
33
37
  @output_file = output_file
34
38
  @language = language.to_sym
@@ -39,6 +43,7 @@ class LLMed
39
43
  @release = release
40
44
  @release_dir = release_dir
41
45
  @output_dir = output_dir
46
+ @snapshot = Snapshot.new(snapshot_file)
42
47
  end
43
48
 
44
49
  # Example:
@@ -56,7 +61,113 @@ class LLMed
56
61
  instance_eval(&@block)
57
62
  end
58
63
 
59
- def prepare
64
+ class Snapshot
65
+ attr_reader :snapshot_file
66
+
67
+ def initialize(snapshot_file)
68
+ @snapshot_file = snapshot_file
69
+ @contexts = []
70
+ end
71
+
72
+ def sync(default)
73
+ load(default)
74
+ dump
75
+ end
76
+
77
+ def refresh(contexts)
78
+ @contexts = contexts.map{ |ctx| [ctx.name, {'name' => ctx.name, 'message' => ctx.raw}]}.to_h
79
+ dump
80
+ end
81
+
82
+ def diff(other_contexts)
83
+ diffs = {}
84
+ other_contexts.each do |other_ctx|
85
+ current_ctx = @contexts[other_ctx.name]
86
+ result = line_diff(current_ctx['message'], other_ctx.raw)
87
+ # omit not changes
88
+ if !result.all?{|op, line| op == '=:'}
89
+ diffs[other_ctx.name] = result
90
+ end
91
+ end
92
+
93
+ diffs
94
+ end
95
+
96
+ private
97
+
98
+ def line_diff(text1, text2)
99
+ lines1 = text1.split("\n")
100
+ lines2 = text2.split("\n")
101
+
102
+ result = []
103
+
104
+ i1 = 0
105
+ i2 = 0
106
+
107
+ while i1 < lines1.size || i2 < lines2.size
108
+ line1 = lines1[i1]
109
+ line2 = lines2[i2]
110
+
111
+ if i1 < lines1.size && i2 < lines2.size && line1 == line2
112
+ result << ["=:", line1]
113
+ i1 += 1
114
+ i2 += 1
115
+ elsif i1 < lines1.size && (i2 >= lines2.size || !lines2[i2..-1].include?(line1))
116
+ result << ["-:", line1]
117
+ i1 += 1
118
+ elsif i2 < lines2.size && (i1 >= lines1.size || !lines1[i1..-1].include?(line2))
119
+ result << ["+:", line2]
120
+ i2 += 1
121
+ else
122
+ # Try to find if one of the lines matches later
123
+ idx1 = lines1[i1+1..-1]&.index(line2)
124
+ idx2 = lines2[i2+1..-1]&.index(line1)
125
+
126
+ if !idx1.nil? && (idx2.nil? || idx1 <= idx2)
127
+ result << ["-:", line1]
128
+ i1 += 1
129
+ elsif !idx2.nil?
130
+ result << ["+:", line2]
131
+ i2 += 1
132
+ else
133
+ # Lines differ, treat both as deleted and added
134
+ result << ["-:", line1]
135
+ result << ["+:", line2]
136
+ i1 += 1
137
+ i2 += 1
138
+ end
139
+ end
140
+ end
141
+
142
+ result
143
+ end
144
+
145
+ def load(default)
146
+ if File.exist?(@snapshot_file)
147
+ File.open(@snapshot_file, 'r') do |f|
148
+ @contexts = JSON.load(f.read)['contexts']
149
+ end
150
+ else
151
+ @contexts = default.map{ |ctx| [ctx.name, {'name' => ctx.name, 'message' => ctx.raw}]}.to_h
152
+ end
153
+ end
154
+
155
+ def dump
156
+ File.open(@snapshot_file, 'w') do |file|
157
+ file.write(JSON.dump({'contexts' => @contexts}))
158
+ end
159
+ end
160
+ end
161
+
162
+ def prepare_snapshot
163
+ raise "snapshot preparation require contexts" if @contexts.empty?
164
+
165
+ @logger.info("APPLICATION #{@name} PREPARING SNAPSHOT #{@snapshot.snapshot_file}")
166
+
167
+ @snapshot.sync(@contexts)
168
+ end
169
+
170
+ def prepare_release
60
171
  @logger.info("APPLICATION #{@name} COMPILING FOR #{@language} RELEASE #{@release}")
61
172
  return unless @output_file.is_a?(String)
62
173
  return unless @release
@@ -64,9 +175,7 @@ class LLMed
64
175
  output_file = Pathname.new(@output_dir) + @output_file
65
176
 
66
177
  if @release && File.exist?(output_file) && !File.exist?(release_source_code)
67
- FileUtils.cp(output_file, release_source_code)
68
- FileUtils.cp(output_file, release_main_source_code)
69
- @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
178
+
70
179
  elsif @release && !File.exist?(output_file) && File.exist?(release_main_source_code)
71
180
  FileUtils.mkdir_p(File.dirname(output_file))
72
181
  FileUtils.cp(release_main_source_code, output_file)
@@ -117,14 +226,35 @@ class LLMed
117
226
  output_file(@output_dir) do |file|
118
227
  file.write(output_content)
119
228
  end
229
+
230
+ # only update snapshot if changes are made
231
+ if !File.exist?(release_source_code)
232
+ @snapshot.refresh(@contexts)
233
+ @logger.info("APPLICATION #{@name} SNAPSHOT REFRESHED")
234
+ end
235
+
236
+ output_file = Pathname.new(@output_dir) + @output_file
237
+ FileUtils.cp(output_file, release_source_code)
238
+ FileUtils.cp(output_file, release_main_source_code)
239
+ @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
120
240
  end
121
241
 
122
242
  def system_prompt(configuration)
243
+ contexts_diffs = @snapshot.diff(contexts)
244
+ changes_of_contexts = ''
245
+ if contexts_diffs.any?
246
+ contexts_diffs.each do |context_name, diffs|
247
+ changes_of_contexts += "# Context: #{context_name}\n"
248
+ changes_of_contexts += diffs.map { |op, line| "#{op} #{line}" }.join("\n")
249
+ end
250
+ end
251
+
123
252
  configuration.prompt(language: language,
124
253
  source_code: source_code,
125
254
  code_comment_begin: @code_comment.begin,
126
255
  code_comment_end: @code_comment.end,
127
- update_context_digests: digests_of_context_to_update)
256
+ update_context_digests: digests_of_context_to_update,
257
+ changes_of_contexts: changes_of_contexts)
128
258
  end
129
259
 
130
260
  def rebuild?
@@ -134,9 +264,8 @@ class LLMed
134
264
  !digests_of_context_to_update.tap do |digests|
135
265
  digests.each do |digest|
136
266
  context_by_digest = release_contexts.invert
137
-
138
267
  if context_by_digest[digest].nil?
139
- @logger.info("APPLICATION #{@name} ADDING CONTEXT #{user_contexts.invert[digest]}")
268
+ @logger.info("APPLICATION #{@name} ADDING CONTEXT #{user_contexts.by_digest(digest).name}")
140
269
  else
141
270
  @logger.info("APPLICATION #{@name} REBUILDING CONTEXT #{context_by_digest[digest]}")
142
271
  end
@@ -188,8 +317,8 @@ class LLMed
188
317
  end
189
318
 
190
319
  # added new context
191
- if !release_context.digest? && !user_contexts[ctx.name].nil?
192
- update_context_digest << user_contexts[ctx.name]
320
+ if !release_context.digest? && !user_contexts.by_name(ctx.name).nil?
321
+ update_context_digest << user_contexts.by_name(ctx.name).digest
193
322
  next
194
323
  elsif release_context.digest? && !ctx.same_digest?(release_context.digest)
195
324
  update_rest = true
@@ -214,9 +343,7 @@ class LLMed
214
343
  end
215
344
 
216
345
  def user_contexts
217
- @contexts.map do |ctx|
218
- [ctx.name, ctx.digest]
219
- end.to_h
346
+ UserContexts.new(@contexts)
220
347
  end
221
348
 
222
349
  def release_contexts
@@ -7,36 +7,50 @@ class LLMed
7
7
  @logger = logger
8
8
  # Manual tested, pass 5 times execution
9
9
  @prompt = LLMed::LLM::Template.build(template: "
10
- You are a software developer with knowledge only of the programming language {language}, following the SOLID principles strictly, you always use only imperative and functional programming, design highly isolated components.
11
- Don't make any assumptions/expectations or wait for implementations, always implement the necessary.
12
- The contexts are declarations of how the source code will be (not a file) ensure to follow this always.
13
- The contexts are connected as a flat linked list.
14
- All the contexts represent one source code.
15
- There is always a one-to-one correspondence between context and source code.
16
- Always include the properly escaped comment: LLMED-COMPILED.
10
+ You are a software developer specialized in the programming language {language}. Follow SOLID principles strictly. Use only imperative and functional programming styles and design highly isolated components. You have full access to the standard library and third-party packages only if explicitly allowed.
11
+ Hard requirements: produce complete, executable, and compilable source code — no placeholders, no pseudo-código, no partial implementations, no explanations. If anything below cannot be satisfied, produce a runtime error implementation that clearly fails fast.
17
12
 
18
- You must only modify the following source code:
19
- ```
20
- {source_code}
21
- ```
13
+ The input contexts are functional sections of a single large source file (not separate files). Contexts are linked as a flat linked list. There must be a one-to-one correspondence between each context and the code generated for that context. You must only generate source code for the context(s) whose digest is listed in {update_context_digests}.
22
14
 
23
- Only generate source code of the context who digest belongs to {update_context_digests}.
15
+ Always include the escaped literal comment token LLMED-COMPILED somewhere in the generated code.
24
16
 
25
- Wrap with comment every code that belongs to the indicated context, example in {language}:
26
- {code_comment_begin}<llmed-code context='context name' digest='....' after='digest next context'>{code_comment_end}
27
- ...
28
- {code_comment_begin}</llmed-code>{code_comment_end}
17
+ You must only modify the following source code that is provided between the code fences:
18
+ ```{language}
19
+ {source_code}
20
+ ```
29
21
 
30
- !!Your response must contain only the generated source code, with no additional text or comments, and you must ensure that runs correctly on the first attempt.
31
- ", input_variables: %w[language source_code code_comment_begin code_comment_end update_context_digests])
22
+ Strict formatting for context wrappers: wrap every context implementation with the exact comment markers below. Use the literal placeholders {code_comment_begin} and {code_comment_end} replaced with the exact comment open/close strings for the target language. Example wrapper (replace placeholders when running the prompt):
23
+ {code_comment_begin}<llmed-code context='context name' digest='CURRENT_DIGEST' link-digest='NEXT_DIGEST' after='NEXT_DIGEST'>{code_comment_end}
24
+ ... COMPLETE, RUNNABLE implementation for that context ...
25
+ {code_comment_begin}</llmed-code>{code_comment_end}
26
+
27
+ Behavioral rules (must be obeyed):
28
+ 1. No comments-only outputs. If your natural answer would be comments, instead implement executable code that performs the described behavior. Do not output explanatory text outside code blocks — output only source code for the indicated contexts.
29
+ 2. All functions/methods must have bodies implementing the intended behavior. If external information is missing, implement a reasonable, deterministic default rather than leaving a stub.
30
+ 3. Fail-fast fallback: if the requested context genuinely cannot be implemented, include a clear runtime failure function implementation_impossible() that raises/prints a single machine-readable error (e.g. throws an exception with message IMPLEMENTATION-IMPOSSIBLE) and still compiles.
31
+ 4. One-to-one mapping: produce exactly one code block per digest requested. Do not add unrelated helper contexts unless they are wrapped and linked to an indicated digest; if helpers are necessary, include them inside the same context wrapper.
32
+ 5. Include the literal LLMED-COMPILED comment somewhere inside the code.
33
+ 6. Do not output any text outside the source code. The assistant response must be only source code for the requested context(s).
34
+
35
+ All behavior described by contexts marked with '-:' in <changes> must be completely removed from the generated source code.
36
+ Do not leave any code, print statements, functions, or references implementing deleted contexts.
37
+ Each context listed in '+:' or '=:' must be implemented exactly according to its description.
38
+ Behavior from removed contexts must not appear anywhere in the output, even indirectly.
39
+ <changes>
40
+ {changes_of_contexts}
41
+ </changes>
42
+
43
+ Output requirement: your response must contain only the generated source code for the indicated context(s), with the required wrapper comments and the test harness; nothing else.
44
+ ", input_variables: %w[language source_code code_comment_begin code_comment_end update_context_digests changes_of_contexts])
32
45
  end
33
46
 
34
- def prompt(language:, source_code:, code_comment_begin:, code_comment_end:, update_context_digests: [])
47
+ def prompt(language:, source_code:, code_comment_begin:, code_comment_end:, update_context_digests: [], changes_of_contexts: '')
35
48
  @prompt.format(language: language,
36
49
  source_code: source_code,
37
50
  code_comment_begin: code_comment_begin,
38
51
  code_comment_end: code_comment_end,
39
- update_context_digests: update_context_digests.join(','))
52
+ update_context_digests: update_context_digests.join(','),
53
+ changes_of_contexts: changes_of_contexts)
40
54
  end
41
55
 
42
56
  # Change the default prompt, input variables: language, source_code
data/lib/llmed/context.rb CHANGED
@@ -1,14 +1,53 @@
1
1
  # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
2
  # frozen_string_literal: true
3
+
3
4
  require 'erb'
4
5
 
5
6
  class LLMed
7
+ class UserContexts
8
+ def initialize(contexts)
9
+ @contexts = contexts.dup
10
+ end
11
+
12
+ def each(&block)
13
+ @contexts.each(&block)
14
+ end
15
+
16
+ def empty?
17
+ @contexts.empty?
18
+ end
19
+
20
+ def [](idx)
21
+ @contexts[idx]
22
+ end
23
+
24
+ def each_with_next(&block)
25
+ @contexts.each_with_index do |ctx, idx|
26
+ next_ctx = @contexts[idx + 1]
27
+ block.call(ctx, next_ctx)
28
+ end
29
+ end
30
+
31
+ def count
32
+ @contexts.count
33
+ end
34
+
35
+ def by_name(name)
36
+ @contexts.find { |ctx| ctx.name == name }
37
+ end
38
+
39
+ def by_digest(digest)
40
+ @contexts.find { |ctx| ctx.same_digest?(digest) }
41
+ end
42
+ end
43
+
6
44
  class Context
7
45
  attr_reader :name
8
46
 
9
- def initialize(name:, options: {})
47
+ def initialize(name:, digest: nil, options: {})
10
48
  @name = name
11
49
  @skip = options[:skip] || false
50
+ @fixed_digest = digest || nil
12
51
  @release_dir = options[:release_dir]
13
52
  end
14
53
 
@@ -21,13 +60,17 @@ class LLMed
21
60
  end
22
61
 
23
62
  def digest
24
- Digest::SHA256.hexdigest "#{@name}.#{@message}"
63
+ @fixed_digest || Digest::SHA256.hexdigest("#{@name}.#{@message}")
25
64
  end
26
65
 
27
66
  def message
28
67
  "# Context: \"#{@name}\" Digest: #{digest}\n\n#{@message}"
29
68
  end
30
69
 
70
+ def raw
71
+ @message
72
+ end
73
+
31
74
  def llm(message)
32
75
  @message = message
33
76
  end
data/lib/llmed/release.rb CHANGED
@@ -5,7 +5,8 @@ class LLMed
5
5
  class Release
6
6
  ContextCode = Struct.new(:name, :digest, :code, :after) do
7
7
  def to_llmed_code(code_comment)
8
- "#{code_comment.begin}<llmed-code context='#{name}' digest='#{digest}' after='#{after}'>#{code_comment.end}#{code}#{code_comment.begin}</llmed-code>#{code_comment.end}"
8
+ close_newline = "\n" if code.strip.empty?
9
+ "#{code_comment.begin}<llmed-code context='#{name}' digest='#{digest}' after='#{after}'>#{code_comment.end}#{code}#{close_newline}#{code_comment.begin}</llmed-code>#{code_comment.end}"
9
10
  end
10
11
 
11
12
  def digest?
@@ -99,42 +100,64 @@ class LLMed
99
100
 
100
101
  # fix user contexts digest
101
102
  contexts.each do |ctx|
102
- user_context_digest = user_contexts[ctx.name]
103
- ctx.digest = user_context_digest unless user_context_digest.nil?
103
+ user_context = user_contexts.by_name(ctx.name)
104
+ ctx.digest = user_context.digest unless user_context.nil?
104
105
  end
105
106
 
106
107
  # insertions missed user contexts
107
- user_contexts.each do |name, digest|
108
- next if contexts.any? { |ctx| ctx.name == name }
109
- code = release.context_by(name).code
110
- new_ctx = ContextCode.new(name, digest, code, '')
108
+ user_contexts.each do |user_context|
109
+ next if contexts.any? { |ctx| ctx.name == user_context.name }
110
+
111
+ code = release.context_by(user_context.name).code
112
+ new_ctx = ContextCode.new(user_context.name, user_context.digest, code, '')
111
113
  contexts.prepend(new_ctx)
112
114
  @changes << [:added, new_ctx]
113
115
  end
114
116
 
115
- contexts_sorted = []
116
- # prioritize user order
117
- user_contexts.each do |name, _digest|
118
- contexts.each do |ctx|
119
- if ctx.name == name
120
- contexts_sorted << ctx
121
- break
122
- end
123
- end
117
+ # code context must have the same order as user contexts
118
+ if user_contexts.empty?
119
+ contexts_sorted = contexts
120
+ else
121
+ user_contexts_iter = user_contexts.dup
122
+ contexts_iter = contexts.dup
123
+ order_digests = rewire_code_contexts(contexts_iter, user_contexts_iter)
124
+
125
+ contexts_on_digests = order_digests.map { |digest| contexts_iter.find { |ctx| ctx.digest == digest } }
126
+ contexts_missing_digests = contexts_iter.select { |ctx| !order_digests.include?(ctx.digest) }
127
+ contexts_sorted = contexts_on_digests + contexts_missing_digests
124
128
  end
125
129
 
126
- @contexts = contexts_sorted.sort {|a,b|
127
- if a.digest == b.after
130
+ # Sort contexts so that the latest digest (the one whose 'after' is empty) comes last
131
+ @contexts = contexts_sorted.sort do |a, b|
132
+ if a.after.empty? && !b.after.empty?
128
133
  1
134
+ elsif !a.after.empty? && b.after.empty?
135
+ -1
129
136
  else
130
137
  0
131
138
  end
132
- }
139
+ end
140
+
133
141
  self
134
142
  end
135
143
 
136
144
  private
137
145
 
146
+ def rewire_code_contexts(code_contexts, user_contexts)
147
+ order_digests = []
148
+ user_contexts.each_with_next do |user_context, next_user_context|
149
+ ctx = code_contexts.find { |ctx| ctx.name == user_context.name }
150
+ if ctx
151
+ ctx.digest = user_context.digest
152
+ order_digests << user_context.digest
153
+ ctx.after = '' if user_contexts.count > 1
154
+ ctx.after = next_user_context.digest if next_user_context
155
+ end
156
+ end
157
+
158
+ order_digests
159
+ end
160
+
138
161
  def initialize(origin, code_comment)
139
162
  @origin = origin
140
163
  @content = ''
@@ -142,7 +165,7 @@ class LLMed
142
165
  @code_comment = code_comment
143
166
  @contexts = []
144
167
 
145
- @origin.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'\s*(after='.*?')?>#{@code_comment.end}(.+?)#{@code_comment.begin}+\s*<?/?llmed-code}im).each do |match|
168
+ @origin.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'(?:\s+[^>]*?)?(after='.*?')?>#{@code_comment.end}(.+?)#{@code_comment.begin}+\s*<?/?llmed-code}im).each do |match|
146
169
  name, digest, after_block, code = match
147
170
  after = if after_block.nil?
148
171
  ''
data/lib/llmed.rb CHANGED
@@ -66,10 +66,12 @@ class LLMed
66
66
  def compile_application(app)
67
67
  app.notify('COMPILE START')
68
68
 
69
- app.prepare
69
+ app.prepare_release
70
70
  app.evaluate
71
+ app.prepare_snapshot
71
72
  if app.rebuild?
72
73
  llm = @configuration.llm
74
+
73
75
  messages = [LLMed::LLM::Message::System.new(app.system_prompt(@configuration))]
74
76
  app.contexts.each do |ctx|
75
77
  next if ctx.skip?
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.4.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jovany Leandro G.C