llmed 0.4.2 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcb6503e5c4ed7cf826c335f2ea016e6f99187b9b2ea9352267881bb5634ccbe
4
- data.tar.gz: 45d1a3538aca0b6e4ed118eec14bfd08fe61445e5229558264d434d0739b34d9
3
+ metadata.gz: 48b4c352f5fd54aa841fe411fd7f5025f1c98f40b8a287589aeb46ad213def68
4
+ data.tar.gz: c53e2efec6164261d6985f9297b101b363d1e9da432123ddeac1e369a7f045d2
5
5
  SHA512:
6
- metadata.gz: 63d03492ef70ede943c6e7b236158962527a4bce1e9ac4015d28c897a7326aa8ae0e016a8a96e52264cc6c2db286ed66c767311f77bdf344baaedf4f32a156c7
7
- data.tar.gz: 8763511711a43d45e0682afb8cbb3352af6bea95baf9c765de259b2ca276fe40faad5647d2ef93a2e0ea747712ab6ec4c254df91553df5659feccefbe4acf67b
6
+ metadata.gz: 1b228350f691be1762449160bafde3f3c3eba0cc180fd918207f09a855939d0ec784f53861f72304a61d6c4c2adfd525d4e29cf868c1bc1d1dbfb9aa0ff7ad15
7
+ data.tar.gz: 7eea2cc52879f589c1b4612409c7c0923aa88135bcb705817966fb4827d449032cc528891140e37a088ff0145fe792982f068162b716785d41e5c3ac736a8bfd
data/exe/llmed.literate CHANGED
@@ -9,12 +9,22 @@ output_dir = './llmed-out'
9
9
  release_dir = output_dir
10
10
  language = 'ruby'
11
11
  provider = :like_openai
12
- provider_api_key = ENV.fetch('LLMED_PROVIDER_API_KEY', nil)
12
+ provider_api_key = ENV.fetch('LLMED_PROVIDER_API_KEY', '')
13
13
  provider_model = ENV.fetch('LLMED_PROVIDER_MODEL', 'Qwen/Qwen2.5-Coder-32B-Instruct')
14
14
  provider_uri_base = ENV.fetch('LLMED_PROVIDER_URI_BASE', 'https://api.together.xyz/v1')
15
15
  output_file = 'a.out.ollmed'
16
16
 
17
17
  template = <<~TMP
18
+ #% Minimal example
19
+ #% llm_provider : like_openai | openai | anthropic
20
+ #!environment llm_provider like_openai
21
+ #% llm_provider_model : depends provider
22
+ #!environment llm_provider_model Qwen/Qwen2.5-Coder-32B-Instruct
23
+ #% llm_provider_model : depends provider
24
+ #!environment llm_provider_uri_base https://api.together.xyz/v1
25
+ #!environment language ruby
26
+ #!environment output_file demo.rb
27
+
18
28
  # Main
19
29
 
20
30
  Show to user 'hi world!'.
@@ -26,7 +36,26 @@ OptionParser.new do |parser|
26
36
  puts parser
27
37
  puts "\n# Website\nhttps://github.com/bit4bit/llm-labs/tree/main/llmed"
28
38
  puts "\n# Examples\nhttps://github.com/bit4bit/llm-labs/tree/main/llmed/examples"
29
- puts "Environment vars:\nLLMED_PROVIDER_API_KEY\nLLMED_PROVIDER_MODEL=#{provider_model}\nLLMED_PROVIDER_URI_BASE=#{provider_uri_base}"
39
+ puts "\n# Current Environment\nLLMED_PROVIDER_API_KEY\nLLMED_PROVIDER_MODEL=#{provider_model}\nLLMED_PROVIDER_URI_BASE=#{provider_uri_base}"
40
+ puts <<~DOC
41
+
42
+ # A minimal example
43
+ <!--
44
+ #% Minimal example
45
+ #% llm_provider : like_openai | openai | anthropic
46
+ #!environment llm_provider like_openai
47
+ #% llm_provider_model : depends provider
48
+ #!environment llm_provider_model Qwen/Qwen2.5-Coder-32B-Instruct
49
+ #% llm_provider_model : depends provider
50
+ #!environment llm_provider_uri_base https://api.together.xyz/v1
51
+ #!environment language ruby
52
+ #!environment output_file fscalls.rb
53
+ -->
54
+
55
+ # Main
56
+
57
+ Show to user 'hi world!'.
58
+ DOC
30
59
  exit
31
60
  end
32
61
 
@@ -47,7 +76,7 @@ OptionParser.new do |parser|
47
76
  release_dir = path
48
77
  end
49
78
 
50
- parser.on('-l', '--language LANGUAGE', String, 'Programming Language: ruby, python') do |lang|
79
+ parser.on('-l', '--language LANGUAGE', String, 'Programming Language: ruby, python (default: ruby)') do |lang|
51
80
  language = lang
52
81
  end
53
82
 
@@ -63,6 +92,34 @@ end
63
92
 
64
93
  llmed = LLMed.new(logger: logger, output_dir: output_dir, release_dir: release_dir)
65
94
  llmed.set_language language
66
- llmed.set_llm provider: provider, api_key: provider_api_key, model: provider_model, options: {uri_base: provider_uri_base}
67
- LLMed::LiterateProgramming.execute(llmed, 'literate_programming', source_code, output_file: output_file)
95
+ LLMed::LiterateProgramming.execute(source_code, output_file: output_file) do |contexts, application_args, environment|
96
+ environment.each do |k, v|
97
+ case k
98
+ when 'llm_provider'
99
+ provider = v
100
+ when 'llm_provider_model'
101
+ provider_model = v
102
+ when 'llm_provider_uri_base'
103
+ provider_uri_base = v
104
+ when 'language'
105
+ language = v
106
+ end
107
+ end
108
+
109
+ if provider_api_key.strip.empty?
110
+ logger.error "LLMED_PROVIDER_API_KEY is not set. Please set the environment variable and try again."
111
+ exit 1
112
+ end
113
+
114
+ logger.info "Compiling for language: #{language}"
115
+ logger.info "LLM Provider: #{provider} Model: #{provider_model} URI Base: #{provider_uri_base}"
116
+
117
+ llmed.set_llm provider: provider, api_key: provider_api_key, model: provider_model, options: {uri_base: provider_uri_base}
118
+ llmed.application('literate_programming', **application_args) do
119
+ contexts.each do |lcontext|
120
+ context(lcontext[:title]) { lcontext[:content] }
121
+ end
122
+ end
123
+ end
124
+
68
125
  llmed.compile()
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
@@ -134,9 +134,8 @@ class LLMed
134
134
  !digests_of_context_to_update.tap do |digests|
135
135
  digests.each do |digest|
136
136
  context_by_digest = release_contexts.invert
137
-
138
137
  if context_by_digest[digest].nil?
139
- @logger.info("APPLICATION #{@name} ADDING CONTEXT #{user_contexts.invert[digest]}")
138
+ @logger.info("APPLICATION #{@name} ADDING CONTEXT #{user_contexts.by_digest(digest).name}")
140
139
  else
141
140
  @logger.info("APPLICATION #{@name} REBUILDING CONTEXT #{context_by_digest[digest]}")
142
141
  end
@@ -188,8 +187,8 @@ class LLMed
188
187
  end
189
188
 
190
189
  # added new context
191
- if !release_context.digest? && !user_contexts[ctx.name].nil?
192
- update_context_digest << user_contexts[ctx.name]
190
+ if !release_context.digest? && !user_contexts.by_name(ctx.name).nil?
191
+ update_context_digest << user_contexts.by_name(ctx.name).digest
193
192
  next
194
193
  elsif release_context.digest? && !ctx.same_digest?(release_context.digest)
195
194
  update_rest = true
@@ -214,9 +213,7 @@ class LLMed
214
213
  end
215
214
 
216
215
  def user_contexts
217
- @contexts.map do |ctx|
218
- [ctx.name, ctx.digest]
219
- end.to_h
216
+ UserContexts.new(@contexts)
220
217
  end
221
218
 
222
219
  def release_contexts
@@ -12,22 +12,22 @@ Don't make any assumptions/expectations or wait for implementations, always impl
12
12
  The contexts are declarations of how the source code will be (not a file) ensure to follow this always.
13
13
  The contexts are connected as a flat linked list.
14
14
  All the contexts represent one source code.
15
- There is always a one-to-one correspondence between context and source code.
15
+ Always exists one-to-one correspondence between context and source code.
16
16
  Always include the properly escaped comment: LLMED-COMPILED.
17
17
 
18
18
  You must only modify the following source code:
19
- ```
19
+ ```{language}
20
20
  {source_code}
21
21
  ```
22
22
 
23
23
  Only generate source code of the context who digest belongs to {update_context_digests}.
24
24
 
25
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}
26
+ {code_comment_begin}<llmed-code context='here context name' digest='....' link-digest='next digest' after='here same value of attribute link-digest'>{code_comment_end}
27
27
  ...
28
28
  {code_comment_begin}</llmed-code>{code_comment_end}
29
29
 
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.
30
+ !!Your response must contain only the generated source code with all indicated contexts, with no additional text or comments, and you must ensure that runs correctly on the first attempt.
31
31
  ", input_variables: %w[language source_code code_comment_begin code_comment_end update_context_digests])
32
32
  end
33
33
 
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,7 +60,7 @@ 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
@@ -2,9 +2,10 @@ require 'open-uri'
2
2
 
3
3
  class LLMed
4
4
  class LiterateProgramming
5
- def self.execute(llmed, application_name, code, **application_args)
5
+ def self.execute(code, **application_args)
6
6
  md = LLMed::LiterateProgramming::Markdown.new()
7
7
  contexts = []
8
+ environment = {}
8
9
 
9
10
  md.parse(code).each do |item|
10
11
  context = {}
@@ -17,12 +18,14 @@ class LLMed
17
18
  value = item_content[:value].strip
18
19
  if [:language, :release, :output_file, :output_dir, :release_dir].include?(name) && !value.empty?
19
20
  application_args[name] = value
21
+ else
22
+ environment[name] = value
20
23
  end
21
24
  end
22
25
  end
23
26
  next
24
27
  end
25
-
28
+
26
29
  case item[:type]
27
30
  when :context
28
31
  context[:title] = item[:title]
@@ -42,11 +45,7 @@ class LLMed
42
45
  end
43
46
  end
44
47
 
45
- llmed.application(application_name, **application_args) do
46
- contexts.each do |lcontext|
47
- context(lcontext[:title]) { lcontext[:content] }
48
- end
49
- end
48
+ yield contexts, application_args, environment
50
49
  end
51
50
  end
52
51
  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,53 @@ 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
+ rewire_code_contexts(contexts_iter, user_contexts_iter)
124
+ contexts_sorted = contexts_iter
124
125
  end
125
126
 
126
- @contexts = contexts_sorted.sort {|a,b|
127
+ @contexts = contexts_sorted.sort do |a, b|
127
128
  if a.digest == b.after
128
129
  1
129
130
  else
130
131
  0
131
132
  end
132
- }
133
+ end
134
+
133
135
  self
134
136
  end
135
137
 
136
138
  private
137
139
 
140
+ def rewire_code_contexts(code_contexts, user_contexts)
141
+ user_contexts.each_with_next do |user_context, next_user_context|
142
+ ctx = code_contexts.find { |ctx| ctx.digest == user_context.digest }
143
+ if ctx
144
+ ctx.after = '' if user_contexts.count > 1
145
+ ctx.after = next_user_context.digest if next_user_context
146
+ end
147
+ end
148
+ end
149
+
138
150
  def initialize(origin, code_comment)
139
151
  @origin = origin
140
152
  @content = ''
@@ -142,7 +154,7 @@ class LLMed
142
154
  @code_comment = code_comment
143
155
  @contexts = []
144
156
 
145
- @origin.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'\s*(after='.*?')?>#{@code_comment.end}(.+?)#{@code_comment.begin}+\s*<?/?llmed-code}im).each do |match|
157
+ @origin.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'(?:\s+[^>]*?)?(after='.*?')?>#{@code_comment.end}(.+?)#{@code_comment.begin}+\s*<?/?llmed-code}im).each do |match|
146
158
  name, digest, after_block, code = match
147
159
  after = if after_block.nil?
148
160
  ''
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jovany Leandro G.C
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-07-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: langchainrb
@@ -94,34 +93,23 @@ files:
94
93
  - README.md
95
94
  - exe/llmed
96
95
  - exe/llmed.literate
97
- - lib/literate_programming.llmed~
98
96
  - lib/llm.rb
99
- - lib/llm.rb~
100
97
  - lib/llmed.rb
101
- - lib/llmed.rb~
102
98
  - lib/llmed/application.rb
103
- - lib/llmed/application.rb~
104
99
  - lib/llmed/configuration.rb
105
- - lib/llmed/configuration.rb~
106
100
  - lib/llmed/context.rb
107
- - lib/llmed/context.rb~
108
101
  - lib/llmed/deployment.rb
109
- - lib/llmed/deployment.rb~
110
102
  - lib/llmed/literate_programming.rb
111
- - lib/llmed/literate_programming.rb~
112
103
  - lib/llmed/literate_programming/markdown.rb
113
- - lib/llmed/literate_programming/markdown.rb.r1ruby.cache
114
104
  - lib/llmed/literate_programming/markdown.rb.release
115
105
  - lib/llmed/literate_programming/markdown.rb.statistics
116
106
  - lib/llmed/release.rb
117
- - lib/llmed/release.rb~
118
107
  homepage: https://github.com/bit4bit/llmed
119
108
  licenses:
120
109
  - GPL-3.0
121
110
  metadata:
122
111
  source_code_uri: https://github.com/bit4bit/llmed
123
112
  allowed_push_host: https://rubygems.org
124
- post_install_message:
125
113
  rdoc_options: []
126
114
  require_paths:
127
115
  - lib
@@ -136,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
124
  - !ruby/object:Gem::Version
137
125
  version: 1.3.7
138
126
  requirements: []
139
- rubygems_version: 3.3.15
140
- signing_key:
127
+ rubygems_version: 3.6.7
141
128
  specification_version: 4
142
129
  summary: LLM Execution Development
143
130
  test_files: []
@@ -1,39 +0,0 @@
1
- set_llm provider: :like_openai, api_key: ENV['TOGETHERAI_API_KEY'], model: 'Qwen/Qwen2.5-Coder-32B-Instruct', options: {uri_base: 'https://api.together.xyz/v1'}
2
-
3
- application "Literate Programming Markdown", release: nil, language: :ruby, output_file: "markdown.rb", output_dir: "literate_programming" do
4
- context "Library LLmed::LiterateProgramming::Markdown" do
5
- <<-LLM
6
- Exports a function call `parse(input: String)`.
7
-
8
- ### Example of usage
9
-
10
- ```ruby
11
- md = LLmed::LiteratePrograming::Markdown.new()
12
- md.parse("
13
- # Context A
14
- Contenido
15
- [link](http://link)
16
- ## SubContexto A
17
- SubContenido
18
-
19
- # Contexto 3
20
- Contenido 3
21
-
22
- ") == [{type: :context,
23
- title: "Context A",
24
- content: [
25
- {type: :string, content: "Contenido\n"},
26
- {type: :link, content: "link", reference: "http://link"},
27
- {type: :string, content: "##SubContexto A\nSubContenido\n\n"}
28
- ]},
29
- {type: :context,
30
- title: "Contexto 3",
31
- content: [
32
- {type: :string, content: "Contenido 3\n\n"}
33
- ]}]
34
- ```
35
-
36
-
37
- LLM
38
- end
39
- end
data/lib/llm.rb~ DELETED
File without changes
@@ -1,182 +0,0 @@
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
@@ -1,76 +0,0 @@
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
@@ -1,62 +0,0 @@
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
@@ -1,16 +0,0 @@
1
- # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
- # frozen_string_literal: true
3
-
4
- class LLMed
5
- class Deployment
6
- def initialize(name:, output_dir:, block:)
7
- @name = name
8
- @output_dir = output_dir
9
- @block = block
10
- end
11
-
12
- def execute()
13
- self.instance_eval(@block)
14
- end
15
- end
16
- end
@@ -1,24 +0,0 @@
1
- #<llmed-code context='Library LLMed::LiterateProgramming::Markdown' digest='18aa5391a8a334f24a80542620a277bb9c085762cb88b7dbcafa26a532a48027' after=''>
2
- class LLMed::LiterateProgramming::Markdown
3
- def parse(input)
4
- contexts = []
5
- current_context = { type: :context, title: "_default", content: [] }
6
-
7
- input.each_line do |line|
8
- if line.strip =~ /^# (.+)$/
9
- contexts << current_context unless current_context[:content].empty?
10
- current_context = { type: :context, title: Regexp.last_match(1), content: [] }
11
- elsif line.strip =~ /^\[(.+)\]\((.+)\)$/
12
- current_context[:content] << { type: :link, content: Regexp.last_match(1), reference: Regexp.last_match(2) }
13
- elsif line.strip =~ /^#% (.+)$/
14
- current_context[:content] << { type: :comment, content: Regexp.last_match(1) + "\n" }
15
- else
16
- current_context[:content] << { type: :string, content: line }
17
- end
18
- end
19
-
20
- contexts << current_context unless current_context[:content].empty?
21
- contexts
22
- end
23
- end
24
- #</llmed-code>
@@ -1 +0,0 @@
1
- require_relative 'literater_programming/markdown'
@@ -1,31 +0,0 @@
1
- # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
- # frozen_string_literal: true
3
-
4
- class LLMed
5
- class Release
6
-
7
- ContextCode = Struct.new(:name, :digest, :code)
8
-
9
- def read(path)
10
- @origin = File.read(path)
11
- @changes = []
12
- end
13
-
14
- def contexts
15
- contexts = []
16
- @origin.scan(%r{<llmed-code context='(.+?)' digest='(.+?)'>(.+?)</llmed-code>}im).each do |match|
17
- name, digest, code = match
18
- contexts << ContextCode.new(name, digest, code)
19
- end
20
- end
21
-
22
- def merge(release)
23
- content = ""
24
- release.contexts.each do |ctx|
25
- content = content.sub(%r{(.*?)(<llmed-code context='#{ctx.name}' digest='.*?'>)(.+?)(</llmed-code>)(.*?)}m) do
26
- "#{::Regexp.last_match(1)}<llmed-code context='#{ctx.name}' digest='#{ctx.digest}'>#{ctx.code}#{::Regexp.last_match(4)}#{::Regexp.last_match(5)}"
27
- end
28
- end
29
- end
30
- end
31
- end
data/lib/llmed.rb~ DELETED
@@ -1,190 +0,0 @@
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