brandonmusic commited on
Commit
ceeca06
·
verified ·
1 Parent(s): bd25874

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +6 -67
app.py CHANGED
@@ -19,18 +19,14 @@ from requests.adapters import HTTPAdapter
19
  from http.client import HTTPConnection
20
  from gpt_helpers import ask_gpt41_mini
21
  from prompt_builder import build_grok_prompt, build_editor_prompt
22
-
23
  # Enable HTTP debugging for detailed request/response logging
24
  HTTPConnection.debuglevel = 1
25
-
26
  app = Flask(__name__)
27
  os.environ["HF_HOME"] = "/data/.huggingface"
28
-
29
  # Logging setup
30
  logging.basicConfig(level=logging.DEBUG)
31
  logger = logging.getLogger("app")
32
  logger.debug("✅ Logging initialized. Starting app setup.")
33
-
34
  # File storage setup
35
  UPLOAD_DIR = '/data/uploads'
36
  try:
@@ -39,20 +35,16 @@ try:
39
  logger.debug(f"Created/verified upload directory: {UPLOAD_DIR}")
40
  except Exception as e:
41
  logger.error(f"Failed to create upload directory {UPLOAD_DIR}: {str(e)}")
42
-
43
  uploaded_files = []
44
  file_lock = threading.Lock()
45
-
46
  # Grok API setup (intentionally hard-coded per user's request)
47
  GROK_API_URL = "https://api.x.ai/v1/chat/completions"
48
  GROK_API_TOKEN = "xai-xHzXVZ2TtRwcqP0UZO8e0fCTtuvg9ZKfHYkdBflYVLxcXHLUbUXQbULs3N5TxcxqzE8E7Yy0cK2umCON"
49
-
50
  # OpenAI key (optional path you already had)
51
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY_VERDICTAI")
52
  if not OPENAI_API_KEY:
53
  logger.error("OPENAI_API_KEY or OPENAI_API_KEY_VERDICTAI not set in environment variables.")
54
  logger.debug("✅ Grok and OpenAI API endpoints and tokens set.")
55
-
56
  # Define STATES dictionary
57
  STATES = {
58
  "KY": "Kentucky",
@@ -63,15 +55,12 @@ STATES = {
63
  "TN": "Tennessee",
64
  # Add more as needed
65
  }
66
-
67
  # Global citation regex (more permissive to catch common formats like S.W.3d, F.3d, etc.)
68
  CITATION_RE = re.compile(
69
  r'([A-Z][A-Za-z&.\- ]+ v\. [A-Z][A-Za-z&.\- ]+, \d+ [A-Z][A-Za-z.]*\d* \d+ \([A-Za-z. ]+ \d{4}\))'
70
  )
71
-
72
  def extract_citations(text: str):
73
  return CITATION_RE.findall(text or "")
74
-
75
  def extract_text_from_file(file_path):
76
  try:
77
  if not os.path.exists(file_path):
@@ -95,7 +84,6 @@ def extract_text_from_file(file_path):
95
  except Exception as e:
96
  logger.error(f"Error extracting text from {file_path}: {str(e)}")
97
  return f"Error: Failed to extract text from {file_path} - {str(e)}"
98
-
99
  def classify_prompt(prompt):
100
  """Baseline classifier when no explicit 'mode' is provided."""
101
  p = prompt.lower()
@@ -132,7 +120,6 @@ def classify_prompt(prompt):
132
  if any(x in p for x in ["summarize", "what does", "order"]):
133
  return "document_analysis"
134
  return "general_qa"
135
-
136
  def ask_grok(messages, stream=False, deep_search=False):
137
  try:
138
  headers = {
@@ -222,7 +209,6 @@ def ask_grok(messages, stream=False, deep_search=False):
222
  yield "data: [DONE]\n\n"
223
  return error_gen()
224
  return "[Grok Error] " + str(e)
225
-
226
  def _set_doc_defaults(doc):
227
  style = doc.styles['Normal']
228
  style.font.name = 'Times New Roman'
@@ -233,7 +219,6 @@ def _set_doc_defaults(doc):
233
  sec.bottom_margin = Inches(1)
234
  sec.left_margin = Inches(1.25)
235
  sec.right_margin = Inches(1)
236
-
237
  def _add_caption_block(doc, court_line, action_no, plaintiff, defendant, motion_title):
238
  p = doc.add_paragraph()
239
  p.alignment = WD_ALIGN_PARAGRAPH.CENTER
@@ -256,7 +241,6 @@ def _add_caption_block(doc, court_line, action_no, plaintiff, defendant, motion_
256
  ]:
257
  run = p.add_run(line + "\n")
258
  run.bold = True if line.isupper() and line not in ("PLAINTIFF,", "DEFENDANT,", "v.") else False
259
-
260
  def create_legal_docx(content, jurisdiction, filename, task_type):
261
  try:
262
  doc = Document()
@@ -302,7 +286,6 @@ def create_legal_docx(content, jurisdiction, filename, task_type):
302
  except Exception as e:
303
  logger.error(f"Error creating DOCX {filename}: {str(e)}")
304
  return f"Error creating document: {str(e)}"
305
-
306
  def web_search(q: str) -> str:
307
  try:
308
  url = f"https://www.google.com/search?q={requests.utils.quote(q)}"
@@ -327,15 +310,12 @@ def web_search(q: str) -> str:
327
  except Exception as e:
328
  logger.error(f"Web search error: {str(e)}")
329
  return ""
330
-
331
  # ---------- Feature Prompt Builders ----------
332
-
333
  def build_prompt_for_mode(mode, user_prompt, jurisdiction, state_name, combined_text):
334
  """Return (system_message, user_message) for a given mode."""
335
  base_context = f"Jurisdiction: {state_name} ({jurisdiction}). Assume Kentucky practice unless otherwise stated. Write in clear, professional legal prose tailored to Kentucky courts."
336
  text_note = f"\n\nAttached/Extracted Documents (truncated):\n{combined_text[:10000]}" if combined_text else ""
337
  mode = (mode or "").lower()
338
-
339
  if mode == "citation_check":
340
  # Use any explicit citations in user prompt + docs
341
  all_text = (user_prompt or "") + "\n" + (combined_text or "")
@@ -344,70 +324,57 @@ def build_prompt_for_mode(mode, user_prompt, jurisdiction, state_name, combined_
344
  sys = base_context + " Verify legal citations and report treatment (followed, distinguished, limited, overruled) with brief parentheticals."
345
  usr = f"Please verify the following citations and list significant subsequent cases and treatments:\n{cit_block}\n{text_note}"
346
  return sys, usr
347
-
348
  if mode == "brief_builder":
349
  sys = base_context + " Build a structured brief with clear headings."
350
  usr = f"Using the facts and issues below, produce a Kentucky-style brief with sections: FACTS, ISSUE(S), RULE(S), APPLICATION/ANALYSIS, CONCLUSION.\n\nUser Prompt:\n{user_prompt}\n{text_note}"
351
  return sys, usr
352
-
353
  if mode == "deposition_qa":
354
  sys = base_context + " Generate practical, targeted deposition questions."
355
  usr = f"Based on the matter below, draft 20 deposition questions for (1) the opposing party, (2) a key fact witness, and (3) an expert (if applicable). Group by witness type and topic.\n\n{user_prompt}\n{text_note}"
356
  return sys, usr
357
-
358
  if mode == "checklist":
359
  sys = base_context + " Create a compliance checklist and red-flag analysis."
360
  usr = f"Produce a Kentucky practice checklist for this document or scenario, and list red flags or missing essentials (caption, certificate of service, venue, signatures, deadlines, rules).\n\n{user_prompt}\n{text_note}"
361
  return sys, usr
362
-
363
  if mode == "comparative_law":
364
  sys = base_context + " Compare Kentucky with Indiana, Ohio, and Federal law succinctly."
365
  usr = f"Answer under Kentucky law first. Then briefly compare with Indiana, Ohio, and Federal law, noting major similarities/differences and key citations.\n\nQuestion:\n{user_prompt}\n{text_note}"
366
  return sys, usr
367
-
368
  if mode == "client_summary":
369
  sys = base_context + " Translate into plain, client-friendly English. Avoid legalese."
370
  usr = f"Summarize in plain English suitable for a client. Provide bullets of key points, obligations, and risks.\n\n{user_prompt}\n{text_note}"
371
  return sys, usr
372
-
373
  if mode == "motion_skeleton":
374
  sys = base_context + " Draft a Kentucky-style motion skeleton with placeholders."
375
  usr = f"Draft a motion skeleton for '{user_prompt}'. Include: Caption placeholder, Introduction, Legal Standard (with rule/case placeholders), Argument headings, Relief Requested, Signature block, Certificate of Service. Use placeholders like [Insert Facts].\n{text_note}"
376
  return sys, usr
377
-
378
  if mode == "opposing_argument":
379
  sys = base_context + " Write the strongest opposing argument under Kentucky law."
380
  usr = f"Given the following draft or position, write the best counter-argument opposing counsel would make. Provide concise headings and authorities.\n\n{user_prompt}\n{text_note}"
381
  return sys, usr
382
-
383
  if mode == "case_extractor":
384
  sys = base_context + " Extract and list all legal citations from the provided text."
385
  usr = f"Extract every legal citation found and list them one per line. Then group by jurisdiction (KY, Federal, Other). If context suggests relevance, add a 1-2 line parenthetical per citation.\n\n{user_prompt}\n{text_note}"
386
  return sys, usr
387
-
388
  if mode == "judge_style":
389
  sys = base_context + " Adapt tone and format to the specified judge/county preferences."
390
  usr = f"Draft the requested text in the style preferred by the specified judge or county as described below.\n\n{user_prompt}\n{text_note}"
391
  return sys, usr
392
-
393
  # Fallback: use existing prompt builder pathways
394
  sys = build_grok_prompt(user_prompt, mode or "general_qa", jurisdiction, "")
395
  usr = f"{user_prompt}\n{text_note}"
396
  return sys, usr
397
-
398
  # ---------- Pipeline Stages ----------
399
-
400
  def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_mode=None):
401
  logger.debug(f"Starting route_model with task_type: {task_type}, jurisdiction: {jurisdiction}, deep_search: {deep_search}, files: {files}, explicit_mode: {explicit_mode}")
402
  try:
403
  jurisdiction = jurisdiction if jurisdiction in STATES else "KY"
404
  state_name = STATES.get(jurisdiction, "Kentucky")
405
- rag_context = "" # reserved for future RAG
406
  needs_deep_search = (
407
  (task_type not in ["document_creation"])
408
  and (deep_search or "research" in messages[-1]['content'].lower())
409
  )
410
-
411
  yield f'data: {{"chunk": "Step 1: Analyzing query..."}}\n\n'
412
  prompt = messages[-1]['content']
413
  prompt_lower = prompt.lower()
@@ -415,16 +382,13 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
415
  if len(messages) > 1 and messages[-2]['role'] == 'assistant':
416
  prior_summary = messages[-2]['content'][:500] + "..."
417
  messages[-1]['content'] += f"\n\nPrior context to retry: {prior_summary}"
418
-
419
  # Gather file text
420
  file_text_combined = "\n".join([extract_text_from_file(p) for p in files if p])
421
  if file_text_combined and not file_text_combined.startswith("Error"):
422
  logger.debug(f"Added document content to prompt: {file_text_combined[:200]}...")
423
  messages[-1]['content'] = f"{messages[-1]['content']}\n\n{rag_context}"
424
-
425
  # Branching by task type / explicit mode
426
  mode = explicit_mode or task_type
427
-
428
  # Document creation (kept as in your pipeline: GPT-4.1-mini draft -> Grok polish -> DOCX/PDF)
429
  if task_type in ["document_creation", "irac", "case_law", "statute", "legal_strategy", "general_qa"] and mode == task_type:
430
  yield f'data: {{"chunk": "Step 2: Generating initial draft with GPT-4.1-mini..."}}\n\n'
@@ -456,7 +420,6 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
456
  logger.error(error_msg)
457
  full_response = gpt_response
458
  yield f'data: {{"chunk": "Grok failed, using GPT-4.1-mini draft..."}}\n\n'
459
-
460
  elif mode in [
461
  "citation_check", "brief_builder", "deposition_qa", "checklist",
462
  "comparative_law", "client_summary", "motion_skeleton",
@@ -472,7 +435,6 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
472
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
473
  yield "data: [DONE]\n\n"
474
  return
475
-
476
  else:
477
  # Fallback: single Grok pass with build_grok_prompt
478
  yield f'data: {{"chunk": "Step 2: Processing with Grok..."}}\n\n'
@@ -485,7 +447,6 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
485
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
486
  yield "data: [DONE]\n\n"
487
  return
488
-
489
  # Step 4: Verify citations lightly (text modes only)
490
  yield f'data: {{"chunk": "Step 4: Verifying citations and facts..."}}\n\n'
491
  citations = extract_citations(full_response)
@@ -510,17 +471,15 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
510
  system_content = build_grok_prompt(prompt, task_type, jurisdiction, rag_context)
511
  re_messages = [{'role': 'system', 'content': system_content}, {'role': 'user', 'content': re_prompt}]
512
  full_response = ask_grok(re_messages, stream=False, deep_search=True)
513
- if full_response.startswith("[Grok Error"]:
514
  error_msg = f"Grok re-polish failed: {full_response}"
515
  logger.error(error_msg)
516
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
517
  yield "data: [DONE]\n\n"
518
  return
519
-
520
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
521
  pdf_filename = f"/tmp/legal_doc_{timestamp}.pdf"
522
  docx_filename = f"/tmp/legal_doc_{timestamp}.docx"
523
-
524
  if task_type == "document_creation" and mode == "document_creation":
525
  yield f'data: {{"chunk": "Step 5: Generating document and PDF preview..."}}\n\n'
526
  main_content = create_legal_docx(full_response, jurisdiction, docx_filename, task_type)
@@ -537,10 +496,9 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
537
  error_msg = f"PDF conversion failed: {str(e)}"
538
  logger.error(error_msg)
539
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
540
- main_content = full_response # still stream text
541
  else:
542
  main_content = full_response
543
-
544
  yield f'data: {{"chunk": "Step 6: Finalizing response..."}}\n\n'
545
  chunks = [main_content[i:i+100] for i in range(0, len(main_content), 100)]
546
  for part in chunks:
@@ -553,9 +511,7 @@ def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_
553
  logger.error(error_msg)
554
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
555
  yield "data: [DONE]\n\n"
556
-
557
  # ---------- Utility Flows (unchanged behavior) ----------
558
-
559
  def summarize_document(files):
560
  def gen():
561
  try:
@@ -584,7 +540,6 @@ def summarize_document(files):
584
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
585
  yield "data: [DONE]\n\n"
586
  return gen()
587
-
588
  def analyze_document(files):
589
  def gen():
590
  try:
@@ -613,7 +568,6 @@ def analyze_document(files):
613
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
614
  yield "data: [DONE]\n\n"
615
  return gen()
616
-
617
  def check_issues(files):
618
  def gen():
619
  try:
@@ -642,14 +596,11 @@ def check_issues(files):
642
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
643
  yield "data: [DONE]\n\n"
644
  return gen()
645
-
646
  # ---------- Flask Routes ----------
647
-
648
  @app.route('/')
649
  def index():
650
  logger.debug("Serving index.html")
651
  return send_from_directory('.', 'index.html')
652
-
653
  @app.route('/api/chat', methods=['POST'])
654
  def api_chat():
655
  logger.info("Received request to /api/chat")
@@ -664,30 +615,25 @@ def api_chat():
664
  message = data.get('message', '')
665
  if not message:
666
  yield f'data: {{"error": "No message provided"}}\n\n'
667
- yield "data: [DONE]\n\n"
668
  return
669
-
670
  # Optional explicit mode from client (e.g., "citation_check", "deposition_qa", etc.)
671
- explicit_mode = data.get('mode') # if provided, we'll use it
672
  jurisdiction = data.get('jurisdiction', 'KY')
673
  irac_mode = data.get('irac', False)
674
  deep_search = data.get('deepSearch', False)
675
  files_filenames = data.get('files', [])
676
-
677
  temp_paths = []
678
  with file_lock:
679
  for f in uploaded_files:
680
  if f['filename'] in files_filenames:
681
  temp_paths.append(f['path'])
682
  yield f'data: {{"uploaded_files": {json.dumps(files_filenames)}}}\n\n'
683
-
684
  prompt_lower = message.lower()
685
  task_type = classify_prompt(message)
686
  if irac_mode:
687
  task_type = "irac"
688
-
689
  logger.info(f"Task type: {task_type}, Explicit mode: {explicit_mode}, Jurisdiction: {jurisdiction}, Deep search: {deep_search}, Files: {files_filenames}")
690
-
691
  # Fast paths for utility flows if keywords + files present
692
  if "summarize" in prompt_lower and temp_paths:
693
  for chunk in summarize_document(temp_paths):
@@ -703,7 +649,6 @@ def api_chat():
703
  messages = [{'role': 'user', 'content': message}]
704
  for line in route_model(messages, task_type, temp_paths, deep_search, jurisdiction, explicit_mode=explicit_mode):
705
  yield line
706
-
707
  logger.info("Response streamed successfully.")
708
  except Exception as e:
709
  error_msg = f"Error in /api/chat: {str(e)}"
@@ -711,7 +656,6 @@ def api_chat():
711
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
712
  yield "data: [DONE]\n\n"
713
  return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no'})
714
-
715
  @app.route('/api/files', methods=['POST'])
716
  def upload_files():
717
  logger.info("Received request to /api/files POST")
@@ -741,7 +685,6 @@ def upload_files():
741
  except Exception as e:
742
  logger.error(f"Error in upload_files: {str(e)}")
743
  return jsonify({'error': str(e)}), 500
744
-
745
  @app.route('/api/files', methods=['GET'])
746
  def list_files():
747
  logger.info("Received request to /api/files GET")
@@ -757,7 +700,6 @@ def list_files():
757
  except Exception as e:
758
  logger.error(f"Error in list_files: {str(e)}")
759
  return jsonify({'error': str(e)}), 500
760
-
761
  @app.route('/api/files/<filename>', methods=['DELETE'])
762
  def delete_file(filename):
763
  logger.info(f"Received request to delete file: {filename}")
@@ -777,7 +719,6 @@ def delete_file(filename):
777
  except Exception as e:
778
  logger.error(f"Error in delete_file: {str(e)}")
779
  return jsonify({'error': str(e)}), 500
780
-
781
  @app.route('/download/<filename>', methods=['GET'])
782
  def download(filename):
783
  logger.info(f"Received request to download: {filename}")
@@ -790,12 +731,10 @@ def download(filename):
790
  except Exception as e:
791
  logger.error(f"Error in download: {str(e)}")
792
  return jsonify({'error': str(e)}), 500
793
-
794
  @app.route('/health', methods=['GET'])
795
  def health():
796
  logger.debug("Health check requested")
797
  return jsonify({'status': 'healthy'}), 200
798
-
799
  if __name__ == '__main__':
800
  # Run the Flask app
801
- app.run(host='0.0.0.0', port=7860, debug=True)
 
19
  from http.client import HTTPConnection
20
  from gpt_helpers import ask_gpt41_mini
21
  from prompt_builder import build_grok_prompt, build_editor_prompt
 
22
  # Enable HTTP debugging for detailed request/response logging
23
  HTTPConnection.debuglevel = 1
 
24
  app = Flask(__name__)
25
  os.environ["HF_HOME"] = "/data/.huggingface"
 
26
  # Logging setup
27
  logging.basicConfig(level=logging.DEBUG)
28
  logger = logging.getLogger("app")
29
  logger.debug("✅ Logging initialized. Starting app setup.")
 
30
  # File storage setup
31
  UPLOAD_DIR = '/data/uploads'
32
  try:
 
35
  logger.debug(f"Created/verified upload directory: {UPLOAD_DIR}")
36
  except Exception as e:
37
  logger.error(f"Failed to create upload directory {UPLOAD_DIR}: {str(e)}")
 
38
  uploaded_files = []
39
  file_lock = threading.Lock()
 
40
  # Grok API setup (intentionally hard-coded per user's request)
41
  GROK_API_URL = "https://api.x.ai/v1/chat/completions"
42
  GROK_API_TOKEN = "xai-xHzXVZ2TtRwcqP0UZO8e0fCTtuvg9ZKfHYkdBflYVLxcXHLUbUXQbULs3N5TxcxqzE8E7Yy0cK2umCON"
 
43
  # OpenAI key (optional path you already had)
44
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY_VERDICTAI")
45
  if not OPENAI_API_KEY:
46
  logger.error("OPENAI_API_KEY or OPENAI_API_KEY_VERDICTAI not set in environment variables.")
47
  logger.debug("✅ Grok and OpenAI API endpoints and tokens set.")
 
48
  # Define STATES dictionary
49
  STATES = {
50
  "KY": "Kentucky",
 
55
  "TN": "Tennessee",
56
  # Add more as needed
57
  }
 
58
  # Global citation regex (more permissive to catch common formats like S.W.3d, F.3d, etc.)
59
  CITATION_RE = re.compile(
60
  r'([A-Z][A-Za-z&.\- ]+ v\. [A-Z][A-Za-z&.\- ]+, \d+ [A-Z][A-Za-z.]*\d* \d+ \([A-Za-z. ]+ \d{4}\))'
61
  )
 
62
  def extract_citations(text: str):
63
  return CITATION_RE.findall(text or "")
 
64
  def extract_text_from_file(file_path):
65
  try:
66
  if not os.path.exists(file_path):
 
84
  except Exception as e:
85
  logger.error(f"Error extracting text from {file_path}: {str(e)}")
86
  return f"Error: Failed to extract text from {file_path} - {str(e)}"
 
87
  def classify_prompt(prompt):
88
  """Baseline classifier when no explicit 'mode' is provided."""
89
  p = prompt.lower()
 
120
  if any(x in p for x in ["summarize", "what does", "order"]):
121
  return "document_analysis"
122
  return "general_qa"
 
123
  def ask_grok(messages, stream=False, deep_search=False):
124
  try:
125
  headers = {
 
209
  yield "data: [DONE]\n\n"
210
  return error_gen()
211
  return "[Grok Error] " + str(e)
 
212
  def _set_doc_defaults(doc):
213
  style = doc.styles['Normal']
214
  style.font.name = 'Times New Roman'
 
219
  sec.bottom_margin = Inches(1)
220
  sec.left_margin = Inches(1.25)
221
  sec.right_margin = Inches(1)
 
222
  def _add_caption_block(doc, court_line, action_no, plaintiff, defendant, motion_title):
223
  p = doc.add_paragraph()
224
  p.alignment = WD_ALIGN_PARAGRAPH.CENTER
 
241
  ]:
242
  run = p.add_run(line + "\n")
243
  run.bold = True if line.isupper() and line not in ("PLAINTIFF,", "DEFENDANT,", "v.") else False
 
244
  def create_legal_docx(content, jurisdiction, filename, task_type):
245
  try:
246
  doc = Document()
 
286
  except Exception as e:
287
  logger.error(f"Error creating DOCX {filename}: {str(e)}")
288
  return f"Error creating document: {str(e)}"
 
289
  def web_search(q: str) -> str:
290
  try:
291
  url = f"https://www.google.com/search?q={requests.utils.quote(q)}"
 
310
  except Exception as e:
311
  logger.error(f"Web search error: {str(e)}")
312
  return ""
 
313
  # ---------- Feature Prompt Builders ----------
 
314
  def build_prompt_for_mode(mode, user_prompt, jurisdiction, state_name, combined_text):
315
  """Return (system_message, user_message) for a given mode."""
316
  base_context = f"Jurisdiction: {state_name} ({jurisdiction}). Assume Kentucky practice unless otherwise stated. Write in clear, professional legal prose tailored to Kentucky courts."
317
  text_note = f"\n\nAttached/Extracted Documents (truncated):\n{combined_text[:10000]}" if combined_text else ""
318
  mode = (mode or "").lower()
 
319
  if mode == "citation_check":
320
  # Use any explicit citations in user prompt + docs
321
  all_text = (user_prompt or "") + "\n" + (combined_text or "")
 
324
  sys = base_context + " Verify legal citations and report treatment (followed, distinguished, limited, overruled) with brief parentheticals."
325
  usr = f"Please verify the following citations and list significant subsequent cases and treatments:\n{cit_block}\n{text_note}"
326
  return sys, usr
 
327
  if mode == "brief_builder":
328
  sys = base_context + " Build a structured brief with clear headings."
329
  usr = f"Using the facts and issues below, produce a Kentucky-style brief with sections: FACTS, ISSUE(S), RULE(S), APPLICATION/ANALYSIS, CONCLUSION.\n\nUser Prompt:\n{user_prompt}\n{text_note}"
330
  return sys, usr
 
331
  if mode == "deposition_qa":
332
  sys = base_context + " Generate practical, targeted deposition questions."
333
  usr = f"Based on the matter below, draft 20 deposition questions for (1) the opposing party, (2) a key fact witness, and (3) an expert (if applicable). Group by witness type and topic.\n\n{user_prompt}\n{text_note}"
334
  return sys, usr
 
335
  if mode == "checklist":
336
  sys = base_context + " Create a compliance checklist and red-flag analysis."
337
  usr = f"Produce a Kentucky practice checklist for this document or scenario, and list red flags or missing essentials (caption, certificate of service, venue, signatures, deadlines, rules).\n\n{user_prompt}\n{text_note}"
338
  return sys, usr
 
339
  if mode == "comparative_law":
340
  sys = base_context + " Compare Kentucky with Indiana, Ohio, and Federal law succinctly."
341
  usr = f"Answer under Kentucky law first. Then briefly compare with Indiana, Ohio, and Federal law, noting major similarities/differences and key citations.\n\nQuestion:\n{user_prompt}\n{text_note}"
342
  return sys, usr
 
343
  if mode == "client_summary":
344
  sys = base_context + " Translate into plain, client-friendly English. Avoid legalese."
345
  usr = f"Summarize in plain English suitable for a client. Provide bullets of key points, obligations, and risks.\n\n{user_prompt}\n{text_note}"
346
  return sys, usr
 
347
  if mode == "motion_skeleton":
348
  sys = base_context + " Draft a Kentucky-style motion skeleton with placeholders."
349
  usr = f"Draft a motion skeleton for '{user_prompt}'. Include: Caption placeholder, Introduction, Legal Standard (with rule/case placeholders), Argument headings, Relief Requested, Signature block, Certificate of Service. Use placeholders like [Insert Facts].\n{text_note}"
350
  return sys, usr
 
351
  if mode == "opposing_argument":
352
  sys = base_context + " Write the strongest opposing argument under Kentucky law."
353
  usr = f"Given the following draft or position, write the best counter-argument opposing counsel would make. Provide concise headings and authorities.\n\n{user_prompt}\n{text_note}"
354
  return sys, usr
 
355
  if mode == "case_extractor":
356
  sys = base_context + " Extract and list all legal citations from the provided text."
357
  usr = f"Extract every legal citation found and list them one per line. Then group by jurisdiction (KY, Federal, Other). If context suggests relevance, add a 1-2 line parenthetical per citation.\n\n{user_prompt}\n{text_note}"
358
  return sys, usr
 
359
  if mode == "judge_style":
360
  sys = base_context + " Adapt tone and format to the specified judge/county preferences."
361
  usr = f"Draft the requested text in the style preferred by the specified judge or county as described below.\n\n{user_prompt}\n{text_note}"
362
  return sys, usr
 
363
  # Fallback: use existing prompt builder pathways
364
  sys = build_grok_prompt(user_prompt, mode or "general_qa", jurisdiction, "")
365
  usr = f"{user_prompt}\n{text_note}"
366
  return sys, usr
 
367
  # ---------- Pipeline Stages ----------
 
368
  def route_model(messages, task_type, files, deep_search, jurisdiction, explicit_mode=None):
369
  logger.debug(f"Starting route_model with task_type: {task_type}, jurisdiction: {jurisdiction}, deep_search: {deep_search}, files: {files}, explicit_mode: {explicit_mode}")
370
  try:
371
  jurisdiction = jurisdiction if jurisdiction in STATES else "KY"
372
  state_name = STATES.get(jurisdiction, "Kentucky")
373
+ rag_context = "" # reserved for future RAG
374
  needs_deep_search = (
375
  (task_type not in ["document_creation"])
376
  and (deep_search or "research" in messages[-1]['content'].lower())
377
  )
 
378
  yield f'data: {{"chunk": "Step 1: Analyzing query..."}}\n\n'
379
  prompt = messages[-1]['content']
380
  prompt_lower = prompt.lower()
 
382
  if len(messages) > 1 and messages[-2]['role'] == 'assistant':
383
  prior_summary = messages[-2]['content'][:500] + "..."
384
  messages[-1]['content'] += f"\n\nPrior context to retry: {prior_summary}"
 
385
  # Gather file text
386
  file_text_combined = "\n".join([extract_text_from_file(p) for p in files if p])
387
  if file_text_combined and not file_text_combined.startswith("Error"):
388
  logger.debug(f"Added document content to prompt: {file_text_combined[:200]}...")
389
  messages[-1]['content'] = f"{messages[-1]['content']}\n\n{rag_context}"
 
390
  # Branching by task type / explicit mode
391
  mode = explicit_mode or task_type
 
392
  # Document creation (kept as in your pipeline: GPT-4.1-mini draft -> Grok polish -> DOCX/PDF)
393
  if task_type in ["document_creation", "irac", "case_law", "statute", "legal_strategy", "general_qa"] and mode == task_type:
394
  yield f'data: {{"chunk": "Step 2: Generating initial draft with GPT-4.1-mini..."}}\n\n'
 
420
  logger.error(error_msg)
421
  full_response = gpt_response
422
  yield f'data: {{"chunk": "Grok failed, using GPT-4.1-mini draft..."}}\n\n'
 
423
  elif mode in [
424
  "citation_check", "brief_builder", "deposition_qa", "checklist",
425
  "comparative_law", "client_summary", "motion_skeleton",
 
435
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
436
  yield "data: [DONE]\n\n"
437
  return
 
438
  else:
439
  # Fallback: single Grok pass with build_grok_prompt
440
  yield f'data: {{"chunk": "Step 2: Processing with Grok..."}}\n\n'
 
447
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
448
  yield "data: [DONE]\n\n"
449
  return
 
450
  # Step 4: Verify citations lightly (text modes only)
451
  yield f'data: {{"chunk": "Step 4: Verifying citations and facts..."}}\n\n'
452
  citations = extract_citations(full_response)
 
471
  system_content = build_grok_prompt(prompt, task_type, jurisdiction, rag_context)
472
  re_messages = [{'role': 'system', 'content': system_content}, {'role': 'user', 'content': re_prompt}]
473
  full_response = ask_grok(re_messages, stream=False, deep_search=True)
474
+ if full_response.startswith("[Grok Error"):
475
  error_msg = f"Grok re-polish failed: {full_response}"
476
  logger.error(error_msg)
477
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
478
  yield "data: [DONE]\n\n"
479
  return
 
480
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
481
  pdf_filename = f"/tmp/legal_doc_{timestamp}.pdf"
482
  docx_filename = f"/tmp/legal_doc_{timestamp}.docx"
 
483
  if task_type == "document_creation" and mode == "document_creation":
484
  yield f'data: {{"chunk": "Step 5: Generating document and PDF preview..."}}\n\n'
485
  main_content = create_legal_docx(full_response, jurisdiction, docx_filename, task_type)
 
496
  error_msg = f"PDF conversion failed: {str(e)}"
497
  logger.error(error_msg)
498
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
499
+ main_content = full_response # still stream text
500
  else:
501
  main_content = full_response
 
502
  yield f'data: {{"chunk": "Step 6: Finalizing response..."}}\n\n'
503
  chunks = [main_content[i:i+100] for i in range(0, len(main_content), 100)]
504
  for part in chunks:
 
511
  logger.error(error_msg)
512
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
513
  yield "data: [DONE]\n\n"
 
514
  # ---------- Utility Flows (unchanged behavior) ----------
 
515
  def summarize_document(files):
516
  def gen():
517
  try:
 
540
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
541
  yield "data: [DONE]\n\n"
542
  return gen()
 
543
  def analyze_document(files):
544
  def gen():
545
  try:
 
568
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
569
  yield "data: [DONE]\n\n"
570
  return gen()
 
571
  def check_issues(files):
572
  def gen():
573
  try:
 
596
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
597
  yield "data: [DONE]\n\n"
598
  return gen()
 
599
  # ---------- Flask Routes ----------
 
600
  @app.route('/')
601
  def index():
602
  logger.debug("Serving index.html")
603
  return send_from_directory('.', 'index.html')
 
604
  @app.route('/api/chat', methods=['POST'])
605
  def api_chat():
606
  logger.info("Received request to /api/chat")
 
615
  message = data.get('message', '')
616
  if not message:
617
  yield f'data: {{"error": "No message provided"}}\n\n'
618
+ yield "data: [DONE]\n\n'
619
  return
 
620
  # Optional explicit mode from client (e.g., "citation_check", "deposition_qa", etc.)
621
+ explicit_mode = data.get('mode') # if provided, we'll use it
622
  jurisdiction = data.get('jurisdiction', 'KY')
623
  irac_mode = data.get('irac', False)
624
  deep_search = data.get('deepSearch', False)
625
  files_filenames = data.get('files', [])
 
626
  temp_paths = []
627
  with file_lock:
628
  for f in uploaded_files:
629
  if f['filename'] in files_filenames:
630
  temp_paths.append(f['path'])
631
  yield f'data: {{"uploaded_files": {json.dumps(files_filenames)}}}\n\n'
 
632
  prompt_lower = message.lower()
633
  task_type = classify_prompt(message)
634
  if irac_mode:
635
  task_type = "irac"
 
636
  logger.info(f"Task type: {task_type}, Explicit mode: {explicit_mode}, Jurisdiction: {jurisdiction}, Deep search: {deep_search}, Files: {files_filenames}")
 
637
  # Fast paths for utility flows if keywords + files present
638
  if "summarize" in prompt_lower and temp_paths:
639
  for chunk in summarize_document(temp_paths):
 
649
  messages = [{'role': 'user', 'content': message}]
650
  for line in route_model(messages, task_type, temp_paths, deep_search, jurisdiction, explicit_mode=explicit_mode):
651
  yield line
 
652
  logger.info("Response streamed successfully.")
653
  except Exception as e:
654
  error_msg = f"Error in /api/chat: {str(e)}"
 
656
  yield f'data: {{"error": {json.dumps(error_msg)}}}\n\n'
657
  yield "data: [DONE]\n\n"
658
  return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no'})
 
659
  @app.route('/api/files', methods=['POST'])
660
  def upload_files():
661
  logger.info("Received request to /api/files POST")
 
685
  except Exception as e:
686
  logger.error(f"Error in upload_files: {str(e)}")
687
  return jsonify({'error': str(e)}), 500
 
688
  @app.route('/api/files', methods=['GET'])
689
  def list_files():
690
  logger.info("Received request to /api/files GET")
 
700
  except Exception as e:
701
  logger.error(f"Error in list_files: {str(e)}")
702
  return jsonify({'error': str(e)}), 500
 
703
  @app.route('/api/files/<filename>', methods=['DELETE'])
704
  def delete_file(filename):
705
  logger.info(f"Received request to delete file: {filename}")
 
719
  except Exception as e:
720
  logger.error(f"Error in delete_file: {str(e)}")
721
  return jsonify({'error': str(e)}), 500
 
722
  @app.route('/download/<filename>', methods=['GET'])
723
  def download(filename):
724
  logger.info(f"Received request to download: {filename}")
 
731
  except Exception as e:
732
  logger.error(f"Error in download: {str(e)}")
733
  return jsonify({'error': str(e)}), 500
 
734
  @app.route('/health', methods=['GET'])
735
  def health():
736
  logger.debug("Health check requested")
737
  return jsonify({'status': 'healthy'}), 200
 
738
  if __name__ == '__main__':
739
  # Run the Flask app
740
+ app.run(host='0.0.0.0', port=7860, debug=True)