You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. from flask import ( Flask, render_template, request, abort, redirect, Response,
  2. send_from_directory, url_for, session, flash, get_flashed_messages)
  3. from flask_session import Session
  4. import openai
  5. from dotenv import load_dotenv
  6. from markdown import markdown
  7. import requests
  8. from glob import glob
  9. import json
  10. import os
  11. import sys
  12. import time
  13. import shutil
  14. import re
  15. import secrets
  16. RE_VID = re.compile("""\[video ["']([^['"]*?)["']\]""")
  17. TEMPLATE_VID = """<video controls class="centered"><source src="{}" type="video/{}"></video>"""
  18. def preprocess_content(defs):
  19. md = defs.get("markdown", "")
  20. if md:
  21. content = markdown(md)
  22. content = RE_VID.sub(lambda m: TEMPLATE_VID.format(m.group(1), m.group(1).split('.')[-1]), content)
  23. else:
  24. content = ""
  25. img = defs.get("image", {})
  26. match img.get("placement", "none"):
  27. case "above":
  28. content = """<img src="{source}" alt="{alt}" class="centered w-{width}"/><br/>\n""".format(**img)+content
  29. case "left":
  30. content = """<img src="{source}" alt="{alt}" class="float-left w-{width}"/><br/>\n""".format(**img)+content
  31. case "right":
  32. content = """<img src="{source}" alt="{alt}" class="float-right w-{width}"/><br/>\n""".format(**img)+content
  33. case "below":
  34. content = content+"""<br/>\n<img src="{source}" alt="{alt}" class="centered w-{width}"/><br/>\n""".format(**img)
  35. if defs.get("use_soundtrack") and not defs.get("use_video_content"): ## No soundtrack if we're playing video
  36. content = """<audio loop data-autoplay><source src="{}" type="audio/{}"></audio>\n""".format(
  37. defs["soundtrack"], defs["soundtrack"].split(".")[-1])+content
  38. return content
  39. def preprocess_payload(payload):
  40. for column in payload.get("columns", []):
  41. column["content"] = preprocess_content(column)
  42. for slide in column.get("slides", []):
  43. slide["content"] = preprocess_content(slide)
  44. RE_YOUTUBE = re.compile("https://(?:www.)?youtube.com/watch\?v=(?P<code>[^/?]*)")
  45. RE_YOUTU_BE = re.compile("https://(?:www.)?youtu.be/(?P<code>[^/?]*)")
  46. RE_VIMEO = re.compile("https://(?:www.)?vimeo.com/(?P<code>[^/?]*)")
  47. def preprocess_embed(e):
  48. res = {}
  49. url = e.get("url")
  50. if not url:
  51. return None
  52. for key in ["id", "description", "proportions", "url_extras"]:
  53. val = e.get(key)
  54. if not val:
  55. if key!="url_extras":
  56. return None
  57. else:
  58. res[key] = val
  59. m = RE_YOUTUBE.search(url) or RE_YOUTU_BE.search(url)
  60. if m:
  61. res["type"] = "youtube"
  62. res["code"] = m.group("code")
  63. else:
  64. m = RE_VIMEO.search(url)
  65. if m:
  66. res["type"] = "vimeo"
  67. res["code"] = m.group("code")
  68. else:
  69. return None
  70. return res
  71. def get_all_embeds():
  72. embeds = json.load(open("static/media/video-embed.json"))
  73. embeds = [preprocess_embed(e) for e in embeds]
  74. embeds = [e for e in embeds if e]
  75. return embeds
  76. def deref_schema(val):
  77. "Deref all `$ref` entries in a json-schema"
  78. if type(val)==type([]):
  79. return [deref_schema(item) for item in val]
  80. elif type(val)==type({}):
  81. val = dict(val)
  82. if "$ref" in val:
  83. ref = val["$ref"]
  84. if ref.startswith("#"):
  85. pass # stub. Currently we don't have internal references...
  86. elif ref.startswith("/"):
  87. ref = url_for("home", _external=True)+ref[1:]
  88. val.update(requests.get(ref).json())
  89. del val["$ref"]
  90. for key in val:
  91. val[key] = deref_schema(val[key])
  92. return val
  93. else:
  94. return val
  95. def file2json(path):
  96. "returns json string without line breaks and indent"
  97. return json.dumps(json.load(open(path)))
  98. def make_prompt(username):
  99. user = open("static/users/{}.txt".format(username)).read()
  100. json_schema = json.dumps(schema())
  101. json_example = file2json("static/chat-example.json")
  102. img_enum = json.dumps(choices("img"))
  103. return render_template("prompt.txt", user=user, schema=json_schema, example=json_example, img_enum=img_enum)
  104. load_dotenv()
  105. openai.organization = "org-GFWgNyt7NSKpCv6GhzXYZTpi"
  106. application = Flask(__name__)
  107. application.config["SECRET_KEY"] = secrets.token_hex()
  108. application.config["SESSION_TYPE"] = 'filesystem'
  109. Session(application)
  110. @application.route("/", methods=['GET', 'POST'])
  111. def home():
  112. is_initial = False
  113. if "messages" not in session:
  114. if "user" not in session:
  115. session["user"] = "odelia" # terrible kludge
  116. session["messages"] = [
  117. {"role": "system", "content": make_prompt(session["user"])} #,
  118. ### This turned to be counter productive
  119. # {"role": "assistant", "content": file2json("static/initial-chat.json")}
  120. ]
  121. session["history"] = []
  122. is_initial = True
  123. prompt = ""
  124. if request.method=='POST' or is_initial:
  125. if is_initial:
  126. pass
  127. ## this turned out to be counter-productive
  128. # session["messages"] += [{"role": "system", "content": "User wishes to begin. Remember to find out how they feel before offering a ceremony or other action."}]
  129. else:
  130. prompt = request.form["prompt"].strip()
  131. if prompt:
  132. session["messages"] += [{"role": "user", "content": prompt}]
  133. else:
  134. session["messages"] += [{"role": "system", "content": "User has hit enter. Please continue."}]
  135. payload = None
  136. while not payload:
  137. reply = openai.ChatCompletion.create(model=os.environ["MODEL_NAME"], messages=session["messages"])
  138. message = dict(reply["choices"][0]["message"])
  139. session["messages"] += [message]
  140. try:
  141. payload = json.loads(message["content"])
  142. payload["prompt"] = prompt.replace('"', '\"')
  143. payload["content"] = preprocess_content(payload)
  144. if payload.get("use_video_content"):
  145. embed_id = payload.get("video_content")
  146. if embed_id:
  147. embeds = get_all_embeds()
  148. embeds = [e for e in embeds if e["id"]==embed_id]
  149. if embeds:
  150. payload["embed"] = embeds[0]
  151. session["history"] += [dict(payload)] # shallow copy so history doesn't get added later on
  152. except Exception as e:
  153. print(repr(e))
  154. session["messages"] += [{"role": "system", "content": "reply was ignored because it's not valid JSON. User is unaware. Assistant should NOT apologize to them, and simply rephrase answer as valid JSON that complies with the schema in the initial system prompt of this chat. It is followed by an example the assistant can learn from."}]
  155. print("=====")
  156. print(reply["choices"][0]["message"]["content"])
  157. print("-----")
  158. print(payload)
  159. print("=====")
  160. else:
  161. payload = json.loads(session["messages"][-1]["content"])
  162. payload["content"] = preprocess_content(payload)
  163. try:
  164. if session["messages"][-2]["role"] == "user":
  165. payload["prompt"] = session["messages"][-2]["content"]
  166. except exception as e:
  167. print(e)
  168. payload["history"] = list(reversed(session.get("history", [])[:-1]))
  169. payload["user"] = session["user"]
  170. return render_template("chat.html", **payload)
  171. @application.get("/reset/<string:user>")
  172. def reset(user):
  173. if not os.path.isfile("static/users/{}.txt".format(user)):
  174. flash("user {} not found".format(user), 'error')
  175. return redirect(url_for('home'))
  176. for key in ["messages", "history"]:
  177. if key in session:
  178. del session[key]
  179. session["user"] = user
  180. return redirect(url_for('home'))
  181. @application.get("/schema.json")
  182. def schema():
  183. "return deref_schema of `chat.schema.json` file's content"
  184. return deref_schema(json.load(open("static/chat.schema.json")))
  185. @application.get("/slides")
  186. def slides():
  187. "Legacy multi-slide format"
  188. payload = json.load(open("static/slides.json"))
  189. preprocess_payload(payload)
  190. return render_template("slides.html", generate_indices=False, **payload)
  191. @application.get("/prompt/<string:user>.txt")
  192. def get_prompt(user):
  193. return Response(make_prompt(user), mimetype="text/plain")
  194. @application.get("/example")
  195. def example():
  196. payload = json.load(open("static/chat-example.json"))
  197. payload["content"] = preprocess_content(payload)
  198. if payload.get("use_video_content"):
  199. embed_id = payload.get("video_content")
  200. if embed_id:
  201. embeds = get_all_embeds()
  202. embeds = [e for e in embeds if e["id"]==embed_id]
  203. if embeds:
  204. payload["embed"] = embeds[0]
  205. return render_template("example.html", **payload)
  206. @application.get("/embeds")
  207. def embeds():
  208. embeds = get_all_embeds()
  209. return render_template("embeds.html", embeds=reversed(embeds))
  210. @application.post("/update")
  211. def update():
  212. "Legacy multi-slide format"
  213. shutil.copy(
  214. "static/slides.json",
  215. time.strftime(
  216. "editor-archive/slides-%Y-%m-%d-%H.%M.%S.json",
  217. time.localtime()))
  218. payload = request.get_json()
  219. print(type(payload))
  220. json.dump(payload, sys.stdout, indent=4)
  221. json.dump(payload, open("static/slides.json", "w"), indent=4)
  222. return {"status": "success"}
  223. @application.post("/update-embeds")
  224. def update_embeds():
  225. shutil.copy(
  226. "static/media/video-embed.json",
  227. time.strftime(
  228. "editor-archive/embeds-%Y-%m-%d-%H.%M.%S.json",
  229. time.localtime()))
  230. payload = request.get_json()
  231. print(type(payload))
  232. json.dump(payload, sys.stdout, indent=4)
  233. json.dump(payload, open("static/media/video-embed.json", "w"), indent=4)
  234. return {"status": "success"}
  235. @application.post("/update-chat")
  236. def update_chat():
  237. shutil.copy(
  238. "static/chat-example.json",
  239. time.strftime(
  240. "editor-archive/chat-%Y-%m-%d-%H.%M.%S.json",
  241. time.localtime()))
  242. payload = request.get_json()
  243. print(type(payload))
  244. json.dump(payload, sys.stdout, indent=4)
  245. json.dump(payload, open("static/chat-example.json", "w"), indent=4)
  246. return {"status": "success"}
  247. @application.get("/enum/<topic>")
  248. def choices(topic):
  249. if topic in ["img", "bg", "bg-video", "audio"]:
  250. choices = sorted(glob("static/media/{}/*.*".format(topic)))
  251. titles = [os.path.basename(c) for c in choices]
  252. try:
  253. descdir = json.load(open("static/media/{}.json".format(topic)))
  254. except FileNotFoundError:
  255. descdir = {}
  256. return {
  257. "type": "string",
  258. "enum": choices,
  259. "options": {
  260. "enum_titles": [descdir.get(t, t.rsplit(".", 1)[0]) for t in titles]
  261. }
  262. }
  263. abort(404)
  264. @application.get("/embeds_enum")
  265. def embeds_enum():
  266. embeds = get_all_embeds()
  267. return {
  268. "enum": [e["id"] for e in embeds],
  269. "options": { "enum_titles": [e["description"] for e in embeds] }
  270. }
  271. @application.route("/save", methods=['GET', 'POST'])
  272. def save():
  273. if request.method=="POST":
  274. filename = request.form["filename"].rsplit("/",1)[-1]
  275. if filename:
  276. path = "archive/{}.json".format(filename)
  277. is_overwrite = os.path.isfile(path)
  278. moment = {
  279. key: session.get(key, [])
  280. for key in ["messages", "history"]
  281. }
  282. moment["user"] = session.get("user", "odelia")
  283. print(moment)
  284. json.dump(moment, open(path,"w"), indent=4)
  285. if is_overwrite:
  286. flash("Successfully overwritten {}.json".format(filename))
  287. else:
  288. flash("Successfully saved to {}.json".format(filename))
  289. else:
  290. flash("Invalid filename. Save aborted.", "error")
  291. return redirect(url_for("home"))
  292. else:
  293. return render_template("save.html",
  294. suggestion=time.strftime(
  295. "moment-%Y-%m-%d-%H.%M.%S"),
  296. files = [os.path.basename(path).rsplit(".",1)[0] for path in sorted(glob("archive/*.json"))])
  297. @application.route("/load", methods=['GET', 'POST'])
  298. def load():
  299. if request.method=="POST":
  300. filename = request.form["filename"]
  301. try:
  302. moment = json.load(open("archive/{}.json".format(filename)))
  303. session["messages"] = moment["messages"]
  304. session["history"] = moment["history"]
  305. session["user"] = moment.get("user", "odelia")
  306. except Exception as e:
  307. flash(repr(e))
  308. return redirect(url_for("home")+"#/oldest")
  309. else:
  310. return render_template("load.html",
  311. files = [os.path.basename(path).rsplit(".",1)[0] for path in sorted(glob("archive/*.json"))])
  312. @application.get("/embed-editor")
  313. def embed_editor():
  314. return render_template("embed-editor.html")
  315. @application.get("/chat-editor")
  316. def chat_editor():
  317. return render_template("chat-editor.html")
  318. @application.get("/editor")
  319. def editor():
  320. "Legacy multi-slide format"
  321. return render_template("editor.html")
  322. @application.route("/img", methods=['GET', 'POST'])
  323. def image():
  324. if request.method=='GET':
  325. src = "static/img/marble-question-mark.png"
  326. alt = "A question mark"
  327. prompt = ""
  328. else:
  329. prompt = request.form["prompt"]
  330. alt = prompt
  331. response = openai.Image.create(prompt=prompt, model="dall-e-3", n=1, size="1024x1024")
  332. src = response['data'][0]['url']
  333. return render_template("image.html", src=src, alt=alt, prompt=prompt)
  334. @application.route("/favicon.ico")
  335. def favicon():
  336. return send_from_directory(os.path.join(application.root_path, "static"),
  337. "favicon.ico", mimetype="image/vnd.microsoft.icon")