jtdearmon commited on
Commit
5952084
·
verified ·
1 Parent(s): 2dc2cad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +165 -144
app.py CHANGED
@@ -15,6 +15,7 @@ import json
15
  import time
16
  import random
17
  import sqlite3
 
18
  from dataclasses import dataclass, asdict
19
  from datetime import datetime, timezone
20
  from typing import List, Dict, Any, Tuple, Optional
@@ -45,7 +46,7 @@ except Exception:
45
  DB_DIR = "/data" if os.path.exists("/data") else "."
46
  DB_PATH = os.path.join(DB_DIR, "sql_trainer_dynamic.db")
47
  EXPORT_DIR = "."
48
- ADMIN_KEY = os.getenv("OPENAI_API_KEY", "demo")
49
  RANDOM_SEED = int(os.getenv("RANDOM_SEED", "7"))
50
  random.seed(RANDOM_SEED)
51
  SYS_RAND = random.SystemRandom()
@@ -65,10 +66,11 @@ def _to_pil(fig) -> Image.Image:
65
 
66
  def draw_dynamic_erd(schema: Dict[str, Any]) -> Image.Image:
67
  """
 
68
  schema = {
69
  "domain": "bookstore",
70
  "tables": [
71
- {"name":"authors","columns":[{"name":"author_id","type":"INTEGER",...}, ...],
72
  "pk":["author_id"], "fks":[{"columns":["author_id"],"ref_table":"...","ref_columns":["..."]}],
73
  "rows":[{...}, {...}]}
74
  ]
@@ -111,46 +113,56 @@ def draw_dynamic_erd(schema: Dict[str, Any]) -> Image.Image:
111
  ax.text(0.5, 0.06, f"Domain: {schema.get('domain','unknown')}", fontsize=9, ha="center")
112
  return _to_pil(fig)
113
 
114
- # -------------------- SQLite helpers --------------------
 
 
115
  def connect_db():
116
- con = sqlite3.connect(DB_PATH)
 
 
 
 
 
 
 
117
  con.execute("PRAGMA foreign_keys = ON;")
118
  return con
119
 
120
  CONN = connect_db()
121
 
122
  def init_progress_tables(con: sqlite3.Connection):
123
- cur = con.cursor()
124
- cur.execute("""
125
- CREATE TABLE IF NOT EXISTS users (
126
- user_id TEXT PRIMARY KEY,
127
- name TEXT,
128
- created_at TEXT
129
- )
130
- """)
131
- cur.execute("""
132
- CREATE TABLE IF NOT EXISTS attempts (
133
- id INTEGER PRIMARY KEY AUTOINCREMENT,
134
- user_id TEXT,
135
- question_id TEXT,
136
- category TEXT,
137
- correct INTEGER,
138
- sql_text TEXT,
139
- timestamp TEXT,
140
- time_taken REAL,
141
- difficulty INTEGER,
142
- source TEXT,
143
- notes TEXT
144
- )
145
- """)
146
- cur.execute("""
147
- CREATE TABLE IF NOT EXISTS session_meta (
148
- id INTEGER PRIMARY KEY CHECK (id=1),
149
- domain TEXT,
150
- schema_json TEXT
151
- )
152
- """)
153
- con.commit()
 
154
 
155
  init_progress_tables(CONN)
156
 
@@ -411,58 +423,60 @@ def llm_generate_domain_and_questions() -> Optional[Dict[str,Any]]:
411
 
412
  # -------------------- Schema install & question handling --------------------
413
  def drop_existing_domain_tables(con: sqlite3.Connection, keep_internal=True):
414
- cur = con.cursor()
415
- cur.execute("SELECT name, type FROM sqlite_master WHERE type IN ('table','view')")
416
- items = cur.fetchall()
417
- for name, typ in items:
418
- if keep_internal and name in ("users","attempts","session_meta"):
419
- continue
420
- try:
421
- cur.execute(f"DROP {typ.upper()} IF EXISTS {name}")
422
- except Exception:
423
- pass
424
- con.commit()
 
425
 
426
  def install_schema(con: sqlite3.Connection, schema: Dict[str,Any]):
427
  drop_existing_domain_tables(con, keep_internal=True)
428
- cur = con.cursor()
429
- # Create tables first
430
- for t in schema.get("tables", []):
431
- cols_sql = []
432
- pk = t.get("pk", [])
433
- for c in t.get("columns", []):
434
- cname = c["name"]
435
- ctype = c.get("type","TEXT")
436
- cols_sql.append(f"{cname} {ctype}")
437
- if pk:
438
- cols_sql.append(f"PRIMARY KEY ({', '.join(pk)})")
439
- create_sql = f"CREATE TABLE {t['name']} ({', '.join(cols_sql)})"
440
- cur.execute(create_sql)
441
- # Insert rows
442
- for t in schema.get("tables", []):
443
- if not t.get("rows"):
444
- continue
445
- cols = [c["name"] for c in t.get("columns", [])]
446
- qmarks = ",".join(["?"]*len(cols))
447
- insert_sql = f"INSERT INTO {t['name']} ({', '.join(cols)}) VALUES ({qmarks})"
448
- # rows can be objects or arrays
449
- for r in t["rows"]:
450
- if isinstance(r, dict):
451
- vals = [r.get(col, None) for col in cols]
452
- elif isinstance(r, list) or isinstance(r, tuple):
453
- vals = list(r) + [None]*(len(cols)-len(r))
454
- vals = vals[:len(cols)]
455
- else:
456
  continue
457
- cur.execute(insert_sql, vals)
458
- con.commit()
459
- # Persist schema JSON
460
- cur.execute("INSERT OR REPLACE INTO session_meta(id, domain, schema_json) VALUES (1, ?, ?)",
461
- (schema.get("domain","unknown"), json.dumps(schema)))
462
- con.commit()
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  def run_df(con: sqlite3.Connection, sql: str) -> pd.DataFrame:
465
- return pd.read_sql_query(sql, con)
 
466
 
467
  def rewrite_select_into(sql: str) -> Tuple[str, Optional[str]]:
468
  s = sql.strip().strip(";")
@@ -491,19 +505,20 @@ def detect_cartesian(con: sqlite3.Connection, sql: str, df_result: pd.DataFrame)
491
  missing_on = (" join " in low) and (" on " not in low) and (" using " not in low) and (" natural " not in low)
492
  if comma_from or missing_on:
493
  try:
494
- cur = con.cursor()
495
- if comma_from:
496
- t1, t2 = comma_from.groups()
497
- else:
498
- m = re.search(r"\bfrom\b\s+([a-z_]\w*)", low)
499
- j = re.search(r"\bjoin\b\s+([a-z_]\w*)", low)
500
- if not m or not j:
501
- return "Possible cartesian product: no join condition detected."
502
- t1, t2 = m.group(1), j.group(1)
503
- cur.execute(f"SELECT COUNT(*) FROM {t1}")
504
- n1 = cur.fetchone()[0]
505
- cur.execute(f"SELECT COUNT(*) FROM {t2}")
506
- n2 = cur.fetchone()[0]
 
507
  prod = n1 * n2
508
  if len(df_result) == prod and prod > 0:
509
  return f"Result row count equals {n1}×{n2}={prod}. Likely cartesian product (missing join)."
@@ -580,14 +595,15 @@ CURRENT_SCHEMA, CURRENT_QS = install_new_domain()
580
 
581
  # -------------------- Progress + mastery --------------------
582
  def upsert_user(con: sqlite3.Connection, user_id: str, name: str):
583
- cur = con.cursor()
584
- cur.execute("SELECT user_id FROM users WHERE user_id = ?", (user_id,))
585
- if cur.fetchone() is None:
586
- cur.execute("INSERT INTO users (user_id, name, created_at) VALUES (?, ?, ?)",
587
- (user_id, name, datetime.now(timezone.utc).isoformat()))
588
- else:
589
- cur.execute("UPDATE users SET name=? WHERE user_id=?", (name, user_id))
590
- con.commit()
 
591
 
592
  CATEGORIES_ORDER = [
593
  "SELECT *", "SELECT columns", "WHERE", "Aliases",
@@ -605,7 +621,8 @@ def topic_stats(df_attempts: pd.DataFrame) -> pd.DataFrame:
605
  return pd.DataFrame(rows)
606
 
607
  def fetch_attempts(con: sqlite3.Connection, user_id: str) -> pd.DataFrame:
608
- return pd.read_sql_query("SELECT * FROM attempts WHERE user_id=? ORDER BY id DESC", con, params=(user_id,))
 
609
 
610
  def pick_next_question(user_id: str) -> Dict[str,Any]:
611
  df = fetch_attempts(CONN, user_id)
@@ -637,30 +654,31 @@ def exec_student_sql(sql_text: str) -> Tuple[Optional[pd.DataFrame], Optional[st
637
  warn = detect_cartesian(CONN, sql_rew, df)
638
  return df, None, warn, note
639
  else:
640
- cur = CONN.cursor()
641
- cur.execute(sql_rew)
642
- CONN.commit()
643
- # Preview newly created objects
644
- if low.startswith("create view"):
645
- m = re.match(r"(?is)^\s*create\s+view\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+(select.*)$", low)
646
- name = m.group(2) if m else None
647
- if name:
648
- try:
649
- df = run_df(CONN, f"SELECT * FROM {name}")
650
- return df, None, None, note
651
- except Exception:
652
- return None, "View created but could not be queried.", None, note
653
- if low.startswith("create table"):
654
- tbl = created_tbl
655
- if not tbl:
656
- m = re.match(r"(?is)^\s*create\s+table\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
657
- tbl = m.group(2) if m else None
658
- if tbl:
659
- try:
660
- df = run_df(CONN, f"SELECT * FROM {tbl}")
661
- return df, None, None, note
662
- except Exception:
663
- return None, "Table created but could not be queried.", None, note
 
664
  return pd.DataFrame(), None, None, note
665
  except Exception as e:
666
  # Tailored messages
@@ -689,19 +707,21 @@ def answer_df(answer_sql: List[str]) -> Optional[pd.DataFrame]:
689
  # temp preview
690
  m = re.match(r"(?is)^\s*create\s+view\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
691
  view_name = m.group(2) if m else "vw_tmp"
692
- cur = CONN.cursor()
693
- cur.execute(f"DROP VIEW IF EXISTS {view_name}")
694
- cur.execute(sql)
695
- CONN.commit()
 
696
  return run_df(CONN, f"SELECT * FROM {view_name}")
697
  if low.startswith("create table"):
698
  m = re.match(r"(?is)^\s*create\s+table\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
699
  tbl = m.group(2) if m else None
700
- cur = CONN.cursor()
701
- if tbl:
702
- cur.execute(f"DROP TABLE IF EXISTS {tbl}")
703
- cur.execute(sql)
704
- CONN.commit()
 
705
  if tbl:
706
  return run_df(CONN, f"SELECT * FROM {tbl}")
707
  except Exception:
@@ -710,7 +730,7 @@ def answer_df(answer_sql: List[str]) -> Optional[pd.DataFrame]:
710
 
711
  def validate_answer(q: Dict[str,Any], student_sql: str, df_student: Optional[pd.DataFrame]) -> Tuple[bool, str]:
712
  df_expected = answer_df(q["answer_sql"])
713
- # If we can't build a canonical DF (e.g., DDL side effect), we accept any successful execution as correct
714
  if df_expected is None:
715
  return (df_student is not None), f"**Explanation:** Your statement executed successfully for this task."
716
  if df_student is None:
@@ -719,13 +739,14 @@ def validate_answer(q: Dict[str,Any], student_sql: str, df_student: Optional[pd.
719
 
720
  def log_attempt(user_id: str, qid: str, category: str, correct: bool, sql_text: str,
721
  time_taken: float, difficulty: int, source: str, notes: str):
722
- cur = CONN.cursor()
723
- cur.execute("""
724
- INSERT INTO attempts (user_id, question_id, category, correct, sql_text, timestamp, time_taken, difficulty, source, notes)
725
- VALUES (?,?,?,?,?,?,?,?,?,?)
726
- """, (user_id, qid, category, int(correct), sql_text, datetime.now(timezone.utc).isoformat(),
727
- time_taken, difficulty, source, notes))
728
- CONN.commit()
 
729
 
730
  # -------------------- UI callbacks --------------------
731
  def start_session(name: str, session: dict):
 
15
  import time
16
  import random
17
  import sqlite3
18
+ import threading
19
  from dataclasses import dataclass, asdict
20
  from datetime import datetime, timezone
21
  from typing import List, Dict, Any, Tuple, Optional
 
46
  DB_DIR = "/data" if os.path.exists("/data") else "."
47
  DB_PATH = os.path.join(DB_DIR, "sql_trainer_dynamic.db")
48
  EXPORT_DIR = "."
49
+ ADMIN_KEY = os.getenv("ADMIN_KEY", "demo")
50
  RANDOM_SEED = int(os.getenv("RANDOM_SEED", "7"))
51
  random.seed(RANDOM_SEED)
52
  SYS_RAND = random.SystemRandom()
 
66
 
67
  def draw_dynamic_erd(schema: Dict[str, Any]) -> Image.Image:
68
  """
69
+ Draw a simple ERD for the current randomized schema.
70
  schema = {
71
  "domain": "bookstore",
72
  "tables": [
73
+ {"name":"authors","columns":[{"name":"author_id","type":"INTEGER"}, ...],
74
  "pk":["author_id"], "fks":[{"columns":["author_id"],"ref_table":"...","ref_columns":["..."]}],
75
  "rows":[{...}, {...}]}
76
  ]
 
113
  ax.text(0.5, 0.06, f"Domain: {schema.get('domain','unknown')}", fontsize=9, ha="center")
114
  return _to_pil(fig)
115
 
116
+ # -------------------- SQLite connection + locking --------------------
117
+ DB_LOCK = threading.RLock()
118
+
119
  def connect_db():
120
+ """
121
+ Single shared connection that is allowed to be used across threads.
122
+ All operations (reads + writes) are serialized via DB_LOCK.
123
+ WAL mode improves read concurrency.
124
+ """
125
+ con = sqlite3.connect(DB_PATH, check_same_thread=False)
126
+ con.execute("PRAGMA journal_mode=WAL;")
127
+ con.execute("PRAGMA synchronous=NORMAL;")
128
  con.execute("PRAGMA foreign_keys = ON;")
129
  return con
130
 
131
  CONN = connect_db()
132
 
133
  def init_progress_tables(con: sqlite3.Connection):
134
+ with DB_LOCK:
135
+ cur = con.cursor()
136
+ cur.execute("""
137
+ CREATE TABLE IF NOT EXISTS users (
138
+ user_id TEXT PRIMARY KEY,
139
+ name TEXT,
140
+ created_at TEXT
141
+ )
142
+ """)
143
+ cur.execute("""
144
+ CREATE TABLE IF NOT EXISTS attempts (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ user_id TEXT,
147
+ question_id TEXT,
148
+ category TEXT,
149
+ correct INTEGER,
150
+ sql_text TEXT,
151
+ timestamp TEXT,
152
+ time_taken REAL,
153
+ difficulty INTEGER,
154
+ source TEXT,
155
+ notes TEXT
156
+ )
157
+ """)
158
+ cur.execute("""
159
+ CREATE TABLE IF NOT EXISTS session_meta (
160
+ id INTEGER PRIMARY KEY CHECK (id=1),
161
+ domain TEXT,
162
+ schema_json TEXT
163
+ )
164
+ """)
165
+ con.commit()
166
 
167
  init_progress_tables(CONN)
168
 
 
423
 
424
  # -------------------- Schema install & question handling --------------------
425
  def drop_existing_domain_tables(con: sqlite3.Connection, keep_internal=True):
426
+ with DB_LOCK:
427
+ cur = con.cursor()
428
+ cur.execute("SELECT name, type FROM sqlite_master WHERE type IN ('table','view')")
429
+ items = cur.fetchall()
430
+ for name, typ in items:
431
+ if keep_internal and name in ("users","attempts","session_meta"):
432
+ continue
433
+ try:
434
+ cur.execute(f"DROP {typ.upper()} IF EXISTS {name}")
435
+ except Exception:
436
+ pass
437
+ con.commit()
438
 
439
  def install_schema(con: sqlite3.Connection, schema: Dict[str,Any]):
440
  drop_existing_domain_tables(con, keep_internal=True)
441
+ with DB_LOCK:
442
+ cur = con.cursor()
443
+ # Create tables first
444
+ for t in schema.get("tables", []):
445
+ cols_sql = []
446
+ pk = t.get("pk", [])
447
+ for c in t.get("columns", []):
448
+ cname = c["name"]
449
+ ctype = c.get("type","TEXT")
450
+ cols_sql.append(f"{cname} {ctype}")
451
+ if pk:
452
+ cols_sql.append(f"PRIMARY KEY ({', '.join(pk)})")
453
+ create_sql = f"CREATE TABLE {t['name']} ({', '.join(cols_sql)})"
454
+ cur.execute(create_sql)
455
+ # Insert rows
456
+ for t in schema.get("tables", []):
457
+ if not t.get("rows"):
 
 
 
 
 
 
 
 
 
 
 
458
  continue
459
+ cols = [c["name"] for c in t.get("columns", [])]
460
+ qmarks = ",".join(["?"]*len(cols))
461
+ insert_sql = f"INSERT INTO {t['name']} ({', '.join(cols)}) VALUES ({qmarks})"
462
+ for r in t["rows"]:
463
+ if isinstance(r, dict):
464
+ vals = [r.get(col, None) for col in cols]
465
+ elif isinstance(r, (list, tuple)):
466
+ vals = list(r) + [None]*(len(cols)-len(r))
467
+ vals = vals[:len(cols)]
468
+ else:
469
+ continue
470
+ cur.execute(insert_sql, vals)
471
+ con.commit()
472
+ # Persist schema JSON
473
+ cur.execute("INSERT OR REPLACE INTO session_meta(id, domain, schema_json) VALUES (1, ?, ?)",
474
+ (schema.get("domain","unknown"), json.dumps(schema)))
475
+ con.commit()
476
 
477
  def run_df(con: sqlite3.Connection, sql: str) -> pd.DataFrame:
478
+ with DB_LOCK:
479
+ return pd.read_sql_query(sql, con)
480
 
481
  def rewrite_select_into(sql: str) -> Tuple[str, Optional[str]]:
482
  s = sql.strip().strip(";")
 
505
  missing_on = (" join " in low) and (" on " not in low) and (" using " not in low) and (" natural " not in low)
506
  if comma_from or missing_on:
507
  try:
508
+ with DB_LOCK:
509
+ cur = con.cursor()
510
+ if comma_from:
511
+ t1, t2 = comma_from.groups()
512
+ else:
513
+ m = re.search(r"\bfrom\b\s+([a-z_]\w*)", low)
514
+ j = re.search(r"\bjoin\b\s+([a-z_]\w*)", low)
515
+ if not m or not j:
516
+ return "Possible cartesian product: no join condition detected."
517
+ t1, t2 = m.group(1), j.group(1)
518
+ cur.execute(f"SELECT COUNT(*) FROM {t1}")
519
+ n1 = cur.fetchone()[0]
520
+ cur.execute(f"SELECT COUNT(*) FROM {t2}")
521
+ n2 = cur.fetchone()[0]
522
  prod = n1 * n2
523
  if len(df_result) == prod and prod > 0:
524
  return f"Result row count equals {n1}×{n2}={prod}. Likely cartesian product (missing join)."
 
595
 
596
  # -------------------- Progress + mastery --------------------
597
  def upsert_user(con: sqlite3.Connection, user_id: str, name: str):
598
+ with DB_LOCK:
599
+ cur = con.cursor()
600
+ cur.execute("SELECT user_id FROM users WHERE user_id = ?", (user_id,))
601
+ if cur.fetchone() is None:
602
+ cur.execute("INSERT INTO users (user_id, name, created_at) VALUES (?, ?, ?)",
603
+ (user_id, name, datetime.now(timezone.utc).isoformat()))
604
+ else:
605
+ cur.execute("UPDATE users SET name=? WHERE user_id=?", (name, user_id))
606
+ con.commit()
607
 
608
  CATEGORIES_ORDER = [
609
  "SELECT *", "SELECT columns", "WHERE", "Aliases",
 
621
  return pd.DataFrame(rows)
622
 
623
  def fetch_attempts(con: sqlite3.Connection, user_id: str) -> pd.DataFrame:
624
+ with DB_LOCK:
625
+ return pd.read_sql_query("SELECT * FROM attempts WHERE user_id=? ORDER BY id DESC", con, params=(user_id,))
626
 
627
  def pick_next_question(user_id: str) -> Dict[str,Any]:
628
  df = fetch_attempts(CONN, user_id)
 
654
  warn = detect_cartesian(CONN, sql_rew, df)
655
  return df, None, warn, note
656
  else:
657
+ with DB_LOCK:
658
+ cur = CONN.cursor()
659
+ cur.execute(sql_rew)
660
+ CONN.commit()
661
+ # Preview newly created objects
662
+ if low.startswith("create view"):
663
+ m = re.match(r"(?is)^\s*create\s+view\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+(select.*)$", low)
664
+ name = m.group(2) if m else None
665
+ if name:
666
+ try:
667
+ df = pd.read_sql_query(f"SELECT * FROM {name}", CONN)
668
+ return df, None, None, note
669
+ except Exception:
670
+ return None, "View created but could not be queried.", None, note
671
+ if low.startswith("create table"):
672
+ tbl = created_tbl
673
+ if not tbl:
674
+ m = re.match(r"(?is)^\s*create\s+table\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
675
+ tbl = m.group(2) if m else None
676
+ if tbl:
677
+ try:
678
+ df = pd.read_sql_query(f"SELECT * FROM {tbl}", CONN)
679
+ return df, None, None, note
680
+ except Exception:
681
+ return None, "Table created but could not be queried.", None, note
682
  return pd.DataFrame(), None, None, note
683
  except Exception as e:
684
  # Tailored messages
 
707
  # temp preview
708
  m = re.match(r"(?is)^\s*create\s+view\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
709
  view_name = m.group(2) if m else "vw_tmp"
710
+ with DB_LOCK:
711
+ cur = CONN.cursor()
712
+ cur.execute(f"DROP VIEW IF EXISTS {view_name}")
713
+ cur.execute(sql)
714
+ CONN.commit()
715
  return run_df(CONN, f"SELECT * FROM {view_name}")
716
  if low.startswith("create table"):
717
  m = re.match(r"(?is)^\s*create\s+table\s+(if\s+not\s+exists\s+)?([a-z_]\w*)\s+as\s+select.*$", low)
718
  tbl = m.group(2) if m else None
719
+ with DB_LOCK:
720
+ cur = CONN.cursor()
721
+ if tbl:
722
+ cur.execute(f"DROP TABLE IF EXISTS {tbl}")
723
+ cur.execute(sql)
724
+ CONN.commit()
725
  if tbl:
726
  return run_df(CONN, f"SELECT * FROM {tbl}")
727
  except Exception:
 
730
 
731
  def validate_answer(q: Dict[str,Any], student_sql: str, df_student: Optional[pd.DataFrame]) -> Tuple[bool, str]:
732
  df_expected = answer_df(q["answer_sql"])
733
+ # If we can't build a canonical DF (e.g., DDL side effect), accept any successful execution as correct
734
  if df_expected is None:
735
  return (df_student is not None), f"**Explanation:** Your statement executed successfully for this task."
736
  if df_student is None:
 
739
 
740
  def log_attempt(user_id: str, qid: str, category: str, correct: bool, sql_text: str,
741
  time_taken: float, difficulty: int, source: str, notes: str):
742
+ with DB_LOCK:
743
+ cur = CONN.cursor()
744
+ cur.execute("""
745
+ INSERT INTO attempts (user_id, question_id, category, correct, sql_text, timestamp, time_taken, difficulty, source, notes)
746
+ VALUES (?,?,?,?,?,?,?,?,?,?)
747
+ """, (user_id, qid, category, int(correct), sql_text, datetime.now(timezone.utc).isoformat(),
748
+ time_taken, difficulty, source, notes))
749
+ CONN.commit()
750
 
751
  # -------------------- UI callbacks --------------------
752
  def start_session(name: str, session: dict):