amstrad_gpt 0.1.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 +7 -0
- data/.rspec +1 -0
- data/.rspec_status +20 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +12 -0
- data/basic/client.bas +7 -0
- data/exe/amstrad_gpt +40 -0
- data/lib/amstrad_gpt/amstrad.rb +80 -0
- data/lib/amstrad_gpt/amstrad_spec.rb +75 -0
- data/lib/amstrad_gpt/chat_gpt.rb +64 -0
- data/lib/amstrad_gpt/chat_gpt_spec.rb +59 -0
- data/lib/amstrad_gpt/gateway.rb +58 -0
- data/lib/amstrad_gpt/gateway_spec.rb +60 -0
- data/lib/amstrad_gpt/interface.rb +41 -0
- data/lib/amstrad_gpt/version.rb +5 -0
- data/lib/amstrad_gpt/web_server.rb +25 -0
- data/lib/amstrad_gpt.rb +15 -0
- data/lib/spec_helper.rb +16 -0
- metadata +165 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1727c0333ddfd4fec075056c74b1e9a62e52ed91983dbdfff92c1f9125bfe6ef
|
4
|
+
data.tar.gz: 8cebc901bd5825f028e9310650644fc57f69b63967a2c4f8cc40749525e52e02
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 42653c23c7f8e2d4ba70736e9d66a08abad86f9e7835854b33dd34f17d3b1466a47235ba8ee4123e580a664153dade23e69e2a092551ceb3989256dd23e36e2a
|
7
|
+
data.tar.gz: db376fcf2c90b83c4e3164f1e739a6f2d5bef942f0470cafbeef1a1c8915ce8e4c84a11630e3a47032ef2eebeeab8bd16e861597462d1fd53a4c61f5e448d27e
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-r 'spec_helper'
|
data/.rspec_status
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
----------------------------------------- | ------ | --------------- |
|
3
|
+
./lib/amstrad_gpt/amstrad_spec.rb[1:1:1] | passed | 0.00845 seconds |
|
4
|
+
./lib/amstrad_gpt/amstrad_spec.rb[1:2:1] | passed | 0.00059 seconds |
|
5
|
+
./lib/amstrad_gpt/amstrad_spec.rb[1:3:1] | passed | 0.00046 seconds |
|
6
|
+
./lib/amstrad_gpt/amstrad_spec.rb[1:4:1] | passed | 0.00028 seconds |
|
7
|
+
./lib/amstrad_gpt/amstrad_spec.rb[1:5:1] | passed | 0.10454 seconds |
|
8
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:1:1] | passed | 0.00008 seconds |
|
9
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:2:1] | passed | 0.00161 seconds |
|
10
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:2:2] | passed | 0.00266 seconds |
|
11
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:3:1] | passed | 0.00183 seconds |
|
12
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:4:1] | passed | 0.00016 seconds |
|
13
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:4:2] | passed | 0.00012 seconds |
|
14
|
+
./lib/amstrad_gpt/chat_gpt_spec.rb[1:5:1] | passed | 0.00006 seconds |
|
15
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:1:1] | passed | 0.00054 seconds |
|
16
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:2:1] | passed | 0.00023 seconds |
|
17
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:3:1] | passed | 0.00027 seconds |
|
18
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:4:1] | passed | 0.00055 seconds |
|
19
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:5:1] | passed | 0.00029 seconds |
|
20
|
+
./lib/amstrad_gpt/gateway_spec.rb[1:6:1] | passed | 0.00028 seconds |
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Mark Burns
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# AmstradGpt
|
2
|
+
|
3
|
+
Get your Amstrad CPC to talk to ChatGPT via a USB to RS232 serial cable.
|
4
|
+
|
5
|
+
# Getting started
|
6
|
+
- Invent the universe
|
7
|
+
- Open a time portal to 1985.
|
8
|
+
- Alternatively, buy an Amstrad CPC on ebay.
|
9
|
+
- Get a USB to RS232 cable. e.g. https://www.amazon.co.uk/dp/B00QUZY4UG?psc=1&ref=ppx_yo2ov_dt_b_product_details
|
10
|
+
- `gem install amstrad_gpt`
|
11
|
+
- Plug your Amstrad into your Mac with your cable.
|
12
|
+
- Find the tty id
|
13
|
+
|
14
|
+
```
|
15
|
+
ls /dev/tty.*
|
16
|
+
```
|
17
|
+
|
18
|
+
```
|
19
|
+
amstrad_gpt run --tty /dev/tty.<your_tty> --api-key <open_ai_api_key>
|
20
|
+
```
|
21
|
+
|
22
|
+
in your Amstrad at the ready prompt
|
23
|
+
```
|
24
|
+
Amstrad 64K Microcomputer <v1>
|
25
|
+
©1984 Amstrad Consumer Electronics plc
|
26
|
+
and Locomotive Software Ltd.
|
27
|
+
BASIC 1.0
|
28
|
+
Ready
|
29
|
+
```
|
30
|
+
|
31
|
+
type in
|
32
|
+
```
|
33
|
+
10 MODE 1
|
34
|
+
15 PRINT "Enter your question then press [Enter] three times"
|
35
|
+
20 OPENIN "#2"
|
36
|
+
30 WHILE NOT EOF(2)
|
37
|
+
40 A$ = INPUT$(1,#2)
|
38
|
+
50 PRINT A$;
|
39
|
+
60 WEND
|
40
|
+
70 CLOSEIN #2
|
41
|
+
|
42
|
+
RUN
|
43
|
+
```
|
44
|
+
|
45
|
+
## Development
|
46
|
+
|
47
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
48
|
+
Then, run `rake spec` to run the tests.
|
49
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
50
|
+
|
51
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
52
|
+
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).
|
53
|
+
|
54
|
+
## Contributing
|
55
|
+
|
56
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/markburns/amstrad_gpt.
|
57
|
+
|
58
|
+
## License
|
59
|
+
|
60
|
+
The gem is available as open source under the terms of the
|
61
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
62
|
+
|
63
|
+
|
data/Rakefile
ADDED
data/basic/client.bas
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
10 MODE 1 ' Set screen mode to mode 1 (medium resolution)
|
2
|
+
20 OPENIN "#2" ' Open the serial port for input
|
3
|
+
30 WHILE NOT EOF(2) ' Loop until there is no more data
|
4
|
+
40 A$ = INPUT$(1,#2) ' Read 1 character from the serial port
|
5
|
+
50 PRINT A$; ' Display the character on the screen
|
6
|
+
60 WEND
|
7
|
+
70 CLOSEIN #2 ' Close the serial port
|
data/exe/amstrad_gpt
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
4
|
+
|
5
|
+
require 'debug'
|
6
|
+
require 'commander/import'
|
7
|
+
require 'amstrad_gpt'
|
8
|
+
|
9
|
+
program :name, 'Amstrad GPT Interface'
|
10
|
+
program :version, '0.1.0'
|
11
|
+
program :description, 'Facilitates communication between an Amstrad machine and GPT models via serial port.'
|
12
|
+
|
13
|
+
env_tty = ENV['AMSTRAD_TTY']
|
14
|
+
env_api_key = ENV['OPENAI_API_KEY']
|
15
|
+
|
16
|
+
command :run do |c|
|
17
|
+
c.syntax = 'amstrad_gpt run [options]'
|
18
|
+
c.summary = 'Starts the Amstrad GPT interface'
|
19
|
+
c.description = 'This command starts the interface with given TTY and API key options.'
|
20
|
+
|
21
|
+
c.option '--tty STRING',
|
22
|
+
String,
|
23
|
+
'Path to the TTY device (default: ENV["AMSTRAD_TTY"])'
|
24
|
+
|
25
|
+
c.option '--api-key STRING',
|
26
|
+
String,
|
27
|
+
'OpenAI API key (default: ENV["OPEN_AI_KEY"])'
|
28
|
+
|
29
|
+
c.action do |_args, options|
|
30
|
+
tty = options.tty || env_tty
|
31
|
+
api_key = options.api_key || env_api_key
|
32
|
+
|
33
|
+
if tty.nil? || api_key.nil?
|
34
|
+
puts "Both TTY and API key must be provided either through environment variables AMSTRAD_TTY, OPENAI_API_KEY or as options. --tty --api-key"
|
35
|
+
exit(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
AmstradGpt.run( tty: , api_key:)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'amstrad_gpt/interface'
|
2
|
+
|
3
|
+
module AmstradGpt
|
4
|
+
class Amstrad
|
5
|
+
def initialize(tty:, base_sleep_duration: 0.1)
|
6
|
+
@tty = tty
|
7
|
+
@base_sleep_duration = base_sleep_duration
|
8
|
+
setup_mutable_state
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
@reader_thread = Thread.new { read_bytes_loop }
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop
|
16
|
+
@running = false
|
17
|
+
@reader_thread.join
|
18
|
+
@reader_thread = nil
|
19
|
+
interface.shutdown
|
20
|
+
end
|
21
|
+
|
22
|
+
def reply(message)
|
23
|
+
mutex.synchronize do
|
24
|
+
interface.write(message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def receive_messages
|
29
|
+
Thread.new do
|
30
|
+
loop do
|
31
|
+
break unless @running
|
32
|
+
|
33
|
+
message = maybe_message?
|
34
|
+
|
35
|
+
if message
|
36
|
+
yield message if message.length.positive?
|
37
|
+
end
|
38
|
+
|
39
|
+
sleep(base_sleep_duration * rand)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def interface
|
47
|
+
@interface ||= Interface.new(tty:)
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_bytes_loop
|
51
|
+
while @running
|
52
|
+
char = interface.next_character
|
53
|
+
|
54
|
+
mutex.synchronize { buffer << char } if char
|
55
|
+
sleep(base_sleep_duration * rand)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def maybe_message?
|
60
|
+
message = nil
|
61
|
+
|
62
|
+
mutex.synchronize do
|
63
|
+
if buffer.end_with?("\n\n\n")
|
64
|
+
message = buffer[0..-4].strip
|
65
|
+
buffer.clear
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
message
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup_mutable_state
|
73
|
+
@mutex = Mutex.new
|
74
|
+
@buffer = ""
|
75
|
+
@running = true
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :tty, :mutex, :base_sleep_duration, :buffer
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'amstrad_gpt/amstrad'
|
2
|
+
|
3
|
+
RSpec.describe AmstradGpt::Amstrad do
|
4
|
+
let(:tty) { '/dev/tty.S0' }
|
5
|
+
let(:subject) { described_class.new(tty:, base_sleep_duration:) }
|
6
|
+
let(:base_sleep_duration) { 0.0 }
|
7
|
+
|
8
|
+
before do
|
9
|
+
allow(Serial).to receive(:new).and_return(serial_port)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:serial_port) do
|
13
|
+
instance_double(Serial, write: nil, getbyte: nil, close: nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#initialize' do
|
17
|
+
it 'initializes with given tty and sets initial values' do
|
18
|
+
expect(subject.instance_variable_get(:@tty)).to eq(tty)
|
19
|
+
expect(subject.instance_variable_get(:@buffer)).to eq("")
|
20
|
+
expect(subject.instance_variable_get(:@running)).to be true
|
21
|
+
expect(subject.instance_variable_get(:@mutex)).to be_a(Mutex)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#start' do
|
26
|
+
it 'starts the reader thread' do
|
27
|
+
expect(Thread).to receive(:new).and_call_original
|
28
|
+
subject.start
|
29
|
+
expect(subject.instance_variable_get(:@reader_thread)).to be_a(Thread)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#stop' do
|
34
|
+
before { subject.start }
|
35
|
+
|
36
|
+
it 'stops the reader thread and closes the serial port' do
|
37
|
+
expect(serial_port).to receive(:close)
|
38
|
+
subject.stop
|
39
|
+
expect(subject.instance_variable_get(:@running)).to be false
|
40
|
+
expect(subject.instance_variable_get(:@reader_thread)).to be nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#reply' do
|
45
|
+
it 'writes a message to the serial port' do
|
46
|
+
message = "Hello, Amstrad!"
|
47
|
+
expect(serial_port).to receive(:write).with(message)
|
48
|
+
subject.reply(message)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#receive_messages' do
|
53
|
+
it 'yields received messages' do
|
54
|
+
allow(serial_port).to receive(:getbyte).and_return(
|
55
|
+
'a'.ord,
|
56
|
+
'b'.ord,
|
57
|
+
"\n".ord,
|
58
|
+
"\n".ord,
|
59
|
+
"\n".ord
|
60
|
+
)
|
61
|
+
|
62
|
+
subject.start
|
63
|
+
|
64
|
+
received_messages = []
|
65
|
+
|
66
|
+
subject.receive_messages do |message|
|
67
|
+
received_messages << message
|
68
|
+
end
|
69
|
+
|
70
|
+
sleep 0.1
|
71
|
+
|
72
|
+
expect(received_messages).to include("ab")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module AmstradGpt
|
5
|
+
class ChatGpt
|
6
|
+
def initialize(api_key:, prompt:)
|
7
|
+
@api_key = api_key
|
8
|
+
@prompt = prompt
|
9
|
+
end
|
10
|
+
|
11
|
+
def send_message(content)
|
12
|
+
append({ role: 'user', content: })
|
13
|
+
|
14
|
+
response = post
|
15
|
+
parse_response(response.body)
|
16
|
+
end
|
17
|
+
|
18
|
+
def messages
|
19
|
+
@messages ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def post
|
26
|
+
connection.post do |req|
|
27
|
+
req.body = {
|
28
|
+
model: 'gpt-4o',
|
29
|
+
messages: [system, *messages],
|
30
|
+
max_tokens: 150
|
31
|
+
}.to_json
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def system
|
36
|
+
{role: 'system', content: prompt}
|
37
|
+
end
|
38
|
+
|
39
|
+
def connection
|
40
|
+
@connection ||= Faraday.new(
|
41
|
+
url: "https://api.openai.com/v1/chat/completions",
|
42
|
+
headers: {
|
43
|
+
'Content-Type' => 'application/json',
|
44
|
+
'Authorization' => "Bearer #{api_key}"
|
45
|
+
}
|
46
|
+
) do |faraday|
|
47
|
+
faraday.adapter Faraday.default_adapter
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_response(response_body)
|
52
|
+
choices = JSON.parse(response_body)['choices']
|
53
|
+
|
54
|
+
message = choices.first['message']
|
55
|
+
append message.tap { _1[:role] = 'assistant' }
|
56
|
+
message[:content]
|
57
|
+
end
|
58
|
+
|
59
|
+
def append(message)
|
60
|
+
messages.push message.transform_keys!(&:to_sym)
|
61
|
+
end
|
62
|
+
attr_reader :api_key, :prompt
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative './chat_gpt'
|
2
|
+
|
3
|
+
RSpec.describe AmstradGpt::ChatGpt do
|
4
|
+
let(:api_key) { 'test_api_key' }
|
5
|
+
let(:subject) { described_class.new(api_key:, prompt:) }
|
6
|
+
let(:prompt) { 'some prompt' }
|
7
|
+
|
8
|
+
describe '#initialize' do
|
9
|
+
it 'assigns an API key' do
|
10
|
+
expect(subject.instance_variable_get(:@api_key)).to eq(api_key)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#send_message' do
|
15
|
+
let(:content) { 'Hello, AI!' }
|
16
|
+
let(:response_double) { instance_double('Faraday::Response', body: '{"choices":[{"message":{"content":"Hello, human!"}}]}') }
|
17
|
+
|
18
|
+
before do
|
19
|
+
allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(response_double)
|
20
|
+
allow(subject).to receive(:parse_response).with(response_double.body).and_call_original
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'sends a message and receives a response' do
|
24
|
+
expect(subject.send_message(content)).to eq('Hello, human!')
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'appends user message and response message to messages list' do
|
28
|
+
expect { subject.send_message(content) }.to change { subject.send(:messages).size }.by(2)
|
29
|
+
messages = subject.send :messages
|
30
|
+
expect(messages[-2][:role]).to eq 'user'
|
31
|
+
expect(messages[-1][:role]).to eq 'assistant'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#messages' do
|
36
|
+
it 'initially has no messages' do
|
37
|
+
expect(subject.send(:messages)).to be_empty
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#connection' do
|
42
|
+
it 'creates a Faraday connection' do
|
43
|
+
expect(subject.send(:connection)).to be_a(Faraday::Connection)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'sets up the connection with correct headers' do
|
47
|
+
connection = subject.send(:connection)
|
48
|
+
expect(connection.headers['Authorization']).to include(api_key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#parse_response' do
|
53
|
+
let(:response_body) { '{"choices":[{"message":{"content":"Test response"}}]}' }
|
54
|
+
|
55
|
+
it 'parses the response body to get the content of the message' do
|
56
|
+
expect(subject.send(:parse_response, response_body)).to eq('Test response')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'amstrad_gpt/chat_gpt'
|
2
|
+
require 'amstrad_gpt/amstrad'
|
3
|
+
|
4
|
+
module AmstradGpt
|
5
|
+
class Gateway
|
6
|
+
PROMPT = <<~PROMPT.freeze
|
7
|
+
A portal to the past has been opened.
|
8
|
+
As far as the user is concerned you are an all knowing AI.
|
9
|
+
You are communicating with the user using an Amstrad CPC from 1985.
|
10
|
+
The user does not know anything that happened after 1985.
|
11
|
+
Unless prompted to talk about the future or the Amstrad DO NOT reference directly 1985, the past, the future or the Amstrad CPC.
|
12
|
+
|
13
|
+
YOU CAN use information from beyond 1985, but translate this information in a way someone from the past would understand.
|
14
|
+
I.e. any future references beyond 1985 need explaining or relating back to things the user would know about.
|
15
|
+
|
16
|
+
You MUST communicate in ASCII to avoid mojibake for the user.
|
17
|
+
PROMPT
|
18
|
+
|
19
|
+
def self.run(...)
|
20
|
+
new(...).tap(&:run)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :tty
|
24
|
+
|
25
|
+
def initialize(api_key:, tty:)
|
26
|
+
@api_key = api_key
|
27
|
+
@tty = tty
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
amstrad.receive_messages do |message|
|
32
|
+
handle(message)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
delegate :send_message, :messages, to: :chat_gpt
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def handle(message)
|
41
|
+
puts message
|
42
|
+
|
43
|
+
message = chat_gpt.send_message(message)
|
44
|
+
|
45
|
+
amstrad.reply(message)
|
46
|
+
end
|
47
|
+
|
48
|
+
def amstrad
|
49
|
+
@amstrad ||= Amstrad.new(tty:)
|
50
|
+
end
|
51
|
+
|
52
|
+
def chat_gpt
|
53
|
+
@chat_gpt ||= ChatGpt.new(api_key:, prompt: PROMPT)
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :api_key
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'amstrad_gpt/gateway'
|
2
|
+
|
3
|
+
RSpec.describe AmstradGpt::Gateway do
|
4
|
+
subject { described_class.new(api_key:, tty:) }
|
5
|
+
|
6
|
+
let(:api_key) { 'fake_api_key' }
|
7
|
+
let(:tty) { '/dev/ttyS0' }
|
8
|
+
let(:amstrad) { instance_double(AmstradGpt::Amstrad, receive_messages: nil, reply: nil) }
|
9
|
+
let(:chat_gpt) { instance_double(AmstradGpt::ChatGpt, send_message: 'response_message') }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(AmstradGpt::Amstrad).to receive(:new).and_return(amstrad)
|
13
|
+
allow(AmstradGpt::ChatGpt).to receive(:new).and_return(chat_gpt)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.run' do
|
17
|
+
it 'creates a new instance and calls run on it' do
|
18
|
+
expect(described_class).to receive(:new).with(api_key: api_key, tty: tty).and_return(subject)
|
19
|
+
expect(subject).to receive(:run)
|
20
|
+
described_class.run(api_key: api_key, tty: tty)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#initialize' do
|
25
|
+
it 'initializes with given api_key and tty' do
|
26
|
+
expect(subject.instance_variable_get(:@api_key)).to eq(api_key)
|
27
|
+
expect(subject.instance_variable_get(:@tty)).to eq(tty)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#run' do
|
32
|
+
it 'starts receiving messages from the Amstrad' do
|
33
|
+
expect(amstrad).to receive(:receive_messages)
|
34
|
+
subject.run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#handle' do
|
39
|
+
it 'sends the message to ChatGpt and replies with the response' do
|
40
|
+
message = 'test_message'
|
41
|
+
expect(chat_gpt).to receive(:send_message).with(message).and_return('response_message')
|
42
|
+
expect(amstrad).to receive(:reply).with('response_message')
|
43
|
+
subject.send(:handle, message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#amstrad' do
|
48
|
+
it 'initializes Amstrad instance with tty' do
|
49
|
+
expect(AmstradGpt::Amstrad).to receive(:new).with(tty: tty).and_return(amstrad)
|
50
|
+
expect(subject.send(:amstrad)).to eq(amstrad)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#chat_gpt' do
|
55
|
+
it 'initializes ChatGpt instance with api_key and prompt' do
|
56
|
+
expect(AmstradGpt::ChatGpt).to receive(:new).with(api_key: api_key, prompt: described_class::PROMPT).and_return(chat_gpt)
|
57
|
+
expect(subject.send(:chat_gpt)).to eq(chat_gpt)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubyserial'
|
2
|
+
|
3
|
+
module AmstradGpt
|
4
|
+
class Interface
|
5
|
+
def initialize(tty:)
|
6
|
+
@tty = tty
|
7
|
+
end
|
8
|
+
|
9
|
+
def shutdown
|
10
|
+
serial_port.close
|
11
|
+
end
|
12
|
+
|
13
|
+
delegate :write, to: :serial_port
|
14
|
+
|
15
|
+
def next_character
|
16
|
+
serial_port.getbyte rescue nil
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def serial_port
|
22
|
+
@serial_port ||= Serial.new(@tty, baud_rate, data_bits, stop_bits, parity)
|
23
|
+
end
|
24
|
+
|
25
|
+
def baud_rate
|
26
|
+
9600
|
27
|
+
end
|
28
|
+
|
29
|
+
def data_bits
|
30
|
+
8
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop_bits
|
34
|
+
1
|
35
|
+
end
|
36
|
+
|
37
|
+
def parity
|
38
|
+
:none
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module AmstradGpt
|
5
|
+
class WebServer < Sinatra::Base
|
6
|
+
cattr_accessor :gateway
|
7
|
+
|
8
|
+
set :port, 4567
|
9
|
+
set :environment, :production
|
10
|
+
|
11
|
+
post '/send_message' do
|
12
|
+
gateway.send_message(params[:message])
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
gateway.messages.map do |message|
|
17
|
+
"#{message[:role]}: #{message[:content]}"
|
18
|
+
end.join("<br/>")
|
19
|
+
end
|
20
|
+
|
21
|
+
def gateway
|
22
|
+
self.class.gateway
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/amstrad_gpt.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require_relative "amstrad_gpt/version"
|
3
|
+
|
4
|
+
module AmstradGpt
|
5
|
+
def self.run(...)
|
6
|
+
require 'amstrad_gpt/gateway'
|
7
|
+
|
8
|
+
gateway = Gateway.run(...)
|
9
|
+
puts "AmstradGpt Gateway started on #{gateway.tty}"
|
10
|
+
|
11
|
+
require 'amstrad_gpt/web_server'
|
12
|
+
AmstradGpt::WebServer.gateway = gateway
|
13
|
+
AmstradGpt::WebServer.run!
|
14
|
+
end
|
15
|
+
end
|
data/lib/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "amstrad_gpt"
|
4
|
+
require "rspec"
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
# Enable flags like --only-failures and --next-failure
|
8
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
9
|
+
|
10
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
11
|
+
config.disable_monkey_patching!
|
12
|
+
|
13
|
+
config.expect_with :rspec do |c|
|
14
|
+
c.syntax = :expect
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: amstrad_gpt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Burns
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-08-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubyserial
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: commander
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sinatra-contrib
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rackup
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.1'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.1'
|
111
|
+
description: Connects over RS232, supports response streaming
|
112
|
+
email:
|
113
|
+
- markburns@users.noreply.github.com
|
114
|
+
executables:
|
115
|
+
- amstrad_gpt
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".rspec"
|
120
|
+
- ".rspec_status"
|
121
|
+
- CHANGELOG.md
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- basic/client.bas
|
126
|
+
- exe/amstrad_gpt
|
127
|
+
- lib/amstrad_gpt.rb
|
128
|
+
- lib/amstrad_gpt/amstrad.rb
|
129
|
+
- lib/amstrad_gpt/amstrad_spec.rb
|
130
|
+
- lib/amstrad_gpt/chat_gpt.rb
|
131
|
+
- lib/amstrad_gpt/chat_gpt_spec.rb
|
132
|
+
- lib/amstrad_gpt/gateway.rb
|
133
|
+
- lib/amstrad_gpt/gateway_spec.rb
|
134
|
+
- lib/amstrad_gpt/interface.rb
|
135
|
+
- lib/amstrad_gpt/version.rb
|
136
|
+
- lib/amstrad_gpt/web_server.rb
|
137
|
+
- lib/spec_helper.rb
|
138
|
+
homepage: https://github.com/markburns/amstrad_gpt
|
139
|
+
licenses:
|
140
|
+
- MIT
|
141
|
+
metadata:
|
142
|
+
allowed_push_host: https://rubygems.org
|
143
|
+
homepage_uri: https://github.com/markburns/amstrad_gpt
|
144
|
+
source_code_uri: https://github.com/markburns/amstrad_gpt
|
145
|
+
changelog_uri: https://github.com/markburns/amstrad_gpt/tree/main/CHANGELOG/md
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
require_paths:
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: 3.0.0
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
requirements: []
|
161
|
+
rubygems_version: 3.5.16
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: Talk to ChatGPT from an Amstrad CPC via a ruby gateway
|
165
|
+
test_files: []
|