amstrad_gpt 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|