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.
- local_deep_research/__version__.py +1 -1
- local_deep_research/config/llm_config.py +61 -1
- local_deep_research/error_handling/__init__.py +13 -0
- local_deep_research/error_handling/error_reporter.py +236 -0
- local_deep_research/error_handling/report_generator.py +403 -0
- local_deep_research/web/database/migrations.py +16 -1
- local_deep_research/web/database/models.py +31 -31
- local_deep_research/web/models/database.py +13 -23
- local_deep_research/web/routes/history_routes.py +1 -1
- local_deep_research/web/routes/research_routes.py +65 -113
- local_deep_research/web/services/research_service.py +218 -160
- local_deep_research/web/static/js/components/progress.js +19 -13
- local_deep_research/web/static/js/components/results.js +1 -1
- local_deep_research/web/templates/pages/research.html +2 -2
- {local_deep_research-0.5.0.dist-info → local_deep_research-0.5.3.dist-info}/METADATA +1 -1
- {local_deep_research-0.5.0.dist-info → local_deep_research-0.5.3.dist-info}/RECORD +19 -17
- local_deep_research/test_migration.py +0 -188
- {local_deep_research-0.5.0.dist-info → local_deep_research-0.5.3.dist-info}/WHEEL +0 -0
- {local_deep_research-0.5.0.dist-info → local_deep_research-0.5.3.dist-info}/entry_points.txt +0 -0
- {local_deep_research-0.5.0.dist-info → local_deep_research-0.5.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
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
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
#
|
679
|
-
|
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
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
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
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
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
|
-
|
942
|
-
|
943
|
-
(
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
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
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
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
|
-
#
|
1075
|
-
|
1076
|
-
|
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
|
-
|
1082
|
-
|
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
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
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
|
-
|
1126
|
-
|
1127
|
-
|
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
|
-
|
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
|
-
|
1142
|
-
|
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
|
-
//
|
634
|
-
if (
|
635
|
-
|
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
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
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
|
921
|
-
|
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
|
-
<
|
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
|
-
<
|
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.
|
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
|