local-deep-research 0.5.0__py3-none-any.whl → 0.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,4 @@
1
1
  import hashlib
2
- import json
3
2
  import threading
4
3
  from datetime import datetime
5
4
  from pathlib import Path
@@ -15,9 +14,10 @@ from ...utilities.log_utils import log_for_research
15
14
  from ...utilities.search_utilities import extract_links_from_search_results
16
15
  from ...utilities.db_utils import get_db_session
17
16
  from ...utilities.threading_utils import thread_context, thread_with_app_context
18
- from ..database.models import ResearchStrategy
19
- from ..models.database import calculate_duration, get_db_connection
17
+ from ..database.models import ResearchStrategy, ResearchHistory
18
+ from ..models.database import calculate_duration
20
19
  from .socket_service import SocketIOService
20
+ from ...error_handling.report_generator import ErrorReportGenerator
21
21
 
22
22
  # Output directory for research results
23
23
  _PROJECT_ROOT = Path(__file__).parents[4]
@@ -336,36 +336,19 @@ def run_research_process(
336
336
  active_research[research_id]["progress"] = adjusted_progress
337
337
 
338
338
  # Update progress in the research_history table (for backward compatibility)
339
- conn = get_db_connection()
340
- cursor = conn.cursor()
339
+ db_session = get_db_session()
341
340
 
342
341
  # Update the progress and log separately to avoid race conditions
343
- if adjusted_progress is not None:
344
- cursor.execute(
345
- "UPDATE research_history SET progress = ? WHERE id = ?",
346
- (adjusted_progress, research_id),
347
- )
348
-
349
- # Add the log entry to the progress_log
350
- cursor.execute(
351
- "SELECT progress_log FROM research_history WHERE id = ?",
352
- (research_id,),
353
- )
354
- log_result = cursor.fetchone()
355
-
356
- if log_result:
357
- try:
358
- current_log = json.loads(log_result[0])
359
- except Exception:
360
- current_log = []
361
-
362
- cursor.execute(
363
- "UPDATE research_history SET progress_log = ? WHERE id = ?",
364
- (json.dumps(current_log), research_id),
365
- )
366
-
367
- conn.commit()
368
- conn.close()
342
+ with db_session:
343
+ if adjusted_progress is not None:
344
+ research = (
345
+ db_session.query(ResearchHistory)
346
+ .filter(ResearchHistory.id == research_id)
347
+ .first()
348
+ )
349
+ if research:
350
+ research.progress = adjusted_progress
351
+ db_session.commit()
369
352
 
370
353
  # Emit a socket event
371
354
  try:
@@ -675,8 +658,40 @@ def run_research_process(
675
658
  )
676
659
 
677
660
  try:
678
- # Get the synthesized content from the LLM directly
679
- clean_markdown = raw_formatted_findings
661
+ # Check if we have an error in the findings and use enhanced error handling
662
+ if isinstance(
663
+ raw_formatted_findings, str
664
+ ) and raw_formatted_findings.startswith("Error:"):
665
+ logger.info(
666
+ "Generating enhanced error report using ErrorReportGenerator"
667
+ )
668
+
669
+ # Get LLM for error explanation if available
670
+ try:
671
+ llm = get_llm(research_id=research_id)
672
+ except Exception:
673
+ llm = None
674
+ logger.warning(
675
+ "Could not get LLM for error explanation"
676
+ )
677
+
678
+ # Generate comprehensive error report
679
+ error_generator = ErrorReportGenerator(llm)
680
+ clean_markdown = error_generator.generate_error_report(
681
+ error_message=raw_formatted_findings,
682
+ query=query,
683
+ partial_results=results,
684
+ search_iterations=results.get("iterations", 0),
685
+ research_id=research_id,
686
+ )
687
+
688
+ logger.info(
689
+ "Generated enhanced error report with %d characters",
690
+ len(clean_markdown),
691
+ )
692
+ else:
693
+ # Get the synthesized content from the LLM directly
694
+ clean_markdown = raw_formatted_findings
680
695
 
681
696
  # Extract all sources from findings to add them to the summary
682
697
  all_links = []
@@ -743,32 +758,28 @@ def run_research_process(
743
758
  logger.info(
744
759
  "Updating database for research_id: %s", research_id
745
760
  )
746
- # Get the start time from the database
747
- conn = get_db_connection()
748
- cursor = conn.cursor()
749
- cursor.execute(
750
- "SELECT created_at FROM research_history WHERE id = ?",
751
- (research_id,),
752
- )
753
- result = cursor.fetchone()
754
-
755
- # Use the helper function for consistent duration calculation
756
- duration_seconds = calculate_duration(result[0])
757
-
758
- # Update the record
759
- cursor.execute(
760
- "UPDATE research_history SET status = ?, completed_at = ?, duration_seconds = ?, report_path = ?, metadata = ? WHERE id = ?",
761
- (
762
- "completed",
763
- completed_at,
764
- duration_seconds,
765
- str(report_path),
766
- json.dumps(metadata),
767
- research_id,
768
- ),
769
- )
770
- conn.commit()
771
- conn.close()
761
+
762
+ db_session = get_db_session()
763
+ with db_session:
764
+ research = (
765
+ db_session.query(ResearchHistory)
766
+ .filter_by(id=research_id)
767
+ .first()
768
+ )
769
+
770
+ # Use the helper function for consistent duration calculation
771
+ duration_seconds = calculate_duration(
772
+ research.created_at, research.completed_at
773
+ )
774
+
775
+ research.status = "completed"
776
+ research.completed_at = completed_at
777
+ research.duration_seconds = duration_seconds
778
+ research.report_path = str(report_path)
779
+ research.research_meta = metadata
780
+
781
+ db_session.commit()
782
+
772
783
  logger.info(
773
784
  f"Database updated successfully for research_id: {research_id}"
774
785
  )
@@ -837,31 +848,26 @@ def run_research_process(
837
848
  now = datetime.utcnow()
838
849
  completed_at = now.isoformat()
839
850
 
840
- # Get the start time from the database
841
- conn = get_db_connection()
842
- cursor = conn.cursor()
843
- cursor.execute(
844
- "SELECT created_at FROM research_history WHERE id = ?",
845
- (research_id,),
846
- )
847
- result = cursor.fetchone()
848
-
849
- # Use the helper function for consistent duration calculation
850
- duration_seconds = calculate_duration(result[0])
851
-
852
- cursor.execute(
853
- "UPDATE research_history SET status = ?, completed_at = ?, duration_seconds = ?, report_path = ?, metadata = ? WHERE id = ?",
854
- (
855
- "completed",
856
- completed_at,
857
- duration_seconds,
858
- str(report_path),
859
- json.dumps(metadata),
860
- research_id,
861
- ),
862
- )
863
- conn.commit()
864
- conn.close()
851
+ db_session = get_db_session()
852
+ with db_session:
853
+ research = (
854
+ db_session.query(ResearchHistory)
855
+ .filter_by(id=research_id)
856
+ .first()
857
+ )
858
+
859
+ # Use the helper function for consistent duration calculation
860
+ duration_seconds = calculate_duration(
861
+ research.created_at, research.completed_at
862
+ )
863
+
864
+ research.status = "completed"
865
+ research.completed_at = completed_at
866
+ research.duration_seconds = duration_seconds
867
+ research.report_path = str(report_path)
868
+ research.research_meta = metadata
869
+
870
+ db_session.commit()
865
871
 
866
872
  progress_callback(
867
873
  "Research completed successfully",
@@ -905,18 +911,82 @@ def run_research_process(
905
911
  "solution": "Check API configuration and credentials."
906
912
  }
907
913
 
914
+ # Generate enhanced error report for failed research
915
+ enhanced_report_content = None
916
+ try:
917
+ # Get LLM for error explanation if available
918
+ try:
919
+ llm = get_llm(research_id=research_id)
920
+ except Exception:
921
+ llm = None
922
+ logger.warning(
923
+ "Could not get LLM for error explanation in failure handler"
924
+ )
925
+
926
+ # Get partial results if they exist
927
+ partial_results = results if "results" in locals() else None
928
+ search_iterations = (
929
+ results.get("iterations", 0) if partial_results else 0
930
+ )
931
+
932
+ # Generate comprehensive error report
933
+ error_generator = ErrorReportGenerator(llm)
934
+ enhanced_report_content = error_generator.generate_error_report(
935
+ error_message=f"Research failed: {str(e)}",
936
+ query=query,
937
+ partial_results=partial_results,
938
+ search_iterations=search_iterations,
939
+ research_id=research_id,
940
+ )
941
+
942
+ logger.info(
943
+ "Generated enhanced error report for failed research (length: %d)",
944
+ len(enhanced_report_content),
945
+ )
946
+
947
+ # Save enhanced error report as the actual report file
948
+ try:
949
+ reports_folder = OUTPUT_DIR
950
+ report_filename = f"research_{research_id}_error_report.md"
951
+ report_path = reports_folder / report_filename
952
+
953
+ with open(report_path, "w", encoding="utf-8") as f:
954
+ f.write(enhanced_report_content)
955
+
956
+ logger.info(
957
+ "Saved enhanced error report to: %s", report_path
958
+ )
959
+
960
+ # Store the report path so it can be retrieved later
961
+ report_path_to_save = str(
962
+ report_path.relative_to(reports_folder.parent)
963
+ )
964
+
965
+ except Exception as report_error:
966
+ logger.exception(
967
+ "Failed to save enhanced error report: %s", report_error
968
+ )
969
+ report_path_to_save = None
970
+
971
+ except Exception as error_gen_error:
972
+ logger.exception(
973
+ "Failed to generate enhanced error report: %s",
974
+ error_gen_error,
975
+ )
976
+ enhanced_report_content = None
977
+ report_path_to_save = None
978
+
908
979
  # Update metadata with more context about the error
909
980
  metadata = {"phase": "error", "error": user_friendly_error}
910
981
  if error_context:
911
982
  metadata.update(error_context)
983
+ if enhanced_report_content:
984
+ metadata["has_enhanced_report"] = True
912
985
 
913
986
  # If we still have an active research record, update its log
914
987
  if research_id in active_research:
915
988
  progress_callback(user_friendly_error, None, metadata)
916
989
 
917
- conn = get_db_connection()
918
- cursor = conn.cursor()
919
-
920
990
  # If termination was requested, mark as suspended instead of failed
921
991
  status = (
922
992
  "suspended"
@@ -938,28 +1008,37 @@ def run_research_process(
938
1008
 
939
1009
  # Get the start time from the database
940
1010
  duration_seconds = None
941
- cursor.execute(
942
- "SELECT created_at FROM research_history WHERE id = ?",
943
- (research_id,),
944
- )
945
- result = cursor.fetchone()
946
-
947
- # Use the helper function for consistent duration calculation
948
- if result and result[0]:
949
- duration_seconds = calculate_duration(result[0])
950
-
951
- cursor.execute(
952
- "UPDATE research_history SET status = ?, completed_at = ?, duration_seconds = ?, metadata = ? WHERE id = ?",
953
- (
954
- status,
955
- completed_at,
956
- duration_seconds,
957
- json.dumps(metadata),
958
- research_id,
959
- ),
960
- )
961
- conn.commit()
962
- conn.close()
1011
+ db_session = get_db_session()
1012
+ with db_session:
1013
+ research = (
1014
+ db_session.query(ResearchHistory)
1015
+ .filter_by(id=research_id)
1016
+ .first()
1017
+ )
1018
+ assert research is not None, "Research not in database"
1019
+
1020
+ duration_seconds = calculate_duration(research.created_at)
1021
+
1022
+ db_session = get_db_session()
1023
+ with db_session:
1024
+ research = (
1025
+ db_session.query(ResearchHistory)
1026
+ .filter_by(id=research_id)
1027
+ .first()
1028
+ )
1029
+ assert research is not None, "Research not in database"
1030
+
1031
+ # Update the ResearchHistory object with the new status and completion time
1032
+ research.status = status
1033
+ research.completed_at = completed_at
1034
+ research.duration_seconds = duration_seconds
1035
+ research.metadata = metadata
1036
+
1037
+ # Add error report path if available
1038
+ if "report_path_to_save" in locals() and report_path_to_save:
1039
+ research.report_path = report_path_to_save
1040
+
1041
+ db_session.commit()
963
1042
 
964
1043
  try:
965
1044
  SocketIOService().emit_to_subscribers(
@@ -993,15 +1072,17 @@ def cleanup_research_resources(research_id, active_research, termination_flags):
993
1072
  # Get the current status from the database to determine the final status message
994
1073
  current_status = "completed" # Default
995
1074
  try:
996
- conn = get_db_connection()
997
- cursor = conn.cursor()
998
- cursor.execute(
999
- "SELECT status FROM research_history WHERE id = ?", (research_id,)
1000
- )
1001
- result = cursor.fetchone()
1002
- if result and result[0]:
1003
- current_status = result[0]
1004
- conn.close()
1075
+ db_session = get_db_session()
1076
+ with db_session:
1077
+ research = (
1078
+ db_session.query(ResearchHistory)
1079
+ .filter(ResearchHistory.id == research_id)
1080
+ .first()
1081
+ )
1082
+ if research:
1083
+ current_status = research.status
1084
+ else:
1085
+ logger.error("Research with ID %s not found", research_id)
1005
1086
  except Exception:
1006
1087
  logger.exception("Error retrieving research status during cleanup")
1007
1088
 
@@ -1063,33 +1144,23 @@ def handle_termination(research_id, active_research, termination_flags):
1063
1144
  active_research: Dictionary of active research processes
1064
1145
  termination_flags: Dictionary of termination flags
1065
1146
  """
1066
- # Explicitly set the status to suspended in the database
1067
- conn = get_db_connection()
1068
- cursor = conn.cursor()
1069
-
1070
- # Calculate duration up to termination point - using UTC consistently
1071
1147
  now = datetime.utcnow()
1072
1148
  completed_at = now.isoformat()
1073
1149
 
1074
- # Get the start time from the database
1075
- cursor.execute(
1076
- "SELECT created_at FROM research_history WHERE id = ?",
1077
- (research_id,),
1078
- )
1079
- result = cursor.fetchone()
1150
+ # Fetch the start time from the database using the ORM
1151
+ session = get_db_session()
1152
+ research = session.query(ResearchHistory).filter_by(id=research_id).first()
1080
1153
 
1081
- # Calculate the duration
1082
- duration_seconds = (
1083
- calculate_duration(result[0]) if result and result[0] else None
1084
- )
1154
+ if research:
1155
+ duration_seconds = calculate_duration(research.created_at)
1085
1156
 
1086
- # Update the database with suspended status
1087
- cursor.execute(
1088
- "UPDATE research_history SET status = ?, completed_at = ?, duration_seconds = ? WHERE id = ?",
1089
- ("suspended", completed_at, duration_seconds, research_id),
1090
- )
1091
- conn.commit()
1092
- conn.close()
1157
+ # Update the database with suspended status using the ORM
1158
+ research.status = "suspended"
1159
+ research.completed_at = completed_at
1160
+ research.duration_seconds = duration_seconds
1161
+ session.commit()
1162
+ else:
1163
+ logger.error(f"Research with ID {research_id} not found.")
1093
1164
 
1094
1165
  # Clean up resources
1095
1166
  cleanup_research_resources(research_id, active_research, termination_flags)
@@ -1097,7 +1168,7 @@ def handle_termination(research_id, active_research, termination_flags):
1097
1168
 
1098
1169
  def cancel_research(research_id):
1099
1170
  """
1100
- Cancel/terminate a research process
1171
+ Cancel/terminate a research process using ORM.
1101
1172
 
1102
1173
  Args:
1103
1174
  research_id: The ID of the research to cancel
@@ -1122,27 +1193,14 @@ def cancel_research(research_id):
1122
1193
  return True
1123
1194
  else:
1124
1195
  # Update database directly if not found in active_research
1125
- from ..models.database import get_db_connection
1126
-
1127
- conn = get_db_connection()
1128
- cursor = conn.cursor()
1129
-
1130
- # First check if the research exists
1131
- cursor.execute(
1132
- "SELECT status FROM research_history WHERE id = ?", (research_id,)
1196
+ session = get_db_session()
1197
+ research = (
1198
+ session.query(ResearchHistory).filter_by(id=research_id).first()
1133
1199
  )
1134
- result = cursor.fetchone()
1135
-
1136
- if not result:
1137
- conn.close()
1200
+ if not research:
1138
1201
  return False
1139
1202
 
1140
1203
  # If it exists but isn't in active_research, still update status
1141
- cursor.execute(
1142
- "UPDATE research_history SET status = ? WHERE id = ?",
1143
- ("suspended", research_id),
1144
- )
1145
- conn.commit()
1146
- conn.close()
1147
-
1204
+ research.status = "suspended"
1205
+ session.commit()
1148
1206
  return True
@@ -630,18 +630,20 @@
630
630
  cancelButton.style.display = 'none';
631
631
  }
632
632
  } else if (data.status === 'failed' || data.status === 'cancelled') {
633
- // Show error message
634
- if (window.ui) {
635
- window.ui.showError(data.error || 'Research was unsuccessful');
633
+ // For failed research, try to show the error report if available
634
+ if (data.status === 'failed') {
635
+ if (viewResultsButton) {
636
+ viewResultsButton.textContent = 'View Error Report';
637
+ viewResultsButton.href = `/research/results/${currentResearchId}`;
638
+ viewResultsButton.style.display = 'inline-block';
639
+ }
636
640
  } else {
637
- console.error('Research failed:', data.error || 'Unknown error');
638
- }
639
-
640
- // Update button to go back to home
641
- if (viewResultsButton) {
642
- viewResultsButton.textContent = 'Start New Research';
643
- viewResultsButton.href = '/';
644
- viewResultsButton.style.display = 'inline-block';
641
+ // For cancelled research, go back to home
642
+ if (viewResultsButton) {
643
+ viewResultsButton.textContent = 'Start New Research';
644
+ viewResultsButton.href = '/';
645
+ viewResultsButton.style.display = 'inline-block';
646
+ }
645
647
  }
646
648
 
647
649
  // Hide cancel button
@@ -917,8 +919,12 @@
917
919
  cancelButton.style.display = 'none';
918
920
  }
919
921
 
920
- // Show results button (might have partial results)
921
- showResultsButton();
922
+ // Show error report button
923
+ if (viewResultsButton) {
924
+ viewResultsButton.textContent = 'View Error Report';
925
+ viewResultsButton.href = `/research/results/${currentResearchId}`;
926
+ viewResultsButton.style.display = 'inline-block';
927
+ }
922
928
 
923
929
  // Show notification if enabled
924
930
  showNotification('Research Error', `There was an error with your research: ${data.error}`);
@@ -98,7 +98,7 @@
98
98
  resultsContainer.innerHTML = '<div class="text-center my-5"><i class="fas fa-spinner fa-pulse"></i><p class="mt-3">Loading research results...</p></div>';
99
99
 
100
100
  // Fetch result from API
101
- const response = await fetch(`/research/api/report/${researchId}`);
101
+ const response = await fetch(`/research/api/history/report/${researchId}`);
102
102
 
103
103
  if (!response.ok) {
104
104
  throw new Error(`HTTP error ${response.status}`);
@@ -48,7 +48,7 @@
48
48
  <label for="mode-quick" class="mode-option active" data-mode="quick" role="radio" aria-checked="true" tabindex="0">
49
49
  <div class="mode-icon"><i class="fas fa-bolt" aria-hidden="true"></i></div>
50
50
  <div class="mode-info">
51
- <h3>Quick Summary</h3>
51
+ <h2>Quick Summary</h2>
52
52
  <p>Generated in a few minutes</p>
53
53
  </div>
54
54
  </label>
@@ -57,7 +57,7 @@
57
57
  <label for="mode-detailed" class="mode-option" data-mode="detailed" role="radio" aria-checked="false" tabindex="-1">
58
58
  <div class="mode-icon"><i class="fas fa-microscope" aria-hidden="true"></i></div>
59
59
  <div class="mode-info">
60
- <h3>Detailed Report</h3>
60
+ <h2>Detailed Report</h2>
61
61
  <p>In-depth analysis (takes longer)</p>
62
62
  </div>
63
63
  </label>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: local-deep-research
3
- Version: 0.5.0
3
+ Version: 0.5.3
4
4
  Summary: AI-powered research assistant with deep, iterative analysis using LLMs and web searches
5
5
  Author-Email: LearningCircuit <185559241+LearningCircuit@users.noreply.github.com>, HashedViking <6432677+HashedViking@users.noreply.github.com>
6
6
  License: MIT License