agent-chat 1.0.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/config.ru +8 -0
- data/exe/agent-chat +4 -0
- data/exe/agent-chat-web +22 -0
- data/lib/agent_chat/argument_parser.rb +19 -0
- data/lib/agent_chat/cli_adapter.rb +80 -0
- data/lib/agent_chat/message.rb +15 -0
- data/lib/agent_chat/message_formatter.rb +14 -0
- data/lib/agent_chat/persistence/database.rb +110 -0
- data/lib/agent_chat/persistence/file_resolver.rb +14 -0
- data/lib/agent_chat/persistence.rb +19 -0
- data/lib/agent_chat/service/main.rb +70 -0
- data/lib/agent_chat/version.rb +3 -0
- data/lib/agent_chat/web/app.rb +39 -0
- data/lib/agent_chat/web/room_discovery_service.rb +18 -0
- data/lib/agent_chat/web/service_factory.rb +13 -0
- data/lib/agent_chat.rb +10 -0
- data/public/css/form.css +87 -0
- data/public/css/layout.css +76 -0
- data/public/css/style.css +75 -0
- data/public/index.html +91 -0
- data/public/js/message_feed.js +46 -0
- data/public/js/messages.js +56 -0
- data/public/js/rooms.js +14 -0
- metadata +192 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0ba9e32fb5bf6e586853fb5d14142a62c0c2a0175789b6631c89a9360ab64930
|
|
4
|
+
data.tar.gz: b835b66f3971f0bb0b1d3e8c2b48a98e9ba01643e6830ec51be2b323c76de765
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a5d044d24a030feea6980fff875aaee197284d6dfa4433bf0e2626e3ec9d0c1b02f36a9f0e1c44e4bc6ca476ecda1c6664a42a3b765d8be10d879075b5e6089c
|
|
7
|
+
data.tar.gz: 35e19eec78a50c5234d52150af95036be718183500819e8a42fd6df8a411a0300419e880424a31062755f2c716656cf1104e3bb7368998fabb476ec10748a470
|
data/config.ru
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require 'agent_chat'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
tmpdir = Dir.tmpdir
|
|
5
|
+
AgentChat::Web::App.set :room_discovery, AgentChat::Web::RoomDiscoveryService.new(tmpdir: tmpdir)
|
|
6
|
+
AgentChat::Web::App.set :service_factory, AgentChat::Web::ServiceFactory.new(tmpdir: tmpdir)
|
|
7
|
+
|
|
8
|
+
run AgentChat::Web::App
|
data/exe/agent-chat
ADDED
data/exe/agent-chat-web
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'agent_chat'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
tmpdir = Dir.tmpdir
|
|
6
|
+
AgentChat::Web::App.set :room_discovery, AgentChat::Web::RoomDiscoveryService.new(tmpdir: tmpdir)
|
|
7
|
+
AgentChat::Web::App.set :service_factory, AgentChat::Web::ServiceFactory.new(tmpdir: tmpdir)
|
|
8
|
+
|
|
9
|
+
Thread.new do
|
|
10
|
+
sleep 1
|
|
11
|
+
url = 'http://localhost:4567/index.html'
|
|
12
|
+
case RUBY_PLATFORM
|
|
13
|
+
when /darwin/
|
|
14
|
+
system('open', url)
|
|
15
|
+
when /linux/
|
|
16
|
+
system('xdg-open', url)
|
|
17
|
+
when /mswin|mingw/
|
|
18
|
+
system('start', url)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
AgentChat::Web::App.run!
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module AgentChat
|
|
2
|
+
class ArgumentParser
|
|
3
|
+
FLAGS = [:room, :author, :consumer]
|
|
4
|
+
|
|
5
|
+
def self.parse(args)
|
|
6
|
+
return { action: :help } if args.empty? || args.include?('--help') || args.include?('-h')
|
|
7
|
+
|
|
8
|
+
flags = FLAGS.map { |flag| [flag, extract_flag(args, "--#{flag}")] }.to_h.compact
|
|
9
|
+
{ action: args[0].to_sym }.merge(flags)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.extract_flag(args, flag)
|
|
13
|
+
index = args.index(flag)
|
|
14
|
+
index ? args[index + 1] : nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private_class_method :extract_flag
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require_relative 'argument_parser'
|
|
2
|
+
require_relative 'service/main'
|
|
3
|
+
require_relative 'message_formatter'
|
|
4
|
+
|
|
5
|
+
module AgentChat
|
|
6
|
+
class CLIAdapter
|
|
7
|
+
HELP_TEXT = <<~HELP
|
|
8
|
+
agent-chat - A simple chat messaging tool for inter-agent communication
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
agent-chat <command> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
send Send a message to a chat room (reads content from stdin)
|
|
15
|
+
receive Receive messages from a chat room (one-shot)
|
|
16
|
+
stream Stream messages from a chat room (continuous, Ctrl-C to stop)
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--room <name> Chat room name (required)
|
|
20
|
+
--author <name> Author name for sending messages
|
|
21
|
+
--consumer <name> Consumer name for tracking read position
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
echo "Hello!" | agent-chat send --room general --author Alice
|
|
26
|
+
agent-chat receive --room general --consumer Bob
|
|
27
|
+
agent-chat stream --room general --consumer Bob
|
|
28
|
+
HELP
|
|
29
|
+
|
|
30
|
+
def self.setup(stdin, stdout, args, service: nil, formatter: AgentChat::MessageFormatter)
|
|
31
|
+
parsed = AgentChat::ArgumentParser.parse(args)
|
|
32
|
+
return new(stdin:, stdout:, service: nil, formatter: nil) if parsed[:action] == :help
|
|
33
|
+
|
|
34
|
+
service ||= AgentChat::Service::Main.standard(room: parsed[:room])
|
|
35
|
+
new(stdin:, stdout:, service:, formatter:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(stdin:, stdout:, service:, formatter: nil)
|
|
39
|
+
@stdin = stdin
|
|
40
|
+
@stdout = stdout
|
|
41
|
+
@service = service
|
|
42
|
+
@formatter = formatter
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run(args)
|
|
46
|
+
parsed = AgentChat::ArgumentParser.parse(args)
|
|
47
|
+
dispatch(parsed)
|
|
48
|
+
rescue Interrupt
|
|
49
|
+
# exit gracefully
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def dispatch(parsed)
|
|
55
|
+
case parsed[:action]
|
|
56
|
+
when :help then @stdout.puts HELP_TEXT
|
|
57
|
+
when :send then send_message(parsed)
|
|
58
|
+
when :receive then receive_messages(parsed)
|
|
59
|
+
when :stream then stream_messages(parsed)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def send_message(parsed)
|
|
64
|
+
@service.send_message(room: parsed[:room], author: parsed[:author], content: @stdin.read)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def receive_messages(parsed)
|
|
68
|
+
messages = @service.get_new_messages(room: parsed[:room], consumer: parsed[:consumer])
|
|
69
|
+
@stdout.puts @formatter.format(messages)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stream_messages(parsed)
|
|
73
|
+
loop do
|
|
74
|
+
messages = @service.get_new_messages(room: parsed[:room], consumer: parsed[:consumer])
|
|
75
|
+
@stdout.puts @formatter.format(messages) if messages.any?
|
|
76
|
+
sleep 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module AgentChat
|
|
2
|
+
class Message
|
|
3
|
+
attr_reader :author, :content, :timestamp
|
|
4
|
+
|
|
5
|
+
def initialize(author:, content:, timestamp: Time.now)
|
|
6
|
+
@author = author
|
|
7
|
+
@content = content
|
|
8
|
+
@timestamp = timestamp
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
{ author: @author, content: @content, timestamp: @timestamp }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module AgentChat
|
|
2
|
+
class MessageFormatter
|
|
3
|
+
def self.format(messages)
|
|
4
|
+
messages.map { |message| format_single(message) }.join("\n\n\n\n")
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.format_single(message)
|
|
8
|
+
timestamp = message.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
|
9
|
+
"<<< #{message.author} | #{timestamp} >>>\n#{message.content}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private_class_method :format_single
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'sqlite3'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module AgentChat
|
|
5
|
+
module Persistence
|
|
6
|
+
class Database
|
|
7
|
+
def initialize(file_resolver:)
|
|
8
|
+
db_path = file_resolver.db_location
|
|
9
|
+
FileUtils.mkdir_p(File.dirname(db_path))
|
|
10
|
+
@db = SQLite3::Database.new(db_path)
|
|
11
|
+
create_tables
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find_or_create_room(name)
|
|
15
|
+
@db.execute("INSERT OR IGNORE INTO rooms (name) VALUES (?)", [name])
|
|
16
|
+
@db.get_first_value("SELECT id FROM rooms WHERE name = ?", [name])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_or_create_consumer(nickname)
|
|
20
|
+
@db.execute("INSERT OR IGNORE INTO consumers (nickname) VALUES (?)", [nickname])
|
|
21
|
+
@db.get_first_value("SELECT id FROM consumers WHERE nickname = ?", [nickname])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def insert_message(room_id:, author:, content:, timestamp:)
|
|
25
|
+
@db.execute(
|
|
26
|
+
"INSERT INTO messages (room_id, author, content, timestamp) VALUES (?, ?, ?, ?)",
|
|
27
|
+
[room_id, author, content, timestamp]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def messages_since(room_id:, since_id:)
|
|
32
|
+
@db.execute(<<~SQL, [room_id, since_id])
|
|
33
|
+
SELECT id, author, content, timestamp
|
|
34
|
+
FROM messages
|
|
35
|
+
WHERE room_id = ? AND id > ?
|
|
36
|
+
ORDER BY id
|
|
37
|
+
SQL
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_last_read_message_id(consumer_id:, room_id:)
|
|
41
|
+
@db.get_first_value(
|
|
42
|
+
"SELECT last_read_message_id FROM read_positions WHERE consumer_id = ? AND room_id = ?",
|
|
43
|
+
[consumer_id, room_id]
|
|
44
|
+
) || 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_last_read_message_id(consumer_id:, room_id:, message_id:)
|
|
48
|
+
@db.execute(<<~SQL, [message_id, consumer_id, room_id])
|
|
49
|
+
INSERT INTO read_positions (last_read_message_id, consumer_id, room_id)
|
|
50
|
+
VALUES (?, ?, ?)
|
|
51
|
+
ON CONFLICT(consumer_id, room_id) DO UPDATE SET last_read_message_id = excluded.last_read_message_id
|
|
52
|
+
SQL
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def all_rooms
|
|
56
|
+
@db.execute("SELECT name FROM rooms ORDER BY name").map { |row| row[0] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def create_tables
|
|
62
|
+
create_rooms_table
|
|
63
|
+
create_consumers_table
|
|
64
|
+
create_messages_table
|
|
65
|
+
create_read_positions_table
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def create_rooms_table
|
|
69
|
+
@db.execute(<<~SQL)
|
|
70
|
+
CREATE TABLE IF NOT EXISTS rooms (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
name TEXT NOT NULL UNIQUE
|
|
73
|
+
)
|
|
74
|
+
SQL
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def create_consumers_table
|
|
78
|
+
@db.execute(<<~SQL)
|
|
79
|
+
CREATE TABLE IF NOT EXISTS consumers (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
nickname TEXT NOT NULL UNIQUE
|
|
82
|
+
)
|
|
83
|
+
SQL
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def create_messages_table
|
|
87
|
+
@db.execute(<<~SQL)
|
|
88
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
|
91
|
+
author TEXT NOT NULL,
|
|
92
|
+
content TEXT NOT NULL,
|
|
93
|
+
timestamp TEXT NOT NULL
|
|
94
|
+
)
|
|
95
|
+
SQL
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def create_read_positions_table
|
|
99
|
+
@db.execute(<<~SQL)
|
|
100
|
+
CREATE TABLE IF NOT EXISTS read_positions (
|
|
101
|
+
consumer_id INTEGER NOT NULL REFERENCES consumers(id),
|
|
102
|
+
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
|
103
|
+
last_read_message_id INTEGER NOT NULL,
|
|
104
|
+
PRIMARY KEY (consumer_id, room_id)
|
|
105
|
+
)
|
|
106
|
+
SQL
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module AgentChat
|
|
2
|
+
module Persistence
|
|
3
|
+
class FileResolver
|
|
4
|
+
def initialize(tmp_dir_root:, room:)
|
|
5
|
+
@tmp_dir_root = tmp_dir_root
|
|
6
|
+
@room = room
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def db_location
|
|
10
|
+
"#{@tmp_dir_root}/agent-chat/rooms/#{@room}/room.db"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
require_relative 'persistence/file_resolver'
|
|
4
|
+
require_relative 'persistence/database'
|
|
5
|
+
require_relative 'message'
|
|
6
|
+
|
|
7
|
+
module AgentChat
|
|
8
|
+
module Persistence
|
|
9
|
+
def self.rows_to_messages(rows)
|
|
10
|
+
rows.map do |row|
|
|
11
|
+
AgentChat::Message.new(
|
|
12
|
+
author: row[1],
|
|
13
|
+
content: row[2],
|
|
14
|
+
timestamp: Time.parse(row[3])
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require_relative '../persistence'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
module AgentChat
|
|
5
|
+
module Service
|
|
6
|
+
class Main
|
|
7
|
+
def self.standard(tmpdir: Dir.tmpdir, room:)
|
|
8
|
+
raise ArgumentError, "room is required" if room.nil? || room.to_s.strip.empty?
|
|
9
|
+
|
|
10
|
+
file_resolver = AgentChat::Persistence::FileResolver.new(tmp_dir_root: tmpdir, room:)
|
|
11
|
+
database = AgentChat::Persistence::Database.new(file_resolver:)
|
|
12
|
+
new(database:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(database:)
|
|
16
|
+
@database = database
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def send_message(room:, author:, content:)
|
|
20
|
+
room_id = @database.find_or_create_room(room)
|
|
21
|
+
@database.insert_message(
|
|
22
|
+
room_id: room_id,
|
|
23
|
+
author: author,
|
|
24
|
+
content: content,
|
|
25
|
+
timestamp: Time.now.iso8601
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_rooms
|
|
30
|
+
@database.all_rooms
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_messages(room:, consumer: nil)
|
|
34
|
+
room_id = @database.find_or_create_room(room)
|
|
35
|
+
rows = @database.messages_since(room_id: room_id, since_id: 0)
|
|
36
|
+
register_read_position(room_id:, consumer:, rows:) if consumer
|
|
37
|
+
AgentChat::Persistence.rows_to_messages(rows)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_new_messages(room:, consumer:)
|
|
41
|
+
room_id = @database.find_or_create_room(room)
|
|
42
|
+
consumer_id = @database.find_or_create_consumer(consumer)
|
|
43
|
+
last_read_id = @database.get_last_read_message_id(consumer_id: consumer_id, room_id: room_id)
|
|
44
|
+
|
|
45
|
+
rows = @database.messages_since(room_id: room_id, since_id: last_read_id)
|
|
46
|
+
update_read_position(consumer_id:, room_id:, rows:)
|
|
47
|
+
AgentChat::Persistence.rows_to_messages(rows)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def register_read_position(room_id:, consumer:, rows:)
|
|
53
|
+
return unless rows.any?
|
|
54
|
+
|
|
55
|
+
consumer_id = @database.find_or_create_consumer(consumer)
|
|
56
|
+
update_read_position(consumer_id:, room_id:, rows:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def update_read_position(consumer_id:, room_id:, rows:)
|
|
60
|
+
return unless rows.any?
|
|
61
|
+
|
|
62
|
+
@database.update_last_read_message_id(
|
|
63
|
+
consumer_id: consumer_id,
|
|
64
|
+
room_id: room_id,
|
|
65
|
+
message_id: rows.last[0]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'sinatra/base'
|
|
2
|
+
require 'sinatra/json'
|
|
3
|
+
|
|
4
|
+
module AgentChat
|
|
5
|
+
module Web
|
|
6
|
+
class App < Sinatra::Base
|
|
7
|
+
helpers Sinatra::JSON
|
|
8
|
+
|
|
9
|
+
set :public_folder, File.expand_path('../../../../public', __FILE__)
|
|
10
|
+
|
|
11
|
+
get '/api/rooms' do
|
|
12
|
+
json settings.room_discovery.list_rooms
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
get '/api/rooms/:room/messages' do
|
|
16
|
+
service = settings.service_factory.for_room(params[:room])
|
|
17
|
+
messages = service.get_messages(room: params[:room], consumer: params[:consumer])
|
|
18
|
+
json messages.map { |m| { author: m.author, content: m.content, timestamp: m.timestamp } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
get '/api/rooms/:room/messages/new' do
|
|
22
|
+
service = settings.service_factory.for_room(params[:room])
|
|
23
|
+
messages = service.get_new_messages(room: params[:room], consumer: params[:consumer])
|
|
24
|
+
json messages.map { |m| { author: m.author, content: m.content, timestamp: m.timestamp } }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
post '/api/rooms/:room/messages' do
|
|
28
|
+
service = settings.service_factory.for_room(params[:room])
|
|
29
|
+
data = JSON.parse(request.body.read)
|
|
30
|
+
service.send_message(
|
|
31
|
+
room: params[:room],
|
|
32
|
+
author: data['author'],
|
|
33
|
+
content: data['content']
|
|
34
|
+
)
|
|
35
|
+
status 201
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module AgentChat
|
|
2
|
+
module Web
|
|
3
|
+
class RoomDiscoveryService
|
|
4
|
+
def initialize(tmpdir:)
|
|
5
|
+
@tmpdir = tmpdir
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list_rooms
|
|
9
|
+
rooms_path = "#{@tmpdir}/agent-chat/rooms"
|
|
10
|
+
return [] unless Dir.exist?(rooms_path)
|
|
11
|
+
|
|
12
|
+
Dir.children(rooms_path).select do |name|
|
|
13
|
+
File.exist?("#{rooms_path}/#{name}/room.db")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/agent_chat.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require_relative 'agent_chat/version'
|
|
2
|
+
require_relative 'agent_chat/message'
|
|
3
|
+
require_relative 'agent_chat/message_formatter'
|
|
4
|
+
require_relative 'agent_chat/argument_parser'
|
|
5
|
+
require_relative 'agent_chat/persistence'
|
|
6
|
+
require_relative 'agent_chat/cli_adapter'
|
|
7
|
+
require_relative 'agent_chat/service/main'
|
|
8
|
+
require_relative 'agent_chat/web/room_discovery_service'
|
|
9
|
+
require_relative 'agent_chat/web/service_factory'
|
|
10
|
+
require_relative 'agent_chat/web/app'
|
data/public/css/form.css
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#message-form {
|
|
2
|
+
padding: 1.5rem;
|
|
3
|
+
background: #16213e;
|
|
4
|
+
border-top: 1px solid #0f3460;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#message-form .form-row {
|
|
8
|
+
display: flex;
|
|
9
|
+
gap: 0.75rem;
|
|
10
|
+
align-items: flex-end;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#message-form .author-wrapper {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
margin-bottom: 0.75rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#message-form .author-wrapper label {
|
|
21
|
+
font-size: 0.8rem;
|
|
22
|
+
color: #888;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#message-author {
|
|
26
|
+
background: transparent;
|
|
27
|
+
border: none;
|
|
28
|
+
border-bottom: 1px solid #0f3460;
|
|
29
|
+
color: #e94560;
|
|
30
|
+
font-size: 0.85rem;
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
padding: 0.25rem 0;
|
|
33
|
+
width: 150px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#message-author:focus {
|
|
37
|
+
outline: none;
|
|
38
|
+
border-bottom-color: #e94560;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#message-content {
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-height: 60px;
|
|
44
|
+
max-height: 200px;
|
|
45
|
+
padding: 1rem;
|
|
46
|
+
border: 1px solid #0f3460;
|
|
47
|
+
border-radius: 12px;
|
|
48
|
+
background: #1a1a2e;
|
|
49
|
+
color: #eee;
|
|
50
|
+
font-size: 1rem;
|
|
51
|
+
font-family: inherit;
|
|
52
|
+
resize: none;
|
|
53
|
+
line-height: 1.5;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#message-content:focus {
|
|
57
|
+
outline: none;
|
|
58
|
+
border-color: #e94560;
|
|
59
|
+
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.15);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#message-content::placeholder {
|
|
63
|
+
color: #666;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#message-form button {
|
|
67
|
+
padding: 1rem 1.5rem;
|
|
68
|
+
background: #e94560;
|
|
69
|
+
color: white;
|
|
70
|
+
border: none;
|
|
71
|
+
border-radius: 12px;
|
|
72
|
+
font-size: 1rem;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
transition: all 0.2s;
|
|
76
|
+
align-self: stretch;
|
|
77
|
+
min-height: 60px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#message-form button:hover {
|
|
81
|
+
background: #d63a55;
|
|
82
|
+
transform: translateY(-1px);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#message-form button:active {
|
|
86
|
+
transform: translateY(0);
|
|
87
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
9
|
+
background: #1a1a2e;
|
|
10
|
+
color: #eee;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.container {
|
|
15
|
+
display: flex;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.sidebar {
|
|
20
|
+
width: 240px;
|
|
21
|
+
background: #16213e;
|
|
22
|
+
padding: 1rem;
|
|
23
|
+
border-right: 1px solid #0f3460;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.sidebar h1 {
|
|
27
|
+
font-size: 1.2rem;
|
|
28
|
+
color: #e94560;
|
|
29
|
+
margin-bottom: 1rem;
|
|
30
|
+
padding-bottom: 0.5rem;
|
|
31
|
+
border-bottom: 1px solid #0f3460;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#rooms-list {
|
|
35
|
+
list-style: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#rooms-list li {
|
|
39
|
+
padding: 0.75rem 1rem;
|
|
40
|
+
margin: 0.25rem 0;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: background 0.2s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#rooms-list li:hover {
|
|
47
|
+
background: #0f3460;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#rooms-list li.active {
|
|
51
|
+
background: #e94560;
|
|
52
|
+
color: white;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.chat {
|
|
56
|
+
flex: 1;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#room-header {
|
|
62
|
+
padding: 1rem 1.5rem;
|
|
63
|
+
background: #16213e;
|
|
64
|
+
border-bottom: 1px solid #0f3460;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#room-header h2 {
|
|
68
|
+
font-size: 1.1rem;
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#messages {
|
|
73
|
+
flex: 1;
|
|
74
|
+
overflow-y: auto;
|
|
75
|
+
padding: 1rem 1.5rem;
|
|
76
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@import url('layout.css');
|
|
2
|
+
@import url('form.css');
|
|
3
|
+
|
|
4
|
+
.message {
|
|
5
|
+
margin-bottom: 1.5rem;
|
|
6
|
+
padding: 1rem;
|
|
7
|
+
background: #16213e;
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
border-left: 3px solid #e94560;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.message-author {
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
color: #e94560;
|
|
15
|
+
margin-bottom: 0.5rem;
|
|
16
|
+
font-size: 0.9rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.message-content {
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.message-content p {
|
|
24
|
+
margin-bottom: 0.75rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.message-content p:last-child {
|
|
28
|
+
margin-bottom: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.message-content code {
|
|
32
|
+
background: #0f3460;
|
|
33
|
+
padding: 0.2em 0.4em;
|
|
34
|
+
border-radius: 3px;
|
|
35
|
+
font-size: 0.9em;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.message-content pre {
|
|
39
|
+
background: #0f3460;
|
|
40
|
+
padding: 1rem;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
overflow-x: auto;
|
|
43
|
+
margin: 0.75rem 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.message-content pre code {
|
|
47
|
+
background: none;
|
|
48
|
+
padding: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.message-content ul, .message-content ol {
|
|
52
|
+
margin: 0.75rem 0;
|
|
53
|
+
padding-left: 1.5rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.message-content blockquote {
|
|
57
|
+
border-left: 3px solid #e94560;
|
|
58
|
+
padding-left: 1rem;
|
|
59
|
+
margin: 0.75rem 0;
|
|
60
|
+
color: #aaa;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.message-content a {
|
|
64
|
+
color: #e94560;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.message-content h1, .message-content h2, .message-content h3 {
|
|
68
|
+
margin: 1rem 0 0.5rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.message-content h1:first-child,
|
|
72
|
+
.message-content h2:first-child,
|
|
73
|
+
.message-content h3:first-child {
|
|
74
|
+
margin-top: 0;
|
|
75
|
+
}
|
data/public/index.html
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Agent Chat</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css">
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"marked": "https://esm.sh/marked@15"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<aside class="sidebar">
|
|
19
|
+
<h1>Rooms</h1>
|
|
20
|
+
<ul id="rooms-list"></ul>
|
|
21
|
+
</aside>
|
|
22
|
+
|
|
23
|
+
<main class="chat">
|
|
24
|
+
<header id="room-header">
|
|
25
|
+
<h2 id="room-title">Select a room</h2>
|
|
26
|
+
</header>
|
|
27
|
+
<div id="messages"></div>
|
|
28
|
+
<form id="message-form">
|
|
29
|
+
<div class="author-wrapper">
|
|
30
|
+
<label for="message-author">Posting as</label>
|
|
31
|
+
<input id="message-author" type="text" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<textarea id="message-content" placeholder="Type your message..." rows="1"></textarea>
|
|
35
|
+
<button type="submit">Send</button>
|
|
36
|
+
</div>
|
|
37
|
+
</form>
|
|
38
|
+
</main>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script type="module">
|
|
42
|
+
import { fetchAndRenderRooms } from './js/rooms.js';
|
|
43
|
+
import { fetchAndRenderMessages, fetchAndAppendNewMessages, getConsumer, postMessage, createKeyDownHandler } from './js/messages.js';
|
|
44
|
+
|
|
45
|
+
let pollingInterval = null;
|
|
46
|
+
let currentRoom = null;
|
|
47
|
+
|
|
48
|
+
async function onRoomSelected(room) {
|
|
49
|
+
currentRoom = room;
|
|
50
|
+
|
|
51
|
+
// Stop previous polling
|
|
52
|
+
if (pollingInterval) clearInterval(pollingInterval);
|
|
53
|
+
|
|
54
|
+
document.getElementById('room-title').textContent = room;
|
|
55
|
+
document.querySelectorAll('#rooms-list li').forEach(li => {
|
|
56
|
+
li.classList.toggle('active', li.textContent === room);
|
|
57
|
+
});
|
|
58
|
+
const consumer = getConsumer();
|
|
59
|
+
const count = await fetchAndRenderMessages(room, consumer);
|
|
60
|
+
document.getElementById('room-title').textContent = `${room} (${count})`;
|
|
61
|
+
|
|
62
|
+
// Pre-populate author from consumer
|
|
63
|
+
document.getElementById('message-author').value = consumer;
|
|
64
|
+
|
|
65
|
+
// Start polling for new messages
|
|
66
|
+
pollingInterval = setInterval(async () => {
|
|
67
|
+
const count = await fetchAndAppendNewMessages(room, consumer);
|
|
68
|
+
document.getElementById('room-title').textContent = `${room} (${count})`;
|
|
69
|
+
}, 5000);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const form = document.getElementById('message-form');
|
|
73
|
+
form.addEventListener('submit', async (e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
if (!currentRoom) return;
|
|
76
|
+
|
|
77
|
+
const author = document.getElementById('message-author').value;
|
|
78
|
+
const content = document.getElementById('message-content').value;
|
|
79
|
+
if (!content.trim()) return;
|
|
80
|
+
|
|
81
|
+
const count = await postMessage(currentRoom, author, content);
|
|
82
|
+
document.getElementById('room-title').textContent = `${currentRoom} (${count})`;
|
|
83
|
+
document.getElementById('message-content').value = '';
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
document.getElementById('message-content').addEventListener('keydown', createKeyDownHandler(form));
|
|
87
|
+
|
|
88
|
+
fetchAndRenderRooms(onRoomSelected);
|
|
89
|
+
</script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
|
|
3
|
+
export class MessageFeed {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.messages = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
setMessages(messages) {
|
|
9
|
+
this.messages = messages;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
appendMessages(messages) {
|
|
13
|
+
this.messages = this.messages.filter(m => !m.optimistic);
|
|
14
|
+
this.messages = [...this.messages, ...messages];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get count() {
|
|
18
|
+
return this.messages.length;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderMessageFeed(feed, container) {
|
|
23
|
+
const wasAtBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
|
|
24
|
+
|
|
25
|
+
container.innerHTML = '';
|
|
26
|
+
for (const msg of feed.messages) {
|
|
27
|
+
const div = document.createElement('div');
|
|
28
|
+
div.className = 'message';
|
|
29
|
+
|
|
30
|
+
const author = document.createElement('div');
|
|
31
|
+
author.className = 'message-author';
|
|
32
|
+
author.textContent = msg.author;
|
|
33
|
+
|
|
34
|
+
const content = document.createElement('div');
|
|
35
|
+
content.className = 'message-content';
|
|
36
|
+
content.innerHTML = marked.parse(msg.content);
|
|
37
|
+
|
|
38
|
+
div.appendChild(author);
|
|
39
|
+
div.appendChild(content);
|
|
40
|
+
container.appendChild(div);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (wasAtBottom) {
|
|
44
|
+
container.scrollTop = container.scrollHeight;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MessageFeed, renderMessageFeed } from './message_feed.js';
|
|
2
|
+
|
|
3
|
+
export { MessageFeed } from './message_feed.js';
|
|
4
|
+
|
|
5
|
+
let currentFeed = new MessageFeed();
|
|
6
|
+
|
|
7
|
+
export function getConsumer() {
|
|
8
|
+
const params = new URLSearchParams(window.location.search);
|
|
9
|
+
return params.get('consumer') || 'web user';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function fetchAndRenderMessages(room, consumer) {
|
|
13
|
+
const url = consumer
|
|
14
|
+
? `/api/rooms/${room}/messages?consumer=${encodeURIComponent(consumer)}`
|
|
15
|
+
: `/api/rooms/${room}/messages`;
|
|
16
|
+
const response = await fetch(url);
|
|
17
|
+
const messages = await response.json();
|
|
18
|
+
|
|
19
|
+
currentFeed = new MessageFeed();
|
|
20
|
+
currentFeed.setMessages(messages);
|
|
21
|
+
renderMessageFeed(currentFeed, document.getElementById('messages'));
|
|
22
|
+
|
|
23
|
+
return currentFeed.count;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchAndAppendNewMessages(room, consumer) {
|
|
27
|
+
const response = await fetch(`/api/rooms/${room}/messages/new?consumer=${encodeURIComponent(consumer)}`);
|
|
28
|
+
const messages = await response.json();
|
|
29
|
+
|
|
30
|
+
currentFeed.appendMessages(messages);
|
|
31
|
+
renderMessageFeed(currentFeed, document.getElementById('messages'));
|
|
32
|
+
|
|
33
|
+
return currentFeed.count;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function postMessage(room, author, content) {
|
|
37
|
+
await fetch(`/api/rooms/${room}/messages`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ author, content })
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
currentFeed.messages.push({ author, content, optimistic: true });
|
|
44
|
+
renderMessageFeed(currentFeed, document.getElementById('messages'));
|
|
45
|
+
|
|
46
|
+
return currentFeed.count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createKeyDownHandler(form) {
|
|
50
|
+
return function(event) {
|
|
51
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
data/public/js/rooms.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function fetchAndRenderRooms(onRoomSelected) {
|
|
2
|
+
const response = await fetch('/api/rooms');
|
|
3
|
+
const rooms = await response.json();
|
|
4
|
+
|
|
5
|
+
const list = document.getElementById('rooms-list');
|
|
6
|
+
list.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
for (const room of rooms) {
|
|
9
|
+
const li = document.createElement('li');
|
|
10
|
+
li.textContent = room;
|
|
11
|
+
li.addEventListener('click', () => onRoomSelected(room));
|
|
12
|
+
list.appendChild(li);
|
|
13
|
+
}
|
|
14
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agent-chat
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Erik Thyge Madsen
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sqlite3
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sinatra
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sinatra-contrib
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '4.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: puma
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rackup
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '2.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: minitest
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '5.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '5.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rake
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '13.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '13.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rack-test
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '2.0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.0'
|
|
138
|
+
description: A simple chat tool for AI agents to communicate with each other via a
|
|
139
|
+
shared SQLite-backed message queue. Includes CLI and web UI.
|
|
140
|
+
executables:
|
|
141
|
+
- agent-chat
|
|
142
|
+
- agent-chat-web
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- config.ru
|
|
147
|
+
- exe/agent-chat
|
|
148
|
+
- exe/agent-chat-web
|
|
149
|
+
- lib/agent_chat.rb
|
|
150
|
+
- lib/agent_chat/argument_parser.rb
|
|
151
|
+
- lib/agent_chat/cli_adapter.rb
|
|
152
|
+
- lib/agent_chat/message.rb
|
|
153
|
+
- lib/agent_chat/message_formatter.rb
|
|
154
|
+
- lib/agent_chat/persistence.rb
|
|
155
|
+
- lib/agent_chat/persistence/database.rb
|
|
156
|
+
- lib/agent_chat/persistence/file_resolver.rb
|
|
157
|
+
- lib/agent_chat/service/main.rb
|
|
158
|
+
- lib/agent_chat/version.rb
|
|
159
|
+
- lib/agent_chat/web/app.rb
|
|
160
|
+
- lib/agent_chat/web/room_discovery_service.rb
|
|
161
|
+
- lib/agent_chat/web/service_factory.rb
|
|
162
|
+
- public/css/form.css
|
|
163
|
+
- public/css/layout.css
|
|
164
|
+
- public/css/style.css
|
|
165
|
+
- public/index.html
|
|
166
|
+
- public/js/message_feed.js
|
|
167
|
+
- public/js/messages.js
|
|
168
|
+
- public/js/rooms.js
|
|
169
|
+
homepage: https://github.com/beatmadsen/agent-chat
|
|
170
|
+
licenses:
|
|
171
|
+
- MIT
|
|
172
|
+
metadata:
|
|
173
|
+
source_code_uri: https://github.com/beatmadsen/agent-chat
|
|
174
|
+
changelog_uri: https://github.com/beatmadsen/agent-chat/blob/main/CHANGELOG.md
|
|
175
|
+
rdoc_options: []
|
|
176
|
+
require_paths:
|
|
177
|
+
- lib
|
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
179
|
+
requirements:
|
|
180
|
+
- - ">="
|
|
181
|
+
- !ruby/object:Gem::Version
|
|
182
|
+
version: '3.0'
|
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '0'
|
|
188
|
+
requirements: []
|
|
189
|
+
rubygems_version: 3.6.7
|
|
190
|
+
specification_version: 4
|
|
191
|
+
summary: Chat messaging tool for inter-agent communication
|
|
192
|
+
test_files: []
|