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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +0 -94
- data/exe/llm-ollama +358 -0
- data/exe/llm-openai +339 -0
- data/lib/elelem/agent.rb +11 -5
- data/lib/elelem/api.rb +34 -21
- data/lib/elelem/configuration.rb +19 -18
- data/lib/elelem/states/working/executing.rb +1 -1
- data/lib/elelem/states/working/thinking.rb +2 -2
- data/lib/elelem/states/working/waiting.rb +2 -2
- data/lib/elelem/states/working.rb +34 -13
- data/lib/elelem/system_prompt.erb +16 -5
- data/lib/elelem/toolbox/{bash.rb → exec.rb} +8 -4
- data/lib/elelem/toolbox/file.rb +66 -0
- data/lib/elelem/toolbox/memory.rb +164 -0
- data/lib/elelem/toolbox/prompt.rb +25 -0
- data/lib/elelem/toolbox/web.rb +126 -0
- data/lib/elelem/toolbox.rb +8 -0
- data/lib/elelem/tools.rb +8 -2
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +2 -4
- metadata +13 -19
- data/sig/elelem.rbs +0 -4
@@ -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
|
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
|
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
|
data/lib/elelem/version.rb
CHANGED
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/
|
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
|
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
|
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-
|
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
|
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.
|
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