backend: separate App and server, add websocket endpoint, accept data directory command argument. initialize sqlalchemy database and models in new module. frontend: create backend class for managing ws connection, move ui to chat_screen, create scaffold chat and agent classes.

This commit is contained in:
Shivering Ocelot committed 2025-05-06 17:50:57 -05:00
1 parent 1e82796db1
commit 44f0b3589c
19 files changed
+935 -121

No files matched your search

+1
View File
@@ -1 +1,2 @@
.venv
horde.db
+2
View File
@@ -14,3 +14,5 @@ web: frontend/build/web/index.html
frontend/build/web/index.html:
cd frontend && flutter build web
frontend/build/web/index.html: $(shell find frontend/lib -name '*.dart')
Binary file not shown.
+129 -9
View File
@@ -1,14 +1,134 @@
from quart import Quart, send_from_directory
import sys
import json
import asyncio
app = Quart(__name__, static_folder='../frontend/build/web')
from database import create_sql_engine, User, Agent
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
from quart import Quart, Response, request, send_from_directory, jsonify, websocket
@app.route('/')
async def hello():
return await send_from_directory(app.static_folder, 'index.html')
class App:
def __init__(self, data_directory):
self.data_directory = data_directory
self.sql_engine = create_sql_engine(self.data_directory)
@app.route('/<path:path>')
async def serve_static(path):
return await send_from_directory(app.static_folder, path)
def sql_session(self):
return Session(self.sql_engine)
app = App("./")
def start_server(app):
server = Quart(__name__, static_folder='../frontend/build/web')
@server.route('/')
async def index():
return await send_from_directory(server.static_folder, 'index.html')
@server.route('/<path:path>')
async def serve_static(path):
return await send_from_directory(server.static_folder, path)
@server.route('/user/signup', methods=['POST'])
async def signup():
data = await request.get_json()
user_id = None
username = data['username']
email = data['email']
password = data['password']
with app.sql_session() as session:
result = session.execute(
select(User.id).where(User.email == email or User.username == username)
).first()
if result is None:
user = User()
user.email = email
user.username = username
user.password = password
session.add(user)
session.commit()
user_id = user.id
print(f"Created user #{user_id}: {username} ({email})")
else:
user_id = None
print(f"Signup error: '{email}' or '{username}' are already used")
if user_id is None:
return {"error": "existing credentials"}
else:
# token = auth_manager.dump_token(user_id)
return {"userId": user_id}
@server.websocket('/ws')
async def ws():
print("incoming ws")
# Authenticate before dedicating any resources
auth_req = json.loads(await websocket.receive())
if "username" not in auth_req:
print("Invalid auth request:", auth_req)
await websocket.send(json.dumps({'error': 'invalid auth'}))
return
# TODO: Actually authenticate credentials against stored users
# TODO: Get the user id from the credentials, use them in create-agent
print(f"User {auth_req['username']} connected")
send_queue = asyncio.Queue()
async def sender_task():
nonlocal send_queue
while True:
msg = await send_queue.get()
await websocket.send(json.dumps(msg))
async def send(msg):
await send_queue.put(msg)
asyncio.create_task(sender_task())
while True:
req = json.loads(await websocket.receive())
action = req['action']
body = req.get('body', {})
nonce = req.get('nonce', None)
if action == "ping":
print("ping")
await send({'content': "pong"})
elif action == "create-agent":
with app.sql_session() as session:
agent = Agent()
agent.user_id = None
agent.name = body['name']
agent.description = body['description']
agent.picture = body.get('picture')
agent.kind = body['kind']
agent.data = body['data']
session.add(agent)
session.commit()
print(f"Created user #{agent.id}: {agent.name}")
await send({'content': "agent created", 'agentId': agent.id, 'nonce': nonce})
else:
print("Unknown message: " + action)
server.run(debug=True)
if __name__ == "__main__":
app.run(debug=True)
if len(sys.argv) > 1:
data_directory = sys.argv[1]
else:
print("No directory specified. Using current directory as default.")
data_directory = "."
app = App(data_directory)
start_server(app)
+90
View File
@@ -0,0 +1,90 @@
from typing import Optional
from sqlalchemy import create_engine, Column, Integer, JSON, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.sql.expression import select
class Base(DeclarativeBase):
pass
def create_sql_engine (data_directory):
sql_engine = create_engine(f"sqlite:///{data_directory}/horde.db")
Base.metadata.create_all(sql_engine)
return sql_engine
class User(Base):
__modelname__ = "user"
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str]
username: Mapped[str]
# Stores the plain password. This must be changed eventually to store a salted hash instead.
password: Mapped[str]
class Agent(Base):
__modelname__ = "agent"
__tablename__ = 'agents'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
handle: Mapped[Optional[str]]
visibility: Mapped[int] = mapped_column(default=0)
# Comma separated list of permission symbols
permissions: Mapped[str] = mapped_column(default="")
name: Mapped[str]
description: Mapped[Optional[str]]
picture: Mapped[Optional[str]]
ordering: Mapped[Optional[int]]
# Agents can be any of a set of agent classes
kind: Mapped[str]
# Stores the agent data specific to its class
data = mapped_column(JSON, default={})
class Chat(Base):
__modelname__ = 'chat'
__tablename__ = 'chats'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
name: Mapped[str]
description: Mapped[Optional[str]]
picture: Mapped[Optional[str]]
ordering: Mapped[Optional[int]]
# For indexing purposes, some information is taken from the last chat message.
# Preview is the (possibly cropped) content,
# time is the timestamp of the last event.
last_message_preview: Mapped[Optional[str]]
last_message_time: Mapped[Optional[int]]
# Stores membership of agents in chats. If a record exists linking an agent and a chat,
# it means the agent participates in that chat.
class Membership(Base):
__modelname__ = 'membership'
__tablename__ = 'memberships'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
agent_id: Mapped[int] = mapped_column(ForeignKey("agents.id"))
chat_id: Mapped[int] = mapped_column(ForeignKey("chats.id"))
class Message(Base):
__modelname__ = 'message'
__tablename__ = 'messages'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
chat_id: Mapped[int] = mapped_column(ForeignKey("chats.id"))
# Timestamp of this message
time: Mapped[int]
# Identifier of the sender: user / system / agent-$ID
sender: Mapped[str]
# Predefined set of message types, interpreted as 'text' when empty.
kind: Mapped[str]
content: Mapped[str]
View File
Whitespace-only changes.
+1
View File
@@ -1 +1,2 @@
Quart==0.20.0
sqlalchemy
+399
View File
@@ -0,0 +1,399 @@
diff --git a/.gitignore b/.gitignore
index 1d17dae..32bceab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.venv
+horde.db
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 02a0261..1f8f801 100644
--- a/Makefile
+++ b/Makefile
@@ -14,3 +14,5 @@ web: frontend/build/web/index.html
frontend/build/web/index.html:
cd frontend && flutter build web
+
+frontend/build/web/index.html: $(shell find frontend/lib -name '*.dart')
\ No newline at end of file
diff --git a/backend/app.py b/backend/app.py
index 22cfb32..939dc09 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -1,14 +1,134 @@
-from quart import Quart, send_from_directory
+import sys
+import json
+import asyncio
-app = Quart(__name__, static_folder='../frontend/build/web')
+from database import create_sql_engine, User, Agent
+from sqlalchemy.orm import Session
+from sqlalchemy.sql.expression import select
+from quart import Quart, Response, request, send_from_directory, jsonify, websocket
-@app.route('/')
-async def hello():
- return await send_from_directory(app.static_folder, 'index.html')
+class App:
+ def __init__(self, data_directory):
+ self.data_directory = data_directory
+ self.sql_engine = create_sql_engine(self.data_directory)
-@app.route('/<path:path>')
-async def serve_static(path):
- return await send_from_directory(app.static_folder, path)
+ def sql_session(self):
+ return Session(self.sql_engine)
+
+app = App("./")
+
+def start_server(app):
+ server = Quart(__name__, static_folder='../frontend/build/web')
+
+ @server.route('/')
+ async def index():
+ return await send_from_directory(server.static_folder, 'index.html')
+
+ @server.route('/<path:path>')
+ async def serve_static(path):
+ return await send_from_directory(server.static_folder, path)
+
+ @server.route('/user/signup', methods=['POST'])
+ async def signup():
+ data = await request.get_json()
+
+ user_id = None
+
+ username = data['username']
+ email = data['email']
+ password = data['password']
+
+ with app.sql_session() as session:
+ result = session.execute(
+ select(User.id).where(User.email == email or User.username == username)
+ ).first()
+
+ if result is None:
+ user = User()
+ user.email = email
+ user.username = username
+ user.password = password
+
+ session.add(user)
+ session.commit()
+
+ user_id = user.id
+ print(f"Created user #{user_id}: {username} ({email})")
+ else:
+ user_id = None
+ print(f"Signup error: '{email}' or '{username}' are already used")
+
+ if user_id is None:
+ return {"error": "existing credentials"}
+ else:
+ # token = auth_manager.dump_token(user_id)
+ return {"userId": user_id}
+
+ @server.websocket('/ws')
+ async def ws():
+ print("incoming ws")
+
+ # Authenticate before dedicating any resources
+ auth_req = json.loads(await websocket.receive())
+ if "username" not in auth_req:
+ print("Invalid auth request:", auth_req)
+ await websocket.send(json.dumps({'error': 'invalid auth'}))
+ return
+
+ # TODO: Actually authenticate credentials against stored users
+ # TODO: Get the user id from the credentials, use them in create-agent
+ print(f"User {auth_req['username']} connected")
+
+ send_queue = asyncio.Queue()
+
+ async def sender_task():
+ nonlocal send_queue
+ while True:
+ msg = await send_queue.get()
+ await websocket.send(json.dumps(msg))
+
+ async def send(msg):
+ await send_queue.put(msg)
+
+ asyncio.create_task(sender_task())
+
+ while True:
+ req = json.loads(await websocket.receive())
+
+ action = req['action']
+ body = req.get('body', {})
+ nonce = req.get('nonce', None)
+
+ if action == "ping":
+ print("ping")
+ await send({'content': "pong"})
+ elif action == "create-agent":
+ with app.sql_session() as session:
+ agent = Agent()
+
+ agent.user_id = None
+ agent.name = body['name']
+ agent.description = body['description']
+ agent.picture = body.get('picture')
+ agent.kind = body['kind']
+ agent.data = body['data']
+
+ session.add(agent)
+ session.commit()
+
+ print(f"Created user #{agent.id}: {agent.name}")
+ await send({'content': "agent created", 'agentId': agent.id, 'nonce': nonce})
+ else:
+ print("Unknown message: " + action)
+
+ server.run(debug=True)
if __name__ == "__main__":
- app.run(debug=True)
+ if len(sys.argv) > 1:
+ data_directory = sys.argv[1]
+ else:
+ print("No directory specified. Using current directory as default.")
+ data_directory = "."
+
+ app = App(data_directory)
+ start_server(app)
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
index f8d6ab2..00a2728 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1 +1,2 @@
Quart==0.20.0
+sqlalchemy
\ No newline at end of file
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 7b7f5b6..325549e 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -1,122 +1,43 @@
+import 'dart:html' as html;
+import 'dart:io' as io;
import 'package:flutter/material.dart';
+import 'chat_screen.dart';
+import 'backend.dart';
+
+class Config {
+ static String? backendUrl; // Declare it as nullable
+
+ static void loadConfig() {
+ // Check the platform for web or other
+ if (html.window.location.origin.isNotEmpty) {
+ // For web applications
+ backendUrl = html.window.location.origin; // Get the server URL from window.location
+ } else if (io.Platform.environment.containsKey('BACKEND_URL')) {
+ // For Android or other platforms, check for the environment variable
+ backendUrl = io.Platform.environment['BACKEND_URL'];
+ } else {
+ // If no value is determined, set it to null
+ backendUrl = null;
+ }
+ }
+}
void main() {
+ Config.loadConfig(); // Load the configuration before running the app
+
+ Backend.setup('${Config.backendUrl}/ws');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
- // This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- // This is the theme of your application.
- //
- // TRY THIS: Try running your application with "flutter run". You'll see
- // the application has a purple toolbar. Then, without quitting the app,
- // try changing the seedColor in the colorScheme below to Colors.green
- // and then invoke "hot reload" (save your changes or press the "hot
- // reload" button in a Flutter-supported IDE, or press "r" if you used
- // the command line to start the app).
- //
- // Notice that the counter didn't reset back to zero; the application
- // state is not lost during the reload. To reset the state, use hot
- // restart instead.
- //
- // This works for code too, not just values: Most code changes can be
- // tested with just a hot reload.
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
- ),
- home: const MyHomePage(title: 'Flutter Demo Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({super.key, required this.title});
-
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
- final String title;
-
- @override
- State<MyHomePage> createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
- int _counter = 0;
-
- void _incrementCounter() {
- setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
- _counter++;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
- return Scaffold(
- appBar: AppBar(
- // TRY THIS: Try changing the color here to a specific color (to
- // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
- // change color while the other colors stay the same.
- backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
- title: Text(widget.title),
- ),
- body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
- child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- //
- // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
- // action in the IDE, or press "p" in the console), to see the
- // wireframe for each widget.
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- const Text('You have pushed the button this many times:'),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
- ), // This trailing comma makes auto-formatting nicer for build methods.
+ title: 'Flutter Chat',
+ theme: ThemeData.dark(),
+ home: const ChatScreen(),
);
}
-}
+}
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 6d03df5..daaf485 100644
--- a/readme.md
+++ b/readme.md
@@ -2,9 +2,9 @@
To run the Flask server, follow these steps:
-1. Build and activate the virtual environment and install the required dependencies:
+1. Build and activate the virtual environment, install the required dependencies, and build the gui by running make with the default target:
```bash
- make install
+ make
```
2. Run the server using the provided script:
```bash
@@ -17,3 +17,6 @@ Now, visit `http://127.0.0.1:5000` in your web browser to see the application in
Frontend uses flutter. For Ubuntu you can use [this guideline](https://ubuntu.com/blog/getting-started-with-flutter-on-ubuntu) to install it.
+## Configuration
+
+You can set the backend URL for Android and native builds using the environment variable `BACKEND_URL`. Make sure to set this variable before running the application. Defaults to `null` if nothing is set.
diff --git a/run.sh b/run.sh
index 638f34a..f6e6484 100644
--- a/run.sh
+++ b/run.sh
@@ -1,4 +1,35 @@
#!/bin/bash
-cd backend
-source ../.venv/bin/activate
-python app.py
+
+case "$1" in
+ "")
+ cd backend
+ source ../.venv/bin/activate
+ python app.py
+ ;;
+ "test")
+ # Setup test data directory
+ mkdir -p test-data
+ rm -f test-data/horde.db
+
+ # Start backend in the background
+ cd backend
+ source ../.venv/bin/activate
+ python app.py ../test-data &
+ sleep 2
+
+ # Run tests
+ cd ../testing
+ #bash signup.sh
+ #node login.js
+ node create-agent.js
+
+ # Finalize the backend server
+ kill $!
+ ;;
+ "watch")
+ echo "TODO: Implement watch functionality"
+ ;;
+ *)
+ echo "Error: unknown command '$1'"
+ ;;
+esac
+9
View File
@@ -0,0 +1,9 @@
import 'chat.dart';
class Agent {
void receivedChatMessage(Chat chat, String message) {
// Simple response from the agent
String responseMessage = "Agent received: " + message;
chat.addAgentMessage(responseMessage);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'dart:html';
import 'dart:convert';
class Backend {
static WebSocket? _socket;
static String? _url;
static void setup(String url) {
_url = url;
connect();
}
static void auth(username) {
_socket!.send(
json.encode({'username': username})
);
}
static void connect() {
_socket = WebSocket(_url!);
_socket!.onOpen.listen((event) {
print('Connected to the WebSocket server');
auth("the-user");
ping();
});
_socket!.onError.listen((event) {
print('Error: $event');
_reconnect();
});
_socket!.onClose.listen((event) {
print('Disconnected. Attempting to reconnect...');
_reconnect();
});
}
static void _reconnect() {
Future.delayed(Duration(seconds: 5), () {
connect();
});
}
static void ping() {
if (_socket != null && _socket!.readyState == WebSocket.OPEN) {
final data = json.encode({'path': 'ping'});
_socket!.send(data);
print('Ping sent: $data');
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import 'agent.dart'; // Import the Agent class
class Message {
final String content;
final String sender;
Message(this.content, this.sender);
}
class Chat {
final String id;
final List<Message> _messages = [];
final Agent agent = Agent(); // Create an instance of Agent
Chat(this.id);
List<Message> get messages => List.unmodifiable(_messages);
void addUserMessage(String message) {
_messages.add(Message(message, 'user'));
agent.receivedChatMessage(this, message); // Call agent's method
}
void addAgentMessage(String message) {
_messages.add(Message(message, 'agent'));
}
void clearMessages() {
_messages.clear();
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'chat.dart'; // Import the new Chat class
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final Chat _chat = Chat('chat1'); // Create an instance of Chat
final TextEditingController _controller = TextEditingController();
void _sendMessage() {
if (_controller.text.isNotEmpty) {
setState(() {
_chat.addUserMessage(_controller.text); // Use the Chat instance to add the message
_controller.clear();
});
}
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isBigScreen = width > 600; // Define threshold for small screens
return Scaffold(
appBar: AppBar(
title: const Text('Chat'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _chat.messages.length,
itemBuilder: (context, index) {
final message = _chat.messages[index];
// Align all messages to left for big screens
final align = message.sender == 'user' && !isBigScreen ? CrossAxisAlignment.end : CrossAxisAlignment.start;
return Row(
mainAxisAlignment: align == CrossAxisAlignment.end ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
'${message.content}',
style: const TextStyle(color: Colors.white),
),
),
],
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Send a message'),
onSubmitted: (_) => _sendMessage(),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
),
],
),
),
],
),
);
}
}
+28 -107
View File
@@ -1,122 +1,43 @@
import 'dart:html' as html;
import 'dart:io' as io;
import 'package:flutter/material.dart';
import 'chat_screen.dart';
import 'backend.dart';
class Config {
static String? backendUrl; // Declare it as nullable
static void loadConfig() {
// Check the platform for web or other
if (html.window.location.origin.isNotEmpty) {
// For web applications
backendUrl = html.window.location.origin; // Get the server URL from window.location
} else if (io.Platform.environment.containsKey('BACKEND_URL')) {
// For Android or other platforms, check for the environment variable
backendUrl = io.Platform.environment['BACKEND_URL'];
} else {
// If no value is determined, set it to null
backendUrl = null;
}
}
}
void main() {
Config.loadConfig(); // Load the configuration before running the app
Backend.setup('${Config.backendUrl}/ws');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
title: 'Flutter Chat',
theme: ThemeData.dark(),
home: const ChatScreen(),
);
}
}
+5 -2
View File
@@ -2,9 +2,9 @@
To run the Flask server, follow these steps:
1. Build and activate the virtual environment and install the required dependencies:
1. Build and activate the virtual environment, install the required dependencies, and build the gui by running make with the default target:
```bash
make install
make
```
2. Run the server using the provided script:
```bash
@@ -17,3 +17,6 @@ Now, visit `http://127.0.0.1:5000` in your web browser to see the application in
Frontend uses flutter. For Ubuntu you can use [this guideline](https://ubuntu.com/blog/getting-started-with-flutter-on-ubuntu) to install it.
## Configuration
You can set the backend URL for Android and native builds using the environment variable `BACKEND_URL`. Make sure to set this variable before running the application. Defaults to `null` if nothing is set.
+34 -3
View File
@@ -1,4 +1,35 @@
#!/bin/bash
cd backend
source ../.venv/bin/activate
python app.py
case "$1" in
"")
cd backend
source ../.venv/bin/activate
python app.py
;;
"test")
# Setup test data directory
mkdir -p test-data
rm -f test-data/horde.db
# Start backend in the background
cd backend
source ../.venv/bin/activate
python app.py ../test-data &
sleep 2
# Run tests
cd ../testing
#bash signup.sh
#node login.js
node create-agent.js
# Finalize the backend server
kill $!
;;
"watch")
echo "TODO: Implement watch functionality"
;;
*)
echo "Error: unknown command '$1'"
;;
esac
+28
View File
@@ -0,0 +1,28 @@
# Scratchpad
This is just loose ideas that need to be consolidated and implemented.
---
There are essentially 2 main classes in the project: Agent, and Chat.
A chat is a fully featured telegram-like chat, with controls for adding messages, searching and retrieving messages, editing existing messages, and upload various data types (audio, image, etc). Tag support is very important too, beyond communication controls. In the future there will also be real time communication functionality, ie. calls, possibly also visibility of draft messages.
An agent is a controller for chats that it is part of. It can be implemented by multiple means, any class that supports the agent interface is an agent. They have the exact same controls over the chat that the user does, any aditional functionality depends on the agent itself.
Unlike chatgpt and the initial horde, chats are not meant to be created for any new conversation. Instead, agents have a singular permanent chat with the user. Chats are only created when a topic needs so much discussion that its better to separate it, or for other purposes based on necessity or organization and with limited frequency, similar to slack/discord groups.
In the same way as with chats, both the user and agents can create and update other agents. They have to be explicitly enabled to do so. Most agents are better not given this capability, but the introduction agent is encouraged to have it enabled.
**ALPHA MVP Urgently Needed.**
Such an MVP is a chat with an Agent that responds to messages one by one. It has a simplistic, brief and conversational prompt. System prompt has brief description of the chat system.
Message history is stored, and its infinite. Agent sees the last N messages and a recursive summary of the previous conversation.
Agent is powered by replicate, whose API key is configured via env variables.
MVP-2 lets the agent (and the user) search in the message history, and the recursive summary will have an enforced size limit.
MVP-3A has configurable agents, both user and agents can create new agents, also modify their own prompt. Probably allowed by default.
MVP-3C has multiple chats, users and agents can create chats, modify chat titles, and alter chat participants.
MVP-3P is permissions. Modifying chats and agents both need a permission system.
**Current Status**: Frontend and backend connect via websocket. Backend does nothing else. Frontend has basic chat classes and gui, and a basic agent class that repeats the user message.
# Next Steps
These must be determined by the current system and the next target, currently MVP.
+29
View File
@@ -0,0 +1,29 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/ws');
ws.on('open', function open() {
const packet = JSON.stringify({ username: "test-user", password: "123456" });
ws.send(packet);
runTest();
});
ws.on('message', function incoming(data) {
const jsonResponse = JSON.parse(data);
console.log(jsonResponse);
});
function runTest() {
const message = {
action: 'create-agent',
body: {
name: 'First Agent',
description: 'First agent does nothing',
kind: 'simple',
data: {}
},
};
ws.send(JSON.stringify(message)); // Send the create-agent message
}
+13
View File
@@ -0,0 +1,13 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/ws');
ws.on('open', function open() {
const packet = JSON.stringify({ username: "test-user", password: "123456" });
ws.send(packet);
});
ws.on('message', function incoming(data) {
const jsonResponse = JSON.parse(data);
console.log(jsonResponse);
});
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
URL="http://localhost:5000/user/signup"
DATA='{"username":"test-user","email":"user@test.com","password":"123456"}'
RESPONSE=$(curl -s -X POST $URL -H "Content-Type: application/json" -d "$DATA")
echo $RESPONSE