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:
1 parent
1e82796db1
commit
44f0b3589c
19 files changed
+936
-122
No files matched your search
+399
@@ -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
|
||||
Reference in new issue
Block a user