elelem 0.1.3 → 0.2.1

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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class File < Tool
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ @tui = configuration.tui
9
+
10
+ super("file", "Read and write files", {
11
+ type: :object,
12
+ properties: {
13
+ action: {
14
+ type: :string,
15
+ enum: ["read", "write"],
16
+ description: "Action to perform: read or write"
17
+ },
18
+ path: {
19
+ type: :string,
20
+ description: "File path"
21
+ },
22
+ content: {
23
+ type: :string,
24
+ description: "Content to write (only for write action)"
25
+ }
26
+ },
27
+ required: [:action, :path]
28
+ })
29
+ end
30
+
31
+ def call(args)
32
+ action = args["action"]
33
+ path = args["path"]
34
+ content = args["content"]
35
+
36
+ case action
37
+ when "read"
38
+ read_file(path)
39
+ when "write"
40
+ write_file(path, content)
41
+ else
42
+ "Invalid action: #{action}"
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :configuration, :tui
49
+
50
+ def read_file(path)
51
+ tui.say("Read: #{path}", newline: true)
52
+ ::File.read(path)
53
+ rescue => e
54
+ "Error reading file: #{e.message}"
55
+ end
56
+
57
+ def write_file(path, content)
58
+ tui.say("Write: #{path}", newline: true)
59
+ ::File.write(path, content)
60
+ "File written successfully"
61
+ rescue => e
62
+ "Error writing file: #{e.message}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class Memory < Tool
6
+ MEMORY_DIR = ".elelem_memory"
7
+ MAX_MEMORY_SIZE = 1_000_000
8
+
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ @tui = configuration.tui
12
+
13
+ super("memory", "Persistent memory for learning and context retention", {
14
+ type: :object,
15
+ properties: {
16
+ action: {
17
+ type: :string,
18
+ enum: %w[store retrieve list search forget],
19
+ description: "Memory action: store, retrieve, list, search, forget"
20
+ },
21
+ key: {
22
+ type: :string,
23
+ description: "Unique key for storing/retrieving memory"
24
+ },
25
+ content: {
26
+ type: :string,
27
+ description: "Content to store (required for store action)"
28
+ },
29
+ query: {
30
+ type: :string,
31
+ description: "Search query for finding memories"
32
+ }
33
+ },
34
+ required: %w[action]
35
+ })
36
+ ensure_memory_dir
37
+ end
38
+
39
+ def call(args)
40
+ action = args["action"]
41
+
42
+ case action
43
+ when "store"
44
+ store_memory(args["key"], args["content"])
45
+ when "retrieve"
46
+ retrieve_memory(args["key"])
47
+ when "list"
48
+ list_memories
49
+ when "search"
50
+ search_memories(args["query"])
51
+ when "forget"
52
+ forget_memory(args["key"])
53
+ else
54
+ "Invalid memory action: #{action}"
55
+ end
56
+ rescue StandardError => e
57
+ "Memory error: #{e.message}"
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :configuration, :tui
63
+
64
+ def ensure_memory_dir
65
+ Dir.mkdir(MEMORY_DIR) unless Dir.exist?(MEMORY_DIR)
66
+ end
67
+
68
+ def memory_path(key)
69
+ ::File.join(MEMORY_DIR, "#{sanitize_key(key)}.json")
70
+ end
71
+
72
+ def sanitize_key(key)
73
+ key.to_s.gsub(/[^a-zA-Z0-9_-]/, "_").slice(0, 100)
74
+ end
75
+
76
+ def store_memory(key, content)
77
+ return "Key and content required for storing" unless key && content
78
+
79
+ total_size = Dir.glob("#{MEMORY_DIR}/*.json").sum { |f| ::File.size(f) }
80
+ return "Memory capacity exceeded" if total_size > MAX_MEMORY_SIZE
81
+
82
+ memory = {
83
+ key: key,
84
+ content: content,
85
+ timestamp: Time.now.iso8601,
86
+ access_count: 0
87
+ }
88
+
89
+ ::File.write(memory_path(key), JSON.pretty_generate(memory))
90
+ "Memory stored: #{key}"
91
+ end
92
+
93
+ def retrieve_memory(key)
94
+ return "Key required for retrieval" unless key
95
+
96
+ path = memory_path(key)
97
+ return "Memory not found: #{key}" unless ::File.exist?(path)
98
+
99
+ memory = JSON.parse(::File.read(path))
100
+ memory["access_count"] += 1
101
+ memory["last_accessed"] = Time.now.iso8601
102
+
103
+ ::File.write(path, JSON.pretty_generate(memory))
104
+ memory["content"]
105
+ end
106
+
107
+ def list_memories
108
+ memories = Dir.glob("#{MEMORY_DIR}/*.json").map do |file|
109
+ memory = JSON.parse(::File.read(file))
110
+ {
111
+ key: memory["key"],
112
+ timestamp: memory["timestamp"],
113
+ size: memory["content"].length,
114
+ access_count: memory["access_count"] || 0
115
+ }
116
+ end
117
+
118
+ memories.sort_by { |m| m[:timestamp] }.reverse
119
+ JSON.pretty_generate(memories)
120
+ end
121
+
122
+ def search_memories(query)
123
+ return "Query required for search" unless query
124
+
125
+ matches = Dir.glob("#{MEMORY_DIR}/*.json").filter_map do |file|
126
+ memory = JSON.parse(::File.read(file))
127
+ if memory["content"].downcase.include?(query.downcase) ||
128
+ memory["key"].downcase.include?(query.downcase)
129
+ {
130
+ key: memory["key"],
131
+ snippet: memory["content"][0, 200] + "...",
132
+ relevance: calculate_relevance(memory, query)
133
+ }
134
+ end
135
+ end
136
+
137
+ matches.sort_by { |m| -m[:relevance] }
138
+ JSON.pretty_generate(matches)
139
+ end
140
+
141
+ def forget_memory(key)
142
+ return "Key required for forgetting" unless key
143
+
144
+ path = memory_path(key)
145
+ return "Memory not found: #{key}" unless ::File.exist?(path)
146
+
147
+ ::File.delete(path)
148
+ "Memory forgotten: #{key}"
149
+ end
150
+
151
+ def calculate_relevance(memory, query)
152
+ content = memory["content"].downcase
153
+ key = memory["key"].downcase
154
+ query = query.downcase
155
+
156
+ score = 0
157
+ score += 3 if key.include?(query)
158
+ score += content.scan(query).length
159
+ score += (memory["access_count"] || 0) * 0.1
160
+ score
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class Prompt < Tool
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ super("prompt", "Ask the user a question and get their response.", {
9
+ type: :object,
10
+ properties: {
11
+ question: {
12
+ type: :string,
13
+ description: "The question to ask the user."
14
+ }
15
+ },
16
+ required: [:question]
17
+ })
18
+ end
19
+
20
+ def call(args)
21
+ @configuration.tui.prompt(args["question"])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module Toolbox
5
+ class Web < Tool
6
+ def initialize(configuration)
7
+ super("web", "Fetch web content and search the internet", {
8
+ type: :object,
9
+ properties: {
10
+ action: {
11
+ type: :string,
12
+ enum: ["fetch", "search"],
13
+ description: "Action to perform: fetch URL or search"
14
+ },
15
+ url: {
16
+ type: :string,
17
+ description: "URL to fetch (for fetch action)"
18
+ },
19
+ query: {
20
+ type: :string,
21
+ description: "Search query (for search action)"
22
+ }
23
+ },
24
+ required: [:action]
25
+ })
26
+ end
27
+
28
+ def call(args)
29
+ action = args["action"]
30
+ case action
31
+ when "fetch"
32
+ fetch_url(args["url"])
33
+ when "search"
34
+ search_web(args["query"])
35
+ else
36
+ "Invalid action: #{action}"
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def fetch_url(url)
43
+ return "URL required for fetch action" unless url
44
+
45
+ uri = URI(url)
46
+ http = Net::HTTP.new(uri.host, uri.port)
47
+ http.use_ssl = uri.scheme == "https"
48
+ http.read_timeout = 10
49
+ http.open_timeout = 5
50
+
51
+ request = Net::HTTP::Get.new(uri)
52
+ request["User-Agent"] = "Elelem Agent/1.0"
53
+
54
+ response = http.request(request)
55
+
56
+ if response.is_a?(Net::HTTPSuccess)
57
+ content_type = response["content-type"] || ""
58
+ if content_type.include?("text/html")
59
+ extract_text_from_html(response.body)
60
+ else
61
+ response.body
62
+ end
63
+ else
64
+ "HTTP Error: #{response.code} #{response.message}"
65
+ end
66
+ end
67
+
68
+ def search_web(query)
69
+ return "Query required for search action" unless query
70
+
71
+ # Use DuckDuckGo instant answers API
72
+ search_url = "https://api.duckduckgo.com/?q=#{URI.encode_www_form_component(query)}&format=json&no_html=1"
73
+
74
+ result = fetch_url(search_url)
75
+ if result.start_with?("Error") || result.start_with?("HTTP Error")
76
+ result
77
+ else
78
+ format_search_results(JSON.parse(result), query)
79
+ end
80
+ end
81
+
82
+ def extract_text_from_html(html)
83
+ # Simple HTML tag stripping
84
+ text = html.gsub(/<script[^>]*>.*?<\/script>/im, "")
85
+ .gsub(/<style[^>]*>.*?<\/style>/im, "")
86
+ .gsub(/<[^>]*>/, " ")
87
+ .gsub(/\s+/, " ")
88
+ .strip
89
+
90
+ # Limit content length
91
+ text.length > 5000 ? text[0...5000] + "..." : text
92
+ end
93
+
94
+ def format_search_results(data, query)
95
+ results = []
96
+
97
+ # Instant answer
98
+ if data["Answer"] && !data["Answer"].empty?
99
+ results << "Answer: #{data["Answer"]}"
100
+ end
101
+
102
+ # Abstract
103
+ if data["Abstract"] && !data["Abstract"].empty?
104
+ results << "Summary: #{data["Abstract"]}"
105
+ end
106
+
107
+ # Related topics
108
+ if data["RelatedTopics"] && data["RelatedTopics"].any?
109
+ topics = data["RelatedTopics"].first(3).map do |topic|
110
+ topic["Text"] if topic["Text"]
111
+ end.compact
112
+
113
+ if topics.any?
114
+ results << "Related: #{topics.join("; ")}"
115
+ end
116
+ end
117
+
118
+ if results.empty?
119
+ "No direct results found for '#{query}'. Try a more specific search or use web fetch to access specific URLs."
120
+ else
121
+ results.join("\n\n")
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "toolbox/exec"
4
+ require_relative "toolbox/file"
5
+ require_relative "toolbox/web"
6
+ require_relative "toolbox/mcp"
7
+ require_relative "toolbox/prompt"
8
+ require_relative "toolbox/memory"
data/lib/elelem/tools.rb CHANGED
@@ -12,8 +12,7 @@ module Elelem
12
12
  end
13
13
 
14
14
  def execute(tool_call)
15
- name = tool_call.dig("function", "name")
16
- args = tool_call.dig("function", "arguments")
15
+ name, args = parse(tool_call)
17
16
 
18
17
  tool = tools.find { |tool| tool.name == name }
19
18
  return "Invalid function name: #{name}" if tool.nil?
@@ -31,5 +30,12 @@ module Elelem
31
30
  private
32
31
 
33
32
  attr_reader :configuration, :tools
33
+
34
+ def parse(tool_call)
35
+ name = tool_call.dig("function", "name")
36
+ arguments = tool_call.dig("function", "arguments")
37
+
38
+ [name, arguments.is_a?(String) ? JSON.parse(arguments) : arguments]
39
+ end
34
40
  end
35
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -5,12 +5,11 @@ require "erb"
5
5
  require "json"
6
6
  require "json-schema"
7
7
  require "logger"
8
- require "net/http"
8
+ require "net/llm"
9
9
  require "open3"
10
10
  require "reline"
11
11
  require "thor"
12
12
  require "timeout"
13
- require "uri"
14
13
 
15
14
  require_relative "elelem/agent"
16
15
  require_relative "elelem/api"
@@ -27,8 +26,7 @@ require_relative "elelem/states/working/talking"
27
26
  require_relative "elelem/states/working/thinking"
28
27
  require_relative "elelem/states/working/waiting"
29
28
  require_relative "elelem/tool"
30
- require_relative "elelem/toolbox/bash"
31
- require_relative "elelem/toolbox/mcp"
29
+ require_relative "elelem/toolbox"
32
30
  require_relative "elelem/tools"
33
31
  require_relative "elelem/tui"
34
32
  require_relative "elelem/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elelem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -80,7 +80,7 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: net-http
83
+ name: net-llm
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
@@ -149,25 +149,13 @@ dependencies:
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
- - !ruby/object:Gem::Dependency
153
- name: uri
154
- requirement: !ruby/object:Gem::Requirement
155
- requirements:
156
- - - ">="
157
- - !ruby/object:Gem::Version
158
- version: '0'
159
- type: :runtime
160
- prerelease: false
161
- version_requirements: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - ">="
164
- - !ruby/object:Gem::Version
165
- version: '0'
166
152
  description: A REPL for Ollama.
167
153
  email:
168
154
  - mo@mokhan.ca
169
155
  executables:
170
156
  - elelem
157
+ - llm-ollama
158
+ - llm-openai
171
159
  extensions: []
172
160
  extra_rdoc_files: []
173
161
  files:
@@ -176,6 +164,8 @@ files:
176
164
  - README.md
177
165
  - Rakefile
178
166
  - exe/elelem
167
+ - exe/llm-ollama
168
+ - exe/llm-openai
179
169
  - lib/elelem.rb
180
170
  - lib/elelem/agent.rb
181
171
  - lib/elelem/api.rb
@@ -193,12 +183,16 @@ files:
193
183
  - lib/elelem/states/working/waiting.rb
194
184
  - lib/elelem/system_prompt.erb
195
185
  - lib/elelem/tool.rb
196
- - lib/elelem/toolbox/bash.rb
186
+ - lib/elelem/toolbox.rb
187
+ - lib/elelem/toolbox/exec.rb
188
+ - lib/elelem/toolbox/file.rb
197
189
  - lib/elelem/toolbox/mcp.rb
190
+ - lib/elelem/toolbox/memory.rb
191
+ - lib/elelem/toolbox/prompt.rb
192
+ - lib/elelem/toolbox/web.rb
198
193
  - lib/elelem/tools.rb
199
194
  - lib/elelem/tui.rb
200
195
  - lib/elelem/version.rb
201
- - sig/elelem.rbs
202
196
  homepage: https://www.mokhan.ca
203
197
  licenses:
204
198
  - MIT
@@ -221,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
215
  - !ruby/object:Gem::Version
222
216
  version: 3.3.11
223
217
  requirements: []
224
- rubygems_version: 3.6.9
218
+ rubygems_version: 3.7.2
225
219
  specification_version: 4
226
220
  summary: A REPL for Ollama.
227
221
  test_files: []
data/sig/elelem.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Elelem
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end