elelem 0.1.0 → 0.1.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 +7 -0
- data/README.md +130 -8
- data/exe/elelem +1 -1
- data/lib/elelem/agent.rb +24 -63
- data/lib/elelem/api.rb +37 -0
- data/lib/elelem/application.rb +28 -9
- data/lib/elelem/configuration.rb +11 -5
- data/lib/elelem/conversation.rb +11 -5
- data/lib/elelem/state.rb +111 -0
- data/lib/elelem/tools.rb +13 -28
- data/lib/elelem/tui.rb +38 -0
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 139cfa01fc1cb7c7a7d7c7b86fa72d49027dfeead68754dfd1be3d864228dbff
|
4
|
+
data.tar.gz: ee500832246d4a8c6e9c5c6aab4daadbbfe9b4c1724afe771de310a3dc402eb3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 920741eb6faf5bcfe67b78c62e65496012919a65d52b1919eefea0bedafd11f65de399c16d9bd6b3d812f2d21fd47303c8145267b9368d15cdba741a92dd50ef
|
7
|
+
data.tar.gz: e95155995fe0e21242bef5cd574699a47025b79064857183617b8d99c6d309885a226851db714ab2c8cb4b0a053055821b1f6b013df8e33b97034b0cd05bc270
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.1] - 2025-08-12
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
- Fixed infinite loop bug after tool execution - loop now continues until assistant provides final response
|
7
|
+
- Fixed conversation history accumulating streaming chunks as separate entries - now properly combines same-role consecutive messages
|
8
|
+
- Improved state machine logging with better debug output
|
9
|
+
|
3
10
|
## [0.1.0] - 2025-08-08
|
4
11
|
|
5
12
|
- Initial release
|
data/README.md
CHANGED
@@ -1,28 +1,56 @@
|
|
1
1
|
# Elelem
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/elelem`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
Elelem is an interactive REPL (Read-Eval-Print Loop) for Ollama that provides a command-line chat interface for communicating with AI models. It features tool calling capabilities, streaming responses, and a clean state machine architecture.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
7
|
Install the gem and add to the application's Gemfile by executing:
|
12
8
|
|
13
9
|
```bash
|
14
|
-
bundle add
|
10
|
+
bundle add elelem
|
15
11
|
```
|
16
12
|
|
17
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
14
|
|
19
15
|
```bash
|
20
|
-
gem install
|
16
|
+
gem install elelem
|
21
17
|
```
|
22
18
|
|
23
19
|
## Usage
|
24
20
|
|
25
|
-
|
21
|
+
Start an interactive chat session with an Ollama model:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
elelem chat
|
25
|
+
```
|
26
|
+
|
27
|
+
### Options
|
28
|
+
|
29
|
+
- `--host`: Specify Ollama host (default: localhost:11434)
|
30
|
+
- `--model`: Specify Ollama model (default: gpt-oss, currently only tested with gpt-oss)
|
31
|
+
- `--token`: Provide authentication token
|
32
|
+
- `--debug`: Enable debug logging
|
33
|
+
|
34
|
+
### Examples
|
35
|
+
|
36
|
+
```bash
|
37
|
+
# Chat with default model
|
38
|
+
elelem chat
|
39
|
+
|
40
|
+
# Chat with specific model and host
|
41
|
+
elelem chat --model llama2 --host remote-host:11434
|
42
|
+
|
43
|
+
# Enable debug mode
|
44
|
+
elelem chat --debug
|
45
|
+
```
|
46
|
+
|
47
|
+
### Features
|
48
|
+
|
49
|
+
- **Interactive REPL**: Clean command-line interface for chatting
|
50
|
+
- **Tool Execution**: Execute shell commands when requested by the AI
|
51
|
+
- **Streaming Responses**: Real-time streaming of AI responses
|
52
|
+
- **State Machine**: Robust state management for different interaction modes
|
53
|
+
- **Conversation History**: Maintains context across the session
|
26
54
|
|
27
55
|
## Development
|
28
56
|
|
@@ -30,6 +58,100 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
30
58
|
|
31
59
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
60
|
|
61
|
+
REPL State Diagram
|
62
|
+
|
63
|
+
```
|
64
|
+
┌─────────────────┐
|
65
|
+
│ START/INIT │
|
66
|
+
└─────────┬───────┘
|
67
|
+
│
|
68
|
+
v
|
69
|
+
┌─────────────────┐
|
70
|
+
┌────▶│ IDLE (Prompt) │◄────┐
|
71
|
+
│ │ Shows "> " │ │
|
72
|
+
│ └─────────┬───────┘ │
|
73
|
+
│ │ │
|
74
|
+
│ │ User input │
|
75
|
+
│ v │
|
76
|
+
│ ┌─────────────────┐ │
|
77
|
+
│ │ PROCESSING │ │
|
78
|
+
│ │ INPUT │ │
|
79
|
+
│ └─────────┬───────┘ │
|
80
|
+
│ │ │
|
81
|
+
│ │ API call │
|
82
|
+
│ v │
|
83
|
+
│ ┌─────────────────┐ │
|
84
|
+
│ │ STREAMING │ │
|
85
|
+
│ ┌──▶│ RESPONSE │─────┤
|
86
|
+
│ │ └─────────┬───────┘ │
|
87
|
+
│ │ │ │ done=true
|
88
|
+
│ │ │ Parse chunk │
|
89
|
+
│ │ v │
|
90
|
+
│ │ ┌─────────────────┐ │
|
91
|
+
│ │ │ MESSAGE TYPE │ │
|
92
|
+
│ │ │ ROUTING │ │
|
93
|
+
│ │ └─────┬─┬─┬───────┘ │
|
94
|
+
│ │ │ │ │ │
|
95
|
+
┌────────┴─┴─────────┘ │ └─────────────┴──────────┐
|
96
|
+
│ │ │
|
97
|
+
v v v
|
98
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
99
|
+
│ THINKING │ │ TOOL │ │ CONTENT │
|
100
|
+
│ STATE │ │ EXECUTION │ │ OUTPUT │
|
101
|
+
│ │ │ STATE │ │ STATE │
|
102
|
+
└─────────────┘ └─────┬───────┘ └─────────────┘
|
103
|
+
│ │ │
|
104
|
+
│ │ done=false │
|
105
|
+
└───────────────────┼──────────────────────────┘
|
106
|
+
│
|
107
|
+
v
|
108
|
+
┌─────────────────┐
|
109
|
+
│ CONTINUE │
|
110
|
+
│ STREAMING │
|
111
|
+
└─────────────────┘
|
112
|
+
│
|
113
|
+
└─────────────────┐
|
114
|
+
│
|
115
|
+
┌─────────────────┐ │
|
116
|
+
│ ERROR STATE │ │
|
117
|
+
│ (Exception) │ │
|
118
|
+
└─────────────────┘ │
|
119
|
+
▲ │
|
120
|
+
│ Invalid response │
|
121
|
+
└────────────────────────────┘
|
122
|
+
|
123
|
+
EXIT CONDITIONS:
|
124
|
+
┌─────────────────────────┐
|
125
|
+
│ • User enters "" │
|
126
|
+
│ • User enters "exit" │
|
127
|
+
│ • EOF (Ctrl+D) │
|
128
|
+
│ • nil input │
|
129
|
+
└─────────────────────────┘
|
130
|
+
│
|
131
|
+
v
|
132
|
+
┌─────────────────────────┐
|
133
|
+
│ TERMINATE │
|
134
|
+
└─────────────────────────┘
|
135
|
+
```
|
136
|
+
|
137
|
+
Key Transitions:
|
138
|
+
|
139
|
+
1. IDLE → PROCESSING: User enters any non-empty, non-"exit" input
|
140
|
+
2. PROCESSING → STREAMING: API call initiated to Ollama
|
141
|
+
3. STREAMING → MESSAGE ROUTING: Each chunk received is parsed
|
142
|
+
4. MESSAGE ROUTING → States: Based on message content:
|
143
|
+
- thinking → THINKING STATE
|
144
|
+
- tool_calls → TOOL EXECUTION STATE
|
145
|
+
- content → CONTENT OUTPUT STATE
|
146
|
+
- Invalid format → ERROR STATE
|
147
|
+
5. All States → IDLE: When done=true from API response
|
148
|
+
6. TOOL EXECUTION → STREAMING: Sets done=false to continue conversation
|
149
|
+
7. Any State → TERMINATE: On exit conditions
|
150
|
+
|
151
|
+
The REPL operates as a continuous loop where the primary flow is IDLE → PROCESSING → STREAMING →
|
152
|
+
back to IDLE, with the streaming phase potentially cycling through multiple message types before
|
153
|
+
completion.
|
154
|
+
|
33
155
|
## Contributing
|
34
156
|
|
35
157
|
Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.
|
data/exe/elelem
CHANGED
data/lib/elelem/agent.rb
CHANGED
@@ -2,86 +2,47 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Agent
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :api, :conversation, :logger
|
6
6
|
|
7
7
|
def initialize(configuration)
|
8
|
+
@api = configuration.api
|
8
9
|
@configuration = configuration
|
9
10
|
@conversation = configuration.conversation
|
10
|
-
@
|
11
|
+
@logger = configuration.logger
|
12
|
+
transition_to(Idle.new)
|
11
13
|
end
|
12
14
|
|
13
15
|
def repl
|
14
16
|
loop do
|
15
|
-
|
16
|
-
user = STDIN.gets&.chomp
|
17
|
-
break if user.nil? || user.empty? || user == 'exit'
|
18
|
-
process_input(user)
|
19
|
-
puts("\u001b[32mDone!\u001b[0m")
|
17
|
+
current_state.run(self)
|
20
18
|
end
|
21
19
|
end
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
conversation.add(role: 'user', content: text)
|
27
|
-
|
28
|
-
done = false
|
29
|
-
loop do
|
30
|
-
call_api(conversation.history) do |chunk|
|
31
|
-
debug_print(chunk)
|
32
|
-
|
33
|
-
response = JSON.parse(chunk)
|
34
|
-
done = response['done']
|
35
|
-
message = response['message'] || {}
|
36
|
-
|
37
|
-
if message['thinking']
|
38
|
-
print("\u001b[90m#{message['thinking']}\u001b[0m")
|
39
|
-
elsif message['tool_calls']&.any?
|
40
|
-
message['tool_calls'].each do |t|
|
41
|
-
conversation.add(role: 'tool', content: tools.execute(t))
|
42
|
-
end
|
43
|
-
done = false
|
44
|
-
elsif message['content'].to_s.strip
|
45
|
-
print message['content'].to_s.strip
|
46
|
-
else
|
47
|
-
raise chunk.inspect
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
break if done
|
52
|
-
end
|
21
|
+
def transition_to(next_state)
|
22
|
+
logger.debug("Transition to: #{next_state.class.name}")
|
23
|
+
@current_state = next_state
|
53
24
|
end
|
54
25
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
model: configuration.model,
|
59
|
-
stream: true,
|
60
|
-
keep_alive: '5m',
|
61
|
-
options: { temperature: 0.1 },
|
62
|
-
tools: tools.to_h
|
63
|
-
}
|
64
|
-
json_body = body.to_json
|
65
|
-
debug_print(json_body)
|
66
|
-
|
67
|
-
req = Net::HTTP::Post.new(configuration.uri)
|
68
|
-
req['Content-Type'] = 'application/json'
|
69
|
-
req.body = json_body
|
70
|
-
req['Authorization'] = "Bearer #{configuration.token}" if configuration.token
|
26
|
+
def prompt(message)
|
27
|
+
configuration.tui.prompt(message)
|
28
|
+
end
|
71
29
|
|
72
|
-
|
73
|
-
|
30
|
+
def say(message, colour: :default, newline: false)
|
31
|
+
configuration.tui.say(message, colour: colour, newline: newline)
|
32
|
+
end
|
74
33
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
$stdout.flush
|
79
|
-
end
|
80
|
-
end
|
34
|
+
def execute(tool_call)
|
35
|
+
logger.debug("Execute: #{tool_call}")
|
36
|
+
configuration.tools.execute(tool_call)
|
81
37
|
end
|
82
38
|
|
83
|
-
def
|
84
|
-
|
39
|
+
def quit
|
40
|
+
logger.debug("Exiting...")
|
41
|
+
exit
|
85
42
|
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :configuration, :current_state
|
86
47
|
end
|
87
48
|
end
|
data/lib/elelem/api.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Api
|
5
|
+
attr_reader :configuration
|
6
|
+
|
7
|
+
def initialize(configuration)
|
8
|
+
@configuration = configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def chat(messages)
|
12
|
+
body = {
|
13
|
+
messages: messages,
|
14
|
+
model: configuration.model,
|
15
|
+
stream: true,
|
16
|
+
keep_alive: "5m",
|
17
|
+
options: { temperature: 0.1 },
|
18
|
+
tools: configuration.tools.to_h
|
19
|
+
}
|
20
|
+
configuration.logger.debug(JSON.pretty_generate(body))
|
21
|
+
json_body = body.to_json
|
22
|
+
|
23
|
+
req = Net::HTTP::Post.new(configuration.uri)
|
24
|
+
req["Content-Type"] = "application/json"
|
25
|
+
req.body = json_body
|
26
|
+
req["Authorization"] = "Bearer #{configuration.token}" if configuration.token
|
27
|
+
|
28
|
+
configuration.http.request(req) do |response|
|
29
|
+
raise response.inspect unless response.code == "200"
|
30
|
+
|
31
|
+
response.read_body do |chunk|
|
32
|
+
yield(chunk)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/elelem/application.rb
CHANGED
@@ -2,21 +2,40 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Application < Thor
|
5
|
-
desc
|
6
|
-
method_option :help,
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
method_option :
|
5
|
+
desc "chat", "Start the REPL"
|
6
|
+
method_option :help,
|
7
|
+
aliases: "-h",
|
8
|
+
type: :boolean,
|
9
|
+
desc: "Display usage information"
|
10
|
+
method_option :host,
|
11
|
+
aliases: "--host",
|
12
|
+
type: :string,
|
13
|
+
desc: "Ollama host",
|
14
|
+
default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
|
15
|
+
method_option :model,
|
16
|
+
aliases: "--model",
|
17
|
+
type: :string,
|
18
|
+
desc: "Ollama model",
|
19
|
+
default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
|
20
|
+
method_option :token,
|
21
|
+
aliases: "--token",
|
22
|
+
type: :string,
|
23
|
+
desc: "Ollama token",
|
24
|
+
default: ENV.fetch("OLLAMA_API_KEY", nil)
|
25
|
+
method_option :debug,
|
26
|
+
aliases: "--debug",
|
27
|
+
type: :boolean,
|
28
|
+
desc: "Debug mode",
|
29
|
+
default: false
|
11
30
|
def chat(*)
|
12
31
|
if options[:help]
|
13
|
-
invoke :help, [
|
32
|
+
invoke :help, ["chat"]
|
14
33
|
else
|
15
34
|
configuration = Configuration.new(
|
16
35
|
host: options[:host],
|
17
36
|
model: options[:model],
|
18
37
|
token: options[:token],
|
19
|
-
debug: options[:debug]
|
38
|
+
debug: options[:debug]
|
20
39
|
)
|
21
40
|
say "Ollama Agent (#{configuration.model})", :green
|
22
41
|
say "Tools:\n #{configuration.tools.banner}", :green
|
@@ -26,7 +45,7 @@ module Elelem
|
|
26
45
|
end
|
27
46
|
end
|
28
47
|
|
29
|
-
desc
|
48
|
+
desc "version", "spandx version"
|
30
49
|
def version
|
31
50
|
puts "v#{Spandx::VERSION}"
|
32
51
|
end
|
data/lib/elelem/configuration.rb
CHANGED
@@ -18,11 +18,17 @@ module Elelem
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
def tui
|
22
|
+
@tui ||= TUI.new($stdin, $stdout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api
|
26
|
+
@api ||= Api.new(self)
|
27
|
+
end
|
28
|
+
|
21
29
|
def logger
|
22
|
-
@logger ||=
|
23
|
-
|
24
|
-
logger.formatter = ->(_, _, _, msg) { msg }
|
25
|
-
end
|
30
|
+
@logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
|
31
|
+
logger.formatter = ->(_, _, _, message) { message.strip + "\n" }
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
@@ -41,7 +47,7 @@ module Elelem
|
|
41
47
|
private
|
42
48
|
|
43
49
|
def scheme
|
44
|
-
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ?
|
50
|
+
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
|
45
51
|
end
|
46
52
|
end
|
47
53
|
end
|
data/lib/elelem/conversation.rb
CHANGED
@@ -4,14 +4,14 @@ module Elelem
|
|
4
4
|
class Conversation
|
5
5
|
SYSTEM_MESSAGE = <<~SYS
|
6
6
|
You are ChatGPT, a helpful assistant with reasoning capabilities.
|
7
|
-
Current date: #{Time.now.strftime(
|
7
|
+
Current date: #{Time.now.strftime("%Y-%m-%d")}.
|
8
8
|
System info: `uname -a` output: #{`uname -a`.strip}
|
9
9
|
Reasoning: high
|
10
10
|
SYS
|
11
11
|
|
12
|
-
ROLES = [
|
12
|
+
ROLES = [:system, :assistant, :user, :tool].freeze
|
13
13
|
|
14
|
-
def initialize(items = [{ role:
|
14
|
+
def initialize(items = [{ role: "system", content: SYSTEM_MESSAGE }])
|
15
15
|
@items = items
|
16
16
|
end
|
17
17
|
|
@@ -20,10 +20,16 @@ module Elelem
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# :TODO truncate conversation history
|
23
|
-
def add(role:
|
23
|
+
def add(role: :user, content: "")
|
24
|
+
role = role.to_sym
|
24
25
|
raise "unknown role: #{role}" unless ROLES.include?(role)
|
26
|
+
return if content.empty?
|
25
27
|
|
26
|
-
@items
|
28
|
+
if @items.last && @items.last[:role] == role
|
29
|
+
@items.last[:content] += content
|
30
|
+
else
|
31
|
+
@items.push({ role: role, content: content })
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
data/lib/elelem/state.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Idle
|
5
|
+
def run(agent)
|
6
|
+
agent.logger.debug("Idling...")
|
7
|
+
input = agent.prompt("\n> ")
|
8
|
+
agent.quit if input.nil? || input.empty? || input == "exit"
|
9
|
+
|
10
|
+
agent.conversation.add(role: :user, content: input)
|
11
|
+
agent.transition_to(Working.new)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Working
|
16
|
+
class State
|
17
|
+
attr_reader :agent
|
18
|
+
|
19
|
+
def initialize(agent)
|
20
|
+
@agent = agent
|
21
|
+
end
|
22
|
+
|
23
|
+
def display_name
|
24
|
+
self.class.name.split("::").last
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Waiting < State
|
29
|
+
def process(message)
|
30
|
+
state = self
|
31
|
+
|
32
|
+
if message["thinking"] && !message["thinking"].empty?
|
33
|
+
state = Thinking.new(agent)
|
34
|
+
elsif message["tool_calls"]&.any?
|
35
|
+
state = Executing.new(agent)
|
36
|
+
elsif message["content"] && !message["content"].empty?
|
37
|
+
state = Talking.new(agent)
|
38
|
+
else
|
39
|
+
state = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
state&.process(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Thinking < State
|
47
|
+
def process(message)
|
48
|
+
if message["thinking"] && !message["thinking"]&.empty?
|
49
|
+
agent.say(message["thinking"], colour: :gray, newline: false)
|
50
|
+
self
|
51
|
+
else
|
52
|
+
agent.say("", newline: true)
|
53
|
+
Waiting.new(agent).process(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Executing < State
|
59
|
+
def process(message)
|
60
|
+
if message["tool_calls"]&.any?
|
61
|
+
message["tool_calls"].each do |tool_call|
|
62
|
+
agent.conversation.add(role: :tool, content: agent.execute(tool_call))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Waiting.new(agent)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Talking < State
|
71
|
+
def process(message)
|
72
|
+
if message["content"] && !message["content"]&.empty?
|
73
|
+
agent.conversation.add(role: message["role"], content: message["content"])
|
74
|
+
agent.say(message["content"], colour: :default, newline: false)
|
75
|
+
self
|
76
|
+
else
|
77
|
+
agent.say("", newline: true)
|
78
|
+
Waiting.new(agent).process(message)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(agent)
|
84
|
+
agent.logger.debug("Working...")
|
85
|
+
state = Waiting.new(agent)
|
86
|
+
done = false
|
87
|
+
|
88
|
+
loop do
|
89
|
+
agent.api.chat(agent.conversation.history) do |chunk|
|
90
|
+
response = JSON.parse(chunk)
|
91
|
+
message = normalize(response["message"] || {})
|
92
|
+
done = response["done"]
|
93
|
+
|
94
|
+
agent.logger.debug("#{state.display_name}: #{message}")
|
95
|
+
state = state.process(message)
|
96
|
+
end
|
97
|
+
|
98
|
+
break if state.nil?
|
99
|
+
break if done && agent.conversation.history.last[:role] != :tool
|
100
|
+
end
|
101
|
+
|
102
|
+
agent.transition_to(Idle.new)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def normalize(message)
|
108
|
+
message.reject { |_key, value| value.empty? }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/elelem/tools.rb
CHANGED
@@ -4,38 +4,23 @@ module Elelem
|
|
4
4
|
class Tools
|
5
5
|
DEFAULT_TOOLS = [
|
6
6
|
{
|
7
|
-
type:
|
7
|
+
type: "function",
|
8
8
|
function: {
|
9
|
-
name:
|
10
|
-
description:
|
9
|
+
name: "execute_command",
|
10
|
+
description: "Execute a shell command.",
|
11
11
|
parameters: {
|
12
|
-
type:
|
13
|
-
properties: {
|
14
|
-
|
12
|
+
type: "object",
|
13
|
+
properties: {
|
14
|
+
command: { type: "string" },
|
15
|
+
},
|
16
|
+
required: ["command"]
|
15
17
|
}
|
16
18
|
},
|
17
|
-
handler:
|
18
|
-
stdout, stderr, _status = Open3.capture3(
|
19
|
+
handler: lambda { |args|
|
20
|
+
stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
|
19
21
|
stdout + stderr
|
20
22
|
}
|
21
23
|
},
|
22
|
-
{
|
23
|
-
type: 'function',
|
24
|
-
function: {
|
25
|
-
name: 'ask_user',
|
26
|
-
description: 'Ask the user to answer a question.',
|
27
|
-
parameters: {
|
28
|
-
type: 'object',
|
29
|
-
properties: { question: { type: 'string' } },
|
30
|
-
required: ['question']
|
31
|
-
}
|
32
|
-
},
|
33
|
-
handler: ->(args) {
|
34
|
-
puts("\u001b[35m#{args['question']}\u001b[0m")
|
35
|
-
print "> "
|
36
|
-
STDIN.gets&.chomp
|
37
|
-
}
|
38
|
-
}
|
39
24
|
]
|
40
25
|
|
41
26
|
def initialize(tools = DEFAULT_TOOLS)
|
@@ -52,13 +37,13 @@ module Elelem
|
|
52
37
|
end
|
53
38
|
|
54
39
|
def execute(tool_call)
|
55
|
-
name = tool_call.dig(
|
56
|
-
args = tool_call.dig(
|
40
|
+
name = tool_call.dig("function", "name")
|
41
|
+
args = tool_call.dig("function", "arguments")
|
57
42
|
|
58
43
|
tool = @tools.find do |tool|
|
59
44
|
tool.dig(:function, :name) == name
|
60
45
|
end
|
61
|
-
tool
|
46
|
+
tool&.fetch(:handler)&.call(args)
|
62
47
|
end
|
63
48
|
|
64
49
|
def to_h
|
data/lib/elelem/tui.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class TUI
|
5
|
+
attr_reader :stdin, :stdout
|
6
|
+
|
7
|
+
def initialize(stdin = $stdin, stdout = $stdout)
|
8
|
+
@stdin = stdin
|
9
|
+
@stdout = stdout
|
10
|
+
end
|
11
|
+
|
12
|
+
def prompt(message)
|
13
|
+
say(message)
|
14
|
+
stdin.gets&.chomp
|
15
|
+
end
|
16
|
+
|
17
|
+
def say(message, colour: :default, newline: false)
|
18
|
+
formatted_message = colourize(message, colour: colour)
|
19
|
+
if newline
|
20
|
+
stdout.puts(formatted_message)
|
21
|
+
else
|
22
|
+
stdout.print(formatted_message)
|
23
|
+
end
|
24
|
+
stdout.flush
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def colourize(text, colour: :default)
|
30
|
+
case colour
|
31
|
+
when :gray
|
32
|
+
"\e[90m#{text}\e[0m"
|
33
|
+
else
|
34
|
+
text
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
@@ -8,10 +8,13 @@ require "thor"
|
|
8
8
|
require "uri"
|
9
9
|
|
10
10
|
require_relative "elelem/agent"
|
11
|
+
require_relative "elelem/api"
|
11
12
|
require_relative "elelem/application"
|
12
13
|
require_relative "elelem/configuration"
|
13
14
|
require_relative "elelem/conversation"
|
15
|
+
require_relative "elelem/state"
|
14
16
|
require_relative "elelem/tools"
|
17
|
+
require_relative "elelem/tui"
|
15
18
|
require_relative "elelem/version"
|
16
19
|
|
17
20
|
module Elelem
|
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.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mo khan
|
@@ -110,10 +110,13 @@ files:
|
|
110
110
|
- exe/elelem
|
111
111
|
- lib/elelem.rb
|
112
112
|
- lib/elelem/agent.rb
|
113
|
+
- lib/elelem/api.rb
|
113
114
|
- lib/elelem/application.rb
|
114
115
|
- lib/elelem/configuration.rb
|
115
116
|
- lib/elelem/conversation.rb
|
117
|
+
- lib/elelem/state.rb
|
116
118
|
- lib/elelem/tools.rb
|
119
|
+
- lib/elelem/tui.rb
|
117
120
|
- lib/elelem/version.rb
|
118
121
|
- mise.toml
|
119
122
|
- sig/elelem.rbs
|