local-deep-research 0.1.0__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/__init__.py +24 -0
- local_deep_research/citation_handler.py +113 -0
- local_deep_research/config.py +166 -0
- local_deep_research/defaults/__init__.py +44 -0
- local_deep_research/defaults/llm_config.py +269 -0
- local_deep_research/defaults/local_collections.toml +47 -0
- local_deep_research/defaults/main.toml +57 -0
- local_deep_research/defaults/search_engines.toml +244 -0
- local_deep_research/local_collections.py +141 -0
- local_deep_research/main.py +113 -0
- local_deep_research/report_generator.py +206 -0
- local_deep_research/search_system.py +241 -0
- local_deep_research/utilties/__init__.py +0 -0
- local_deep_research/utilties/enums.py +9 -0
- local_deep_research/utilties/llm_utils.py +116 -0
- local_deep_research/utilties/search_utilities.py +115 -0
- local_deep_research/utilties/setup_utils.py +6 -0
- local_deep_research/web/__init__.py +2 -0
- local_deep_research/web/app.py +1209 -0
- local_deep_research/web/static/css/styles.css +1008 -0
- local_deep_research/web/static/js/app.js +2078 -0
- local_deep_research/web/templates/api_keys_config.html +82 -0
- local_deep_research/web/templates/collections_config.html +90 -0
- local_deep_research/web/templates/index.html +312 -0
- local_deep_research/web/templates/llm_config.html +120 -0
- local_deep_research/web/templates/main_config.html +89 -0
- local_deep_research/web/templates/search_engines_config.html +154 -0
- local_deep_research/web/templates/settings.html +519 -0
- local_deep_research/web/templates/settings_dashboard.html +207 -0
- local_deep_research/web_search_engines/__init__.py +0 -0
- local_deep_research/web_search_engines/engines/__init__.py +0 -0
- local_deep_research/web_search_engines/engines/full_search.py +128 -0
- local_deep_research/web_search_engines/engines/meta_search_engine.py +274 -0
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +367 -0
- local_deep_research/web_search_engines/engines/search_engine_brave.py +245 -0
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +123 -0
- local_deep_research/web_search_engines/engines/search_engine_github.py +663 -0
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +283 -0
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +337 -0
- local_deep_research/web_search_engines/engines/search_engine_local.py +901 -0
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +153 -0
- local_deep_research/web_search_engines/engines/search_engine_medrxiv.py +623 -0
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +992 -0
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +230 -0
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +474 -0
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +242 -0
- local_deep_research/web_search_engines/full_search.py +254 -0
- local_deep_research/web_search_engines/search_engine_base.py +197 -0
- local_deep_research/web_search_engines/search_engine_factory.py +233 -0
- local_deep_research/web_search_engines/search_engines_config.py +54 -0
- local_deep_research-0.1.0.dist-info/LICENSE +21 -0
- local_deep_research-0.1.0.dist-info/METADATA +328 -0
- local_deep_research-0.1.0.dist-info/RECORD +56 -0
- local_deep_research-0.1.0.dist-info/WHEEL +5 -0
- local_deep_research-0.1.0.dist-info/entry_points.txt +3 -0
- local_deep_research-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
from typing import Dict, List, Optional
|
2
|
+
from .config import get_llm
|
3
|
+
import re
|
4
|
+
from datetime import datetime
|
5
|
+
from .search_system import AdvancedSearchSystem
|
6
|
+
from local_deep_research import config
|
7
|
+
from . import utilties
|
8
|
+
from .utilties import search_utilities
|
9
|
+
|
10
|
+
class IntegratedReportGenerator:
|
11
|
+
def __init__(self, searches_per_section: int = 2):
|
12
|
+
self.model = get_llm()
|
13
|
+
self.search_system = AdvancedSearchSystem()
|
14
|
+
self.searches_per_section = (
|
15
|
+
searches_per_section # Control search depth per section
|
16
|
+
)
|
17
|
+
|
18
|
+
def _remove_think_tags(self, text: str) -> str:
|
19
|
+
print(text)
|
20
|
+
return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
|
21
|
+
|
22
|
+
def generate_report(self, initial_findings: Dict, query: str) -> Dict:
|
23
|
+
"""Generate a complete research report with section-specific research."""
|
24
|
+
|
25
|
+
# Step 1: Determine structure
|
26
|
+
structure = self._determine_report_structure(initial_findings, query)
|
27
|
+
|
28
|
+
# Step 2: Research and generate content for each section in one step
|
29
|
+
sections = self._research_and_generate_sections(initial_findings, structure, query)
|
30
|
+
|
31
|
+
# Step 3: Format final report
|
32
|
+
report = self._format_final_report(sections, structure, query)
|
33
|
+
|
34
|
+
return report
|
35
|
+
|
36
|
+
def _determine_report_structure(
|
37
|
+
self, findings: Dict, query: str
|
38
|
+
) -> List[Dict]:
|
39
|
+
|
40
|
+
"""Analyze content and determine optimal report structure."""
|
41
|
+
combined_content = findings["current_knowledge"]
|
42
|
+
prompt = f"""
|
43
|
+
Analyze this research content about: {query}
|
44
|
+
|
45
|
+
Content Summary:
|
46
|
+
{combined_content[:1000]}... [truncated]
|
47
|
+
|
48
|
+
Determine the most appropriate report structure by:
|
49
|
+
1. Analyzing the type of content (technical, business, academic, etc.)
|
50
|
+
2. Identifying main themes and logical groupings
|
51
|
+
3. Considering the depth and breadth of the research
|
52
|
+
|
53
|
+
Return a table of contents structure in this exact format:
|
54
|
+
STRUCTURE
|
55
|
+
1. [Section Name]
|
56
|
+
- [Subsection] | [purpose]
|
57
|
+
2. [Section Name]
|
58
|
+
- [Subsection] | [purpose]
|
59
|
+
...
|
60
|
+
END_STRUCTURE
|
61
|
+
|
62
|
+
Make the structure specific to the content, not generic.
|
63
|
+
Each subsection must include its purpose after the | symbol.
|
64
|
+
"""
|
65
|
+
|
66
|
+
response = self._remove_think_tags(self.model.invoke(prompt).content)
|
67
|
+
|
68
|
+
# Parse the structure
|
69
|
+
structure = []
|
70
|
+
current_section = None
|
71
|
+
|
72
|
+
for line in response.split("\n"):
|
73
|
+
if line.strip() in ["STRUCTURE", "END_STRUCTURE"]:
|
74
|
+
continue
|
75
|
+
|
76
|
+
if line.strip().startswith(tuple("123456789")):
|
77
|
+
# Main section
|
78
|
+
section_name = line.split(".")[1].strip()
|
79
|
+
current_section = {"name": section_name, "subsections": []}
|
80
|
+
structure.append(current_section)
|
81
|
+
elif line.strip().startswith("-") and current_section:
|
82
|
+
# Subsection with purpose
|
83
|
+
parts = line.strip("- ").split("|")
|
84
|
+
if len(parts) == 2:
|
85
|
+
current_section["subsections"].append(
|
86
|
+
{"name": parts[0].strip(), "purpose": parts[1].strip()}
|
87
|
+
)
|
88
|
+
|
89
|
+
return structure
|
90
|
+
|
91
|
+
def _research_and_generate_sections(
|
92
|
+
self,
|
93
|
+
initial_findings: Dict,
|
94
|
+
structure: List[Dict],
|
95
|
+
query: str,
|
96
|
+
) -> Dict[str, str]:
|
97
|
+
"""Research and generate content for each section in one step."""
|
98
|
+
sections = {}
|
99
|
+
|
100
|
+
for section in structure:
|
101
|
+
print(f"Processing section: {section['name']}")
|
102
|
+
section_content = []
|
103
|
+
section_content.append(f"# {section['name']}\n")
|
104
|
+
|
105
|
+
# Process each subsection by directly researching it
|
106
|
+
for subsection in section["subsections"]:
|
107
|
+
# Add subsection header
|
108
|
+
section_content.append(f"## {subsection['name']}\n")
|
109
|
+
section_content.append(f"_{subsection['purpose']}_\n\n")
|
110
|
+
|
111
|
+
# Generate a specific search query for this subsection
|
112
|
+
subsection_query = f"{query} {section['name']} {subsection['name']} {subsection['purpose']}"
|
113
|
+
|
114
|
+
print(f"Researching subsection: {subsection['name']} with query: {subsection_query}")
|
115
|
+
|
116
|
+
# Configure search system for focused search
|
117
|
+
original_max_iterations = self.search_system.max_iterations
|
118
|
+
self.search_system.max_iterations = 1 # Keep search focused
|
119
|
+
|
120
|
+
# Perform search for this subsection
|
121
|
+
subsection_results = self.search_system.analyze_topic(subsection_query)
|
122
|
+
|
123
|
+
# Restore original iterations setting
|
124
|
+
self.search_system.max_iterations = original_max_iterations
|
125
|
+
|
126
|
+
# Add the researched content for this subsection
|
127
|
+
if "current_knowledge" in subsection_results and subsection_results["current_knowledge"]:
|
128
|
+
section_content.append(subsection_results["current_knowledge"])
|
129
|
+
else:
|
130
|
+
section_content.append("*Limited information was found for this subsection.*\n")
|
131
|
+
|
132
|
+
section_content.append("\n\n")
|
133
|
+
|
134
|
+
# Combine all content for this section
|
135
|
+
sections[section["name"]] = "\n".join(section_content)
|
136
|
+
|
137
|
+
return sections
|
138
|
+
|
139
|
+
def _generate_sections(
|
140
|
+
self,
|
141
|
+
initial_findings: Dict,
|
142
|
+
section_research: Dict[str, List[Dict]],
|
143
|
+
structure: List[Dict],
|
144
|
+
query: str,
|
145
|
+
) -> Dict[str, str]:
|
146
|
+
"""
|
147
|
+
This method is kept for compatibility but no longer used.
|
148
|
+
The functionality has been moved to _research_and_generate_sections.
|
149
|
+
"""
|
150
|
+
return {}
|
151
|
+
|
152
|
+
def _format_final_report(
|
153
|
+
self,
|
154
|
+
sections: Dict[str, str],
|
155
|
+
structure: List[Dict],
|
156
|
+
query: str,
|
157
|
+
) -> Dict:
|
158
|
+
"""Format the final report with table of contents and sections."""
|
159
|
+
# Generate TOC
|
160
|
+
toc = ["# Table of Contents\n"]
|
161
|
+
for i, section in enumerate(structure, 1):
|
162
|
+
toc.append(f"{i}. **{section['name']}**")
|
163
|
+
for j, subsection in enumerate(section["subsections"], 1):
|
164
|
+
toc.append(f" {i}.{j} {subsection['name']} | _{subsection['purpose']}_")
|
165
|
+
|
166
|
+
# Combine TOC and sections
|
167
|
+
report_parts = ["\n".join(toc), ""]
|
168
|
+
|
169
|
+
# Add a summary of the research
|
170
|
+
report_parts.append("# Research Summary")
|
171
|
+
report_parts.append(f"This report was researched using an advanced search system.")
|
172
|
+
report_parts.append(f"Research included targeted searches for each section and subsection.")
|
173
|
+
report_parts.append("\n---\n")
|
174
|
+
|
175
|
+
# Add each section's content
|
176
|
+
for section in structure:
|
177
|
+
if section["name"] in sections:
|
178
|
+
report_parts.append(sections[section["name"]])
|
179
|
+
report_parts.append("")
|
180
|
+
|
181
|
+
# Format links from search system
|
182
|
+
formatted_all_links = utilties.search_utilities.format_links(links=self.search_system.all_links_of_system)
|
183
|
+
|
184
|
+
# Create final report with all parts
|
185
|
+
final_report_content = "\n\n".join(report_parts)
|
186
|
+
final_report_content = final_report_content + "\n\n## Sources\n\n" + formatted_all_links
|
187
|
+
|
188
|
+
# Create metadata dictionary
|
189
|
+
from datetime import datetime
|
190
|
+
metadata = {
|
191
|
+
"generated_at": datetime.utcnow().isoformat(),
|
192
|
+
"initial_sources": len(self.search_system.all_links_of_system),
|
193
|
+
"sections_researched": len(structure),
|
194
|
+
"searches_per_section": self.searches_per_section,
|
195
|
+
"query": query
|
196
|
+
}
|
197
|
+
|
198
|
+
# Return both content and metadata
|
199
|
+
return {
|
200
|
+
"content": final_report_content,
|
201
|
+
"metadata": metadata
|
202
|
+
}
|
203
|
+
|
204
|
+
def _generate_error_report(self, query: str, error_msg: str) -> str:
|
205
|
+
error_report = f"=== ERROR REPORT ===\nQuery: {query}\nError: {error_msg}"
|
206
|
+
return error_report
|
@@ -0,0 +1,241 @@
|
|
1
|
+
from typing import Dict, List, Optional, Callable
|
2
|
+
from datetime import datetime
|
3
|
+
from .utilties.search_utilities import remove_think_tags, format_findings_to_text, print_search_results, format_links
|
4
|
+
import os
|
5
|
+
from .utilties.enums import KnowledgeAccumulationApproach
|
6
|
+
from .config import settings, get_llm, get_search
|
7
|
+
from .citation_handler import CitationHandler
|
8
|
+
from datetime import datetime
|
9
|
+
from .utilties.search_utilities import extract_links_from_search_results
|
10
|
+
import logging
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
class AdvancedSearchSystem:
|
13
|
+
def __init__(self):
|
14
|
+
|
15
|
+
|
16
|
+
# Get fresh configuration
|
17
|
+
|
18
|
+
self.search = get_search()
|
19
|
+
self.model = get_llm()
|
20
|
+
self.max_iterations = settings.search.iterations
|
21
|
+
self.questions_per_iteration = settings.search.questions_per_iteration
|
22
|
+
|
23
|
+
self.context_limit = settings.general.knowledge_accumulation_context_limit
|
24
|
+
self.questions_by_iteration = {}
|
25
|
+
self.citation_handler = CitationHandler(self.model)
|
26
|
+
self.progress_callback = None
|
27
|
+
self.all_links_of_system = list()
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
def set_progress_callback(self, callback: Callable[[str, int, dict], None]) -> None:
|
32
|
+
"""Set a callback function to receive progress updates.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
callback: Function that takes (message, progress_percent, metadata)
|
36
|
+
"""
|
37
|
+
self.progress_callback = callback
|
38
|
+
|
39
|
+
def _update_progress(self, message: str, progress_percent: int = None, metadata: dict = None) -> None:
|
40
|
+
"""Send a progress update via the callback if available.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
message: Description of the current progress state
|
44
|
+
progress_percent: Progress percentage (0-100), if applicable
|
45
|
+
metadata: Additional data about the progress state
|
46
|
+
"""
|
47
|
+
if self.progress_callback:
|
48
|
+
self.progress_callback(message, progress_percent, metadata or {})
|
49
|
+
|
50
|
+
def _get_follow_up_questions(self, current_knowledge: str, query: str) -> List[str]:
|
51
|
+
now = datetime.now()
|
52
|
+
current_time = now.strftime("%Y-%m-%d")
|
53
|
+
|
54
|
+
self._update_progress("Generating follow-up questions...", None, {"iteration": len(self.questions_by_iteration)})
|
55
|
+
|
56
|
+
if self.questions_by_iteration:
|
57
|
+
prompt = f"""Critically reflect current knowledge (e.g., timeliness), what {self.questions_per_iteration} high-quality internet search questions remain unanswered to exactly answer the query?
|
58
|
+
Query: {query}
|
59
|
+
Today: {current_time}
|
60
|
+
Past questions: {str(self.questions_by_iteration)}
|
61
|
+
Knowledge: {current_knowledge}
|
62
|
+
Include questions that critically reflect current knowledge.
|
63
|
+
\n\n\nFormat: One question per line, e.g. \n Q: question1 \n Q: question2\n\n"""
|
64
|
+
else:
|
65
|
+
prompt = f" You will have follow up questions. First, identify if your knowledge is outdated (high chance). Today: {current_time}. Generate {self.questions_per_iteration} high-quality internet search questions to exactly answer: {query}\n\n\nFormat: One question per line, e.g. \n Q: question1 \n Q: question2\n\n"
|
66
|
+
|
67
|
+
response = self.model.invoke(prompt)
|
68
|
+
questions = [
|
69
|
+
q.replace("Q:", "").strip()
|
70
|
+
for q in remove_think_tags(response.content).split("\n")
|
71
|
+
if q.strip().startswith("Q:")
|
72
|
+
][: self.questions_per_iteration]
|
73
|
+
|
74
|
+
self._update_progress(
|
75
|
+
f"Generated {len(questions)} follow-up questions",
|
76
|
+
None,
|
77
|
+
{"questions": questions}
|
78
|
+
)
|
79
|
+
|
80
|
+
return questions
|
81
|
+
|
82
|
+
def _compress_knowledge(self, current_knowledge: str, query: str, section_links) -> List[str]:
|
83
|
+
self._update_progress("Compressing and summarizing knowledge...", None)
|
84
|
+
|
85
|
+
now = datetime.now()
|
86
|
+
current_time = now.strftime("%Y-%m-%d")
|
87
|
+
formatted_links = format_links(links=section_links)
|
88
|
+
if self.questions_by_iteration:
|
89
|
+
prompt = f"""First provide a high-quality 1 page explanation with IEEE Referencing Style e.g. [1,2]. Never make up sources. Than provide a exact high-quality one sentence-long answer to the query.
|
90
|
+
|
91
|
+
Knowledge: {current_knowledge}
|
92
|
+
Query: {query}
|
93
|
+
I will append following text to your output for the sources (dont repeat it):\n\n {formatted_links}"""
|
94
|
+
response = self.model.invoke(prompt)
|
95
|
+
|
96
|
+
self._update_progress("Knowledge compression complete", None)
|
97
|
+
response = remove_think_tags(response.content)
|
98
|
+
response = str(response) #+ "\n\n" + str(formatted_links)
|
99
|
+
print(response)
|
100
|
+
return response
|
101
|
+
|
102
|
+
def analyze_topic(self, query: str) -> Dict:
|
103
|
+
logger.info(f"Starting research on topic: {query}")
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
findings = []
|
108
|
+
current_knowledge = ""
|
109
|
+
iteration = 0
|
110
|
+
total_iterations = self.max_iterations
|
111
|
+
section_links = list()
|
112
|
+
|
113
|
+
self._update_progress("Initializing research system", 5, {
|
114
|
+
"phase": "init",
|
115
|
+
"iterations_planned": total_iterations
|
116
|
+
})
|
117
|
+
|
118
|
+
while iteration < self.max_iterations:
|
119
|
+
iteration_progress_base = (iteration / total_iterations) * 100
|
120
|
+
self._update_progress(f"Starting iteration {iteration + 1} of {total_iterations}",
|
121
|
+
int(iteration_progress_base),
|
122
|
+
{"phase": "iteration_start", "iteration": iteration + 1})
|
123
|
+
|
124
|
+
# Generate questions for this iteration
|
125
|
+
questions = self._get_follow_up_questions(current_knowledge, query)
|
126
|
+
self.questions_by_iteration[iteration] = questions
|
127
|
+
logger.info(f"Generated questions: {questions}")
|
128
|
+
question_count = len(questions)
|
129
|
+
for q_idx, question in enumerate(questions):
|
130
|
+
question_progress_base = iteration_progress_base + (((q_idx+1) / question_count) * (100/total_iterations) * 0.5)
|
131
|
+
|
132
|
+
self._update_progress(f"Searching for: {question}",
|
133
|
+
int(question_progress_base),
|
134
|
+
{"phase": "search", "iteration": iteration + 1, "question_index": q_idx + 1})
|
135
|
+
|
136
|
+
search_results = self.search.run(question)
|
137
|
+
|
138
|
+
if search_results is None:
|
139
|
+
self._update_progress(f"No search results found for question: {question}",
|
140
|
+
int(question_progress_base + 2),
|
141
|
+
{"phase": "search_complete", "result_count": 0})
|
142
|
+
search_results = [] # Initialize to empty list instead of None
|
143
|
+
continue
|
144
|
+
|
145
|
+
self._update_progress(f"Found {len(search_results)} results for question: {question}",
|
146
|
+
int(question_progress_base + 2),
|
147
|
+
{"phase": "search_complete", "result_count": len(search_results)})
|
148
|
+
|
149
|
+
logger.info("len search", len(search_results))
|
150
|
+
|
151
|
+
if len(search_results) == 0:
|
152
|
+
continue
|
153
|
+
|
154
|
+
self._update_progress(f"Analyzing results for: {question}",
|
155
|
+
int(question_progress_base + 5),
|
156
|
+
{"phase": "analysis"})
|
157
|
+
print("NR OF SOURCES: ", len(self.all_links_of_system))
|
158
|
+
result = self.citation_handler.analyze_followup(
|
159
|
+
question, search_results, current_knowledge, nr_of_links=len(self.all_links_of_system)
|
160
|
+
)
|
161
|
+
links = extract_links_from_search_results(search_results)
|
162
|
+
self.all_links_of_system.extend(links)
|
163
|
+
section_links.extend(links)
|
164
|
+
formatted_links = ""
|
165
|
+
if links:
|
166
|
+
formatted_links=format_links(links=links)
|
167
|
+
|
168
|
+
logger.debug(f"Generated questions: {formatted_links}")
|
169
|
+
if result is not None:
|
170
|
+
results_with_links = str(result["content"])
|
171
|
+
findings.append(
|
172
|
+
{
|
173
|
+
"phase": f"Follow-up {iteration}.{questions.index(question) + 1}",
|
174
|
+
"content": results_with_links,
|
175
|
+
"question": question,
|
176
|
+
"search_results": search_results,
|
177
|
+
"documents": result["documents"],
|
178
|
+
}
|
179
|
+
)
|
180
|
+
|
181
|
+
if settings.general.knowledge_accumulation != KnowledgeAccumulationApproach.NO_KNOWLEDGE:
|
182
|
+
current_knowledge = current_knowledge + "\n\n\n New: \n" + results_with_links
|
183
|
+
|
184
|
+
print(current_knowledge)
|
185
|
+
if settings.general.knowledge_accumulation == KnowledgeAccumulationApproach.QUESTION:
|
186
|
+
self._update_progress(f"Compress Knowledge for: {question}",
|
187
|
+
int(question_progress_base + 0),
|
188
|
+
{"phase": "analysis"})
|
189
|
+
current_knowledge = self._compress_knowledge(current_knowledge , query, section_links)
|
190
|
+
|
191
|
+
self._update_progress(f"Analysis complete for question: {question}",
|
192
|
+
int(question_progress_base + 10),
|
193
|
+
{"phase": "analysis_complete"})
|
194
|
+
|
195
|
+
iteration += 1
|
196
|
+
|
197
|
+
self._update_progress(f"Compressing knowledge after iteration {iteration}",
|
198
|
+
int((iteration / total_iterations) * 100 - 5),
|
199
|
+
{"phase": "knowledge_compression"})
|
200
|
+
if settings.general.knowledge_accumulation == KnowledgeAccumulationApproach.ITERATION:
|
201
|
+
current_knowledge = self._compress_knowledge(current_knowledge , query, section_links)
|
202
|
+
|
203
|
+
|
204
|
+
self._update_progress(f"Iteration {iteration} complete",
|
205
|
+
int((iteration / total_iterations) * 100),
|
206
|
+
{"phase": "iteration_complete", "iteration": iteration})
|
207
|
+
|
208
|
+
formatted_findings = self._save_findings(findings, current_knowledge, query)
|
209
|
+
|
210
|
+
self._update_progress("Research complete", 95, {"phase": "complete"})
|
211
|
+
|
212
|
+
return {
|
213
|
+
"findings": findings,
|
214
|
+
"iterations": iteration,
|
215
|
+
"questions": self.questions_by_iteration,
|
216
|
+
"formatted_findings": formatted_findings,
|
217
|
+
"current_knowledge": current_knowledge
|
218
|
+
}
|
219
|
+
|
220
|
+
def _save_findings(self, findings: List[Dict], current_knowledge: str, query: str):
|
221
|
+
self._update_progress("Saving research findings...", None)
|
222
|
+
|
223
|
+
formatted_findings = format_findings_to_text(
|
224
|
+
findings, current_knowledge, self.questions_by_iteration
|
225
|
+
)
|
226
|
+
safe_query = "".join(x for x in query if x.isalnum() or x in [" ", "-", "_"])[
|
227
|
+
:50
|
228
|
+
]
|
229
|
+
safe_query = safe_query.replace(" ", "_").lower()
|
230
|
+
|
231
|
+
output_dir = "research_outputs"
|
232
|
+
if not os.path.exists(output_dir):
|
233
|
+
os.makedirs(output_dir)
|
234
|
+
|
235
|
+
filename = os.path.join(output_dir, f"formatted_output_{safe_query}.txt")
|
236
|
+
|
237
|
+
with open(filename, "w", encoding="utf-8") as text_file:
|
238
|
+
text_file.write(formatted_findings)
|
239
|
+
|
240
|
+
self._update_progress("Research findings saved", None, {"filename": filename})
|
241
|
+
return formatted_findings
|
File without changes
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# utilties/llm_utils.py
|
2
|
+
"""
|
3
|
+
LLM utilities for Local Deep Research.
|
4
|
+
|
5
|
+
This module provides utility functions for working with language models
|
6
|
+
when the user's llm_config.py is missing or incomplete.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any, Optional
|
12
|
+
|
13
|
+
# Setup logging
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
def get_model(
|
17
|
+
model_name: Optional[str] = None,
|
18
|
+
model_type: Optional[str] = None,
|
19
|
+
temperature: Optional[float] = None,
|
20
|
+
**kwargs
|
21
|
+
) -> Any:
|
22
|
+
"""
|
23
|
+
Get a language model instance as fallback when llm_config.get_llm is not available.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
model_name: Name of the model to use
|
27
|
+
model_type: Type of the model provider
|
28
|
+
temperature: Model temperature
|
29
|
+
**kwargs: Additional parameters
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
LangChain language model instance
|
33
|
+
"""
|
34
|
+
# Get default values from kwargs or use reasonable defaults
|
35
|
+
model_name = model_name or kwargs.get('DEFAULT_MODEL', 'mistral')
|
36
|
+
model_type = model_type or kwargs.get('DEFAULT_MODEL_TYPE', 'ollama')
|
37
|
+
temperature = temperature or kwargs.get('DEFAULT_TEMPERATURE', 0.7)
|
38
|
+
max_tokens = kwargs.get('max_tokens', kwargs.get('MAX_TOKENS', 30000))
|
39
|
+
|
40
|
+
# Common parameters
|
41
|
+
common_params = {
|
42
|
+
"temperature": temperature,
|
43
|
+
"max_tokens": max_tokens,
|
44
|
+
}
|
45
|
+
|
46
|
+
# Add additional kwargs
|
47
|
+
for key, value in kwargs.items():
|
48
|
+
if key not in ['DEFAULT_MODEL', 'DEFAULT_MODEL_TYPE', 'DEFAULT_TEMPERATURE', 'MAX_TOKENS']:
|
49
|
+
common_params[key] = value
|
50
|
+
|
51
|
+
# Try to load the model based on type
|
52
|
+
if model_type == "ollama":
|
53
|
+
try:
|
54
|
+
from langchain_ollama import ChatOllama
|
55
|
+
return ChatOllama(model=model_name, **common_params)
|
56
|
+
except ImportError:
|
57
|
+
try:
|
58
|
+
from langchain_community.llms import Ollama
|
59
|
+
return Ollama(model=model_name, **common_params)
|
60
|
+
except ImportError:
|
61
|
+
logger.error("Neither langchain_ollama nor langchain_community.llms.Ollama available")
|
62
|
+
raise
|
63
|
+
|
64
|
+
elif model_type == "openai":
|
65
|
+
try:
|
66
|
+
from langchain_openai import ChatOpenAI
|
67
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
68
|
+
if not api_key:
|
69
|
+
raise ValueError("OPENAI_API_KEY environment variable not set")
|
70
|
+
return ChatOpenAI(model=model_name, api_key=api_key, **common_params)
|
71
|
+
except ImportError:
|
72
|
+
logger.error("langchain_openai not available")
|
73
|
+
raise
|
74
|
+
|
75
|
+
elif model_type == "anthropic":
|
76
|
+
try:
|
77
|
+
from langchain_anthropic import ChatAnthropic
|
78
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
79
|
+
if not api_key:
|
80
|
+
raise ValueError("ANTHROPIC_API_KEY environment variable not set")
|
81
|
+
return ChatAnthropic(model=model_name, anthropic_api_key=api_key, **common_params)
|
82
|
+
except ImportError:
|
83
|
+
logger.error("langchain_anthropic not available")
|
84
|
+
raise
|
85
|
+
|
86
|
+
elif model_type == "openai_endpoint":
|
87
|
+
try:
|
88
|
+
from langchain_openai import ChatOpenAI
|
89
|
+
api_key = os.getenv("OPENAI_ENDPOINT_API_KEY")
|
90
|
+
if not api_key:
|
91
|
+
raise ValueError("OPENAI_ENDPOINT_API_KEY environment variable not set")
|
92
|
+
|
93
|
+
endpoint_url = kwargs.get("OPENAI_ENDPOINT_URL", "https://openrouter.ai/api/v1")
|
94
|
+
|
95
|
+
if model_name is None and not kwargs.get("OPENAI_ENDPOINT_REQUIRES_MODEL", True):
|
96
|
+
return ChatOpenAI(api_key=api_key, openai_api_base=endpoint_url, **common_params)
|
97
|
+
else:
|
98
|
+
return ChatOpenAI(model=model_name, api_key=api_key, openai_api_base=endpoint_url, **common_params)
|
99
|
+
except ImportError:
|
100
|
+
logger.error("langchain_openai not available")
|
101
|
+
raise
|
102
|
+
|
103
|
+
# Default fallback
|
104
|
+
try:
|
105
|
+
from langchain_ollama import ChatOllama
|
106
|
+
logger.warning(f"Unknown model type '{model_type}', defaulting to Ollama")
|
107
|
+
return ChatOllama(model=model_name, **common_params)
|
108
|
+
except (ImportError, Exception) as e:
|
109
|
+
logger.error(f"Failed to load any model: {e}")
|
110
|
+
|
111
|
+
# Last resort: create a dummy model
|
112
|
+
try:
|
113
|
+
from langchain_community.llms.fake import FakeListLLM
|
114
|
+
return FakeListLLM(responses=["No language models are available. Please install Ollama or set up API keys."])
|
115
|
+
except ImportError:
|
116
|
+
raise ValueError("No language models available and could not create dummy model")
|