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
Binary file not shown.
+129
-9
@@ -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)
|
||||
@@ -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]
|
||||
|
||||
Whitespace-only changes.
@@ -1 +1,2 @@
|
||||
Quart==0.20.0
|
||||
sqlalchemy
|
||||
Reference in new issue
Block a user