Guest

Untitled 1111

Apr 9th, 2026
25
0
Never
Not a member of GistPad yet? Sign Up, it unlocks many cool features!
None 51.89 KB | None | 0 0
  1. """
  2. Grand Vision Secretary — Local Flask Application
  3. Runs on localhost:5000
  4. Handles: Word Count Tracking, Brief Implementation, Workflow Registry,
  5. Session Log, Usage Dashboard, PDF to Text Converter
  6. """
  7. import os
  8. import json
  9. import re
  10. import sqlite3
  11. import fitz # PyMuPDF
  12. from datetime import datetime
  13. from flask import Flask, render_template, request, jsonify, redirect, url_for
  14.  
  15. # Fix: Force Flask to look for templates in the folder where app.py lives
  16. current_dir = os.path.dirname(os.path.abspath(__file__))
  17. template_dir = os.path.join(current_dir, 'templates')
  18.  
  19. app = Flask(__name__, template_folder=template_dir)
  20.  
  21. # --- PATHS ---
  22. BASE_DIR = r"V:\GrandVision"
  23. AGENTS_DIR = os.path.join(BASE_DIR, "Secretary", "agents")
  24. WORKFLOW_DIR = os.path.join(BASE_DIR, "Secretary", "workflow")
  25. REGISTRY_PATH = os.path.join(WORKFLOW_DIR, "workflow_registry.jsonl")
  26. DB_PATH = os.path.join(BASE_DIR, "Secretary", "secretary.db")
  27.  
  28. # --- DATABASE SETUP ---
  29. def get_db():
  30. conn = sqlite3.connect(DB_PATH)
  31. conn.row_factory = sqlite3.Row
  32. return conn
  33.  
  34. def init_db():
  35. conn = get_db()
  36. conn.executescript("""
  37. CREATE TABLE IF NOT EXISTS word_count_sessions (
  38. id INTEGER PRIMARY KEY AUTOINCREMENT,
  39. session_date TEXT NOT NULL,
  40. agent_identifier TEXT NOT NULL,
  41. session_number INTEGER,
  42. author_word_count INTEGER NOT NULL,
  43. agent_word_count INTEGER NOT NULL,
  44. author_token_estimate REAL NOT NULL,
  45. agent_token_estimate REAL NOT NULL,
  46. claude_usage_panel_tokens INTEGER,
  47. notes TEXT,
  48. created_at TEXT NOT NULL
  49. );
  50.  
  51. CREATE TABLE IF NOT EXISTS session_log (
  52. id INTEGER PRIMARY KEY AUTOINCREMENT,
  53. timestamp TEXT NOT NULL,
  54. action_type TEXT NOT NULL,
  55. description TEXT NOT NULL,
  56. agent TEXT,
  57. details TEXT
  58. );
  59.  
  60. CREATE TABLE IF NOT EXISTS brief_implementations (
  61. id INTEGER PRIMARY KEY AUTOINCREMENT,
  62. timestamp TEXT NOT NULL,
  63. target_script TEXT NOT NULL,
  64. action TEXT NOT NULL,
  65. authorised INTEGER NOT NULL,
  66. diff TEXT
  67. );
  68. """)
  69. conn.commit()
  70. conn.close()
  71.  
  72. def log_action(action_type, description, agent=None, details=None):
  73. conn = get_db()
  74. conn.execute(
  75. "INSERT INTO session_log (timestamp, action_type, description, agent, details) VALUES (?,?,?,?,?)",
  76. (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), action_type, description, agent, details)
  77. )
  78. conn.commit()
  79. conn.close()
  80.  
  81. # --- WORD COUNT UTILITIES ---
  82. def parse_dialogue(text):
  83. """
  84. Parses pasted session dialogue into Author and Agent turns.
  85. Detects speaker by common patterns: 'Human:', 'You:', 'Claude:', 'Assistant:'
  86. Returns dict with author_words and agent_words counts.
  87. """
  88. author_patterns = r'^(Human|You|Author)\s*:'
  89. agent_patterns = r'^(Claude|Assistant|CA|CS|PIT|PIF|DOT|CIO|MRA|MD|Dean|Secretary)\s*[\u26c8\u03a3\u2b21\u25ce\u25c7\u2699\u25b3\u229e\u03a8]?\s*:'
  90.  
  91. lines = text.strip().split('\n')
  92. author_words = 0
  93. agent_words = 0
  94. current_speaker = None
  95. current_block = []
  96.  
  97. for line in lines:
  98. if re.match(author_patterns, line, re.IGNORECASE):
  99. if current_speaker == 'agent' and current_block:
  100. agent_words += len(' '.join(current_block).split())
  101. elif current_speaker == 'author' and current_block:
  102. author_words += len(' '.join(current_block).split())
  103. current_speaker = 'author'
  104. content = re.sub(author_patterns, '', line, flags=re.IGNORECASE).strip()
  105. current_block = [content] if content else []
  106. elif re.match(agent_patterns, line, re.IGNORECASE):
  107. if current_speaker == 'author' and current_block:
  108. author_words += len(' '.join(current_block).split())
  109. elif current_speaker == 'agent' and current_block:
  110. agent_words += len(' '.join(current_block).split())
  111. current_speaker = 'agent'
  112. content = re.sub(agent_patterns, '', line, flags=re.IGNORECASE).strip()
  113. current_block = [content] if content else []
  114. else:
  115. if current_block is not None:
  116. current_block.append(line)
  117.  
  118. # Flush final block
  119. if current_speaker == 'author' and current_block:
  120. author_words += len(' '.join(current_block).split())
  121. elif current_speaker == 'agent' and current_block:
  122. agent_words += len(' '.join(current_block).split())
  123.  
  124. # Fallback: if no speakers detected, split 50/50 by paragraphs
  125. if author_words == 0 and agent_words == 0:
  126. total = len(text.split())
  127. author_words = total // 2
  128. agent_words = total - author_words
  129.  
  130. return {
  131. 'author_words': author_words,
  132. 'agent_words': agent_words,
  133. 'author_tokens': round(author_words * 1.33),
  134. 'agent_tokens': round(agent_words * 1.33),
  135. 'total_words': author_words + agent_words,
  136. 'total_tokens': round((author_words + agent_words) * 1.33)
  137. }
  138.  
  139. # --- ROUTES ---
  140.  
  141. @app.route('/')
  142. def index():
  143. return redirect(url_for('word_count'))
  144.  
  145. @app.route('/word-count', methods=['GET', 'POST'])
  146. def word_count():
  147. result = None
  148. error = None
  149.  
  150. if request.method == 'POST':
  151. dialogue = request.form.get('dialogue', '').strip()
  152. agent = request.form.get('agent', '').strip()
  153. session_number = request.form.get('session_number', '').strip()
  154. claude_usage = request.form.get('claude_usage', '').strip()
  155. notes = request.form.get('notes', '').strip()
  156.  
  157. if not dialogue:
  158. error = 'No dialogue provided.'
  159. elif not agent:
  160. error = 'Agent identifier required.'
  161. else:
  162. parsed = parse_dialogue(dialogue)
  163. result = parsed
  164. result['agent'] = agent # type: ignore
  165. result['session_number'] = session_number # type: ignore
  166. result['notes'] = notes # type: ignore
  167. result['claude_usage'] = claude_usage # type: ignore
  168.  
  169. # Store in DB
  170. conn = get_db()
  171. conn.execute("""
  172. INSERT INTO word_count_sessions
  173. (session_date, agent_identifier, session_number, author_word_count,
  174. agent_word_count, author_token_estimate, agent_token_estimate,
  175. claude_usage_panel_tokens, notes, created_at)
  176. VALUES (?,?,?,?,?,?,?,?,?,?)
  177. """, (
  178. datetime.now().strftime("%Y-%m-%d"),
  179. agent,
  180. int(session_number) if session_number.isdigit() else None,
  181. parsed['author_words'],
  182. parsed['agent_words'],
  183. parsed['author_tokens'],
  184. parsed['agent_tokens'],
  185. int(claude_usage) if claude_usage.isdigit() else None,
  186. notes,
  187. datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  188. ))
  189. conn.commit()
  190. conn.close()
  191.  
  192. log_action('WORD_COUNT', f'Session logged for {agent}', agent=agent)
  193.  
  194. # Fetch recent sessions
  195. conn = get_db()
  196. recent = conn.execute("""
  197. SELECT * FROM word_count_sessions
  198. ORDER BY created_at DESC LIMIT 10
  199. """).fetchall()
  200.  
  201. # Totals per agent
  202. agent_totals = conn.execute("""
  203. SELECT agent_identifier,
  204. SUM(author_word_count) as total_author_words,
  205. SUM(agent_word_count) as total_agent_words,
  206. SUM(author_token_estimate) as total_author_tokens,
  207. SUM(agent_token_estimate) as total_agent_tokens,
  208. COUNT(*) as session_count
  209. FROM word_count_sessions
  210. GROUP BY agent_identifier
  211. ORDER BY total_agent_tokens DESC
  212. """).fetchall()
  213. conn.close()
  214.  
  215. return render_template('word_count.html',
  216. result=result,
  217. error=error,
  218. recent=recent,
  219. agent_totals=agent_totals)
  220.  
  221. @app.route('/brief-implementer')
  222. def brief_implementer():
  223. scripts = []
  224. if os.path.exists(AGENTS_DIR):
  225. scripts = [f for f in os.listdir(AGENTS_DIR) if f.endswith('.py')]
  226. conn = get_db()
  227. implementations = conn.execute("""
  228. SELECT * FROM brief_implementations
  229. ORDER BY timestamp DESC LIMIT 20
  230. """).fetchall()
  231. conn.close()
  232. return render_template('brief_implementer.html', scripts=scripts,
  233. implementations=implementations)
  234.  
  235.  
  236. @app.route('/brief-implementer/parse', methods=["POST"])
  237. def brief_implementer_parse():
  238. import difflib
  239. data = request.get_json()
  240. script_name = data.get("script", "")
  241. brief_text = data.get("brief", "").strip()
  242.  
  243. script_path = os.path.join(AGENTS_DIR, script_name)
  244. if not os.path.exists(script_path):
  245. return jsonify({"error": f"Script not found: {script_name}"})
  246.  
  247. with open(script_path, "r", encoding="utf-8") as f:
  248. script_content = f.read()
  249.  
  250. # Parse brief fields
  251. lines = {line.split(":", 1)[0].strip().upper(): line.split(":", 1)[1].strip()
  252. for line in brief_text.splitlines() if ":" in line}
  253.  
  254. action = lines.get("ACTION", "").upper()
  255. if action not in ("ADD", "REPLACE", "DELETE"):
  256. return jsonify({"error": "Brief must contain ACTION: ADD, REPLACE, or DELETE."})
  257.  
  258. # Locate target — AFTER, REPLACE, or DELETE field
  259. locator_key = "AFTER" if action == "ADD" else ("REPLACE" if action == "REPLACE" else "DELETE")
  260. locator = lines.get(locator_key, "").strip()
  261. new_content = lines.get("INSERT", lines.get("WITH", "")).strip()
  262.  
  263. if not locator:
  264. return jsonify({"error": f"Brief must contain {locator_key}: <exact step text to locate>"})
  265.  
  266. # Find locator in script
  267. if locator not in script_content:
  268. # Try partial — match just the action field
  269. action_match = re.search(r'"action"\s*:\s*"' + re.escape(
  270. locator.strip('{}').split('"action"')[1].split('"')[2]
  271. if '"action"' in locator else locator
  272. ) + r'"', script_content)
  273. if action_match:
  274. start = script_content.rfind('\n', 0, action_match.start()) + 1
  275. end = script_content.find('\n', action_match.end()) + 1
  276. locator = script_content[start:end].rstrip()
  277. else:
  278. return jsonify({
  279. "error": f"Locator not found in script: '{locator[:80]}'. Copy the exact step text from the script."
  280. })
  281.  
  282. # Apply change
  283. if action == "DELETE":
  284. after = script_content.replace(locator, "", 1)
  285. description = f"DELETE step: {locator[:60]}"
  286. elif action == "ADD":
  287. after = script_content.replace(locator, locator + ",\n " + new_content, 1)
  288. description = f"ADD after: {locator[:60]}"
  289. else: # REPLACE
  290. after = script_content.replace(locator, new_content, 1)
  291. description = f"REPLACE: {locator[:60]}"
  292.  
  293. # Generate diff
  294. diff_lines = list(difflib.unified_diff(
  295. script_content.splitlines(),
  296. after.splitlines(),
  297. lineterm='',
  298. n=3
  299. ))
  300. diff_display = [l for l in diff_lines
  301. if not l.startswith('---') and not l.startswith('+++')]
  302.  
  303. if not diff_display:
  304. return jsonify({"error": "No difference detected. Check the locator matches exactly."})
  305.  
  306. return jsonify({
  307. "action": action,
  308. "description": description,
  309. "target_match": locator,
  310. "new_content": new_content,
  311. "diff": diff_display,
  312. "script": script_name,
  313. "script_path": script_path,
  314. "original_content": script_content
  315. })
  316.  
  317. @app.route("/brief-implementer/authorise", methods=["POST"])
  318. def brief_implementer_authorise():
  319. data = request.get_json()
  320. script_path = data.get("script_path", "")
  321. original_content = data.get("original_content", "")
  322. target_match = data.get("target_match", "")
  323. new_content = data.get("new_content", "")
  324. action = data.get("action", "")
  325. description = data.get("description", "")
  326. script_name = data.get("script", "")
  327.  
  328. if not script_path or not os.path.exists(script_path):
  329. return jsonify({"success": False, "error": "Script path invalid."})
  330.  
  331. try:
  332. if action == "DELETE":
  333. updated = original_content.replace(target_match, "")
  334. elif action in ("ADD", "REPLACE"):
  335. if target_match not in original_content:
  336. return jsonify({"success": False, "error": "Target match string not found in script."})
  337. if action == "ADD":
  338. updated = original_content.replace(target_match, target_match + ",\n " + new_content, 1)
  339.  
  340. else:
  341. updated = original_content.replace(target_match, new_content)
  342. else:
  343. return jsonify({"success": False, "error": f"Unknown action: {action}"})
  344.  
  345. with open(script_path, "w", encoding="utf-8") as f:
  346. f.write(updated)
  347.  
  348. conn = get_db()
  349. conn.execute(
  350. "INSERT INTO brief_implementations (timestamp, target_script, action, authorised, diff) VALUES (?,?,?,?,?)",
  351. (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), script_name, description, 1, json.dumps(data.get("diff", [])))
  352. )
  353. conn.commit()
  354. conn.close()
  355. log_action("BRIEF_IMPL", f"Authorised: {description}", details=script_name)
  356. return jsonify({"success": True})
  357. except Exception as e:
  358. return jsonify({"success": False, "error": str(e)})
  359.  
  360. @app.route('/brief-compiler/authorise', methods=['POST'])
  361. def brief_compiler_authorise():
  362. data = request.get_json(force=True, silent=True)
  363. if not data or 'compiled' not in data or 'title' not in data:
  364. return jsonify({"status": "error", "message": "Missing compiled plan or title"}), 400
  365.  
  366. clean_title = re.sub(r'[\\/*?:"<>|]', "_", data['title'])
  367. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  368. filename = f"{clean_title}_{timestamp}.json"
  369.  
  370. # Path: V:\GrandVision\Secretary\briefs
  371. briefs_dir = os.path.join(BASE_DIR, "Secretary", "briefs")
  372. os.makedirs(briefs_dir, exist_ok=True)
  373. filepath = os.path.join(briefs_dir, filename)
  374.  
  375. try:
  376. with open(filepath, 'w', encoding='utf-8') as f:
  377. f.write(data['compiled'])
  378. return jsonify({"status": "ok", "filename": filename, "message": "Saved successfully"})
  379. except Exception as e:
  380. return jsonify({"status": "error", "message": str(e)}), 500
  381.  
  382. @app.route('/workflow-registry', methods=['GET', 'POST'])
  383. def workflow_registry():
  384. entries = []
  385. error = None
  386. success = None
  387.  
  388. if request.method == 'POST':
  389. entry = {
  390. 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  391. 'document_type': request.form.get('document_type', ''),
  392. 'issuing_agent': request.form.get('issuing_agent', ''),
  393. 'receiving_agent': request.form.get('receiving_agent', ''),
  394. 'brief_reference': request.form.get('brief_reference', ''),
  395. 'filename': request.form.get('filename', '')
  396. }
  397. if not entry['document_type'] or not entry['issuing_agent']:
  398. error = 'Document type and issuing agent are required.'
  399. else:
  400. os.makedirs(os.path.dirname(REGISTRY_PATH), exist_ok=True)
  401. with open(REGISTRY_PATH, 'a', encoding='utf-8') as f:
  402. f.write(json.dumps(entry) + '\n')
  403. log_action('REGISTRY', f'Entry added: {entry["document_type"]} from {entry["issuing_agent"]}')
  404. success = 'Registry entry added.'
  405.  
  406. if os.path.exists(REGISTRY_PATH):
  407. with open(REGISTRY_PATH, 'r', encoding='utf-8') as f:
  408. for line in f:
  409. line = line.strip()
  410. if line:
  411. entries.append(json.loads(line))
  412. entries = list(reversed(entries))
  413.  
  414. return render_template('workflow_registry.html',
  415. entries=entries,
  416. error=error,
  417. success=success)
  418.  
  419. @app.route('/session-log')
  420. def session_log():
  421. conn = get_db()
  422. logs = conn.execute("""
  423. SELECT * FROM session_log
  424. ORDER BY timestamp DESC LIMIT 100
  425. """).fetchall()
  426. conn.close()
  427. return render_template('session_log.html', logs=logs)
  428.  
  429. @app.route('/usage-dashboard')
  430. def usage_dashboard():
  431. conn = get_db()
  432. sessions = conn.execute("""
  433. SELECT session_date, agent_identifier,
  434. SUM(author_token_estimate + agent_token_estimate) as total_tokens,
  435. SUM(claude_usage_panel_tokens) as claude_tokens
  436. FROM word_count_sessions
  437. GROUP BY session_date, agent_identifier
  438. ORDER BY session_date DESC
  439. """).fetchall()
  440.  
  441. grand_totals = conn.execute("""
  442. SELECT
  443. SUM(author_word_count) as total_author_words,
  444. SUM(agent_word_count) as total_agent_words,
  445. SUM(author_token_estimate) as total_author_tokens,
  446. SUM(agent_token_estimate) as total_agent_tokens,
  447. SUM(claude_usage_panel_tokens) as total_claude_tokens,
  448. COUNT(*) as total_sessions
  449. FROM word_count_sessions
  450. """).fetchone()
  451. conn.close()
  452.  
  453. return render_template('usage_dashboard.html',
  454. sessions=sessions,
  455. grand_totals=grand_totals)
  456.  
  457. @app.route('/pdf-converter')
  458. def pdf_converter():
  459. return render_template('pdf_converter.html')
  460.  
  461. @app.route('/pdf-converter/convert', methods=['POST'])
  462. def pdf_converter_convert():
  463. if 'file' not in request.files:
  464. return jsonify({"status": "error", "message": "No file part"}), 400
  465.  
  466. files = request.files.getlist('file')
  467. if not files or all(f.filename == '' for f in files):
  468. return jsonify({"status": "error", "message": "No selected files"}), 400
  469.  
  470. # Output directory defined using os.path.join
  471. plaintext_dir = os.path.join(BASE_DIR, "Secretary", "plaintext")
  472. os.makedirs(plaintext_dir, exist_ok=True)
  473.  
  474. results = []
  475.  
  476. for file in files:
  477. if file.filename == '':
  478. continue
  479.  
  480. try:
  481. # 1. Extract text
  482. file_bytes = file.read()
  483. doc = fitz.open(stream=file_bytes, filetype="pdf")
  484.  
  485. text_blocks = []
  486. for page in doc:
  487. text = page.get_text()
  488. text_blocks.append(text)
  489. doc.close()
  490.  
  491. full_text = "\n".join(text_blocks)
  492.  
  493. # 2. Clean text
  494. # Strip excessive blank lines (max two consecutive)
  495. cleaned_text = re.sub(r'\n{3,}', '\n\n', full_text)
  496. cleaned_text = cleaned_text.strip()
  497.  
  498. # 3. Derive filename
  499. base_name, _ = os.path.splitext(str(file.filename))
  500. # Remove illegal path characters
  501. safe_base_name = re.sub(r'[^a-zA-Z0-9_\-\s]', '', base_name).strip()
  502. output_filename = f"{safe_base_name}.txt"
  503.  
  504. # 4. Save file
  505. save_path = os.path.join(plaintext_dir, output_filename)
  506. with open(save_path, 'w', encoding='utf-8') as f:
  507. f.write(cleaned_text)
  508.  
  509. # 5. Record result
  510. char_count = len(cleaned_text)
  511. word_count = len(cleaned_text.split())
  512.  
  513. results.append({
  514. "filename": file.filename,
  515. "output_filename": output_filename,
  516. "word_count": word_count,
  517. "char_count": char_count,
  518. "path": save_path,
  519. "status": "ok",
  520. "error": None
  521. })
  522.  
  523. except Exception as e:
  524. results.append({
  525. "filename": file.filename,
  526. "output_filename": "",
  527. "word_count": 0,
  528. "char_count": 0,
  529. "path": "",
  530. "status": "error",
  531. "error": str(e)
  532. })
  533.  
  534. return jsonify({"status": "ok", "results": results})
  535.  
  536. @app.route('/pdf-converter/clear', methods=['POST'])
  537. def pdf_converter_clear():
  538. plaintext_dir = os.path.join(BASE_DIR, "Secretary", "plaintext")
  539. deleted_count = 0
  540. if os.path.exists(plaintext_dir):
  541. for filename in os.listdir(plaintext_dir):
  542. if filename.endswith('.txt'):
  543. try:
  544. filepath = os.path.join(plaintext_dir, filename)
  545. os.remove(filepath)
  546. deleted_count += 1
  547. except Exception:
  548. pass
  549. return jsonify({"status": "ok", "deleted": deleted_count})
  550.  
  551. @app.route('/pdf-converter/files', methods=['GET'])
  552. def pdf_converter_files():
  553. plaintext_dir = os.path.join(BASE_DIR, "Secretary", "plaintext")
  554. files_list = []
  555.  
  556. if os.path.exists(plaintext_dir):
  557. for filename in os.listdir(plaintext_dir):
  558. if filename.endswith('.txt'):
  559. filepath = os.path.join(plaintext_dir, filename)
  560. try:
  561. stats = os.stat(filepath)
  562. # Get word count directly from reading the file contents
  563. with open(filepath, 'r', encoding='utf-8') as f:
  564. content = f.read()
  565. word_count = len(content.split())
  566.  
  567. mod_time = datetime.fromtimestamp(stats.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
  568.  
  569. files_list.append({
  570. "filename": filename,
  571. "word_count": word_count,
  572. "modified": mod_time,
  573. "path": filepath
  574. })
  575. except Exception:
  576. pass
  577.  
  578. # Sort files by modified date descending
  579. files_list.sort(key=lambda x: x['modified'], reverse=True)
  580. return jsonify({"status": "ok", "files": files_list})
  581.  
  582. if __name__ == '__main__':
  583. # Initialize database
  584. try:
  585. init_db()
  586. except Exception as e:
  587. print(f"Database init failed: {e}")
  588.  
  589. # PID Registration
  590. import atexit
  591. # We use a more robust path detection for the PID file
  592. current_file_dir = os.path.dirname(os.path.abspath(__file__))
  593. pid_path = os.path.join(current_file_dir, "secretary.pid")
  594.  
  595. try:
  596. with open(pid_path, "w") as f:
  597. f.write(str(os.getpid()))
  598.  
  599. def cleanup_pid():
  600. if os.path.exists(pid_path):
  601. os.remove(pid_path)
  602. atexit.register(cleanup_pid)
  603. except Exception as e:
  604. print(f"Could not write PID file: {e}")
  605.  
  606. print("\n[SECRETARY] Grand Vision Secretary starting...")
  607. print("Accessible at: http://127.0.0.1:5000";)
  608.  
  609. # Run the app
  610. # host='0.0.0.0' ensures it listens on all local addresses
  611. app.run(debug=True, port=5000, host='0.0.0.0', use_reloader=False)
RAW Paste Data Copied