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.
Files changed (56) hide show
  1. local_deep_research/__init__.py +24 -0
  2. local_deep_research/citation_handler.py +113 -0
  3. local_deep_research/config.py +166 -0
  4. local_deep_research/defaults/__init__.py +44 -0
  5. local_deep_research/defaults/llm_config.py +269 -0
  6. local_deep_research/defaults/local_collections.toml +47 -0
  7. local_deep_research/defaults/main.toml +57 -0
  8. local_deep_research/defaults/search_engines.toml +244 -0
  9. local_deep_research/local_collections.py +141 -0
  10. local_deep_research/main.py +113 -0
  11. local_deep_research/report_generator.py +206 -0
  12. local_deep_research/search_system.py +241 -0
  13. local_deep_research/utilties/__init__.py +0 -0
  14. local_deep_research/utilties/enums.py +9 -0
  15. local_deep_research/utilties/llm_utils.py +116 -0
  16. local_deep_research/utilties/search_utilities.py +115 -0
  17. local_deep_research/utilties/setup_utils.py +6 -0
  18. local_deep_research/web/__init__.py +2 -0
  19. local_deep_research/web/app.py +1209 -0
  20. local_deep_research/web/static/css/styles.css +1008 -0
  21. local_deep_research/web/static/js/app.js +2078 -0
  22. local_deep_research/web/templates/api_keys_config.html +82 -0
  23. local_deep_research/web/templates/collections_config.html +90 -0
  24. local_deep_research/web/templates/index.html +312 -0
  25. local_deep_research/web/templates/llm_config.html +120 -0
  26. local_deep_research/web/templates/main_config.html +89 -0
  27. local_deep_research/web/templates/search_engines_config.html +154 -0
  28. local_deep_research/web/templates/settings.html +519 -0
  29. local_deep_research/web/templates/settings_dashboard.html +207 -0
  30. local_deep_research/web_search_engines/__init__.py +0 -0
  31. local_deep_research/web_search_engines/engines/__init__.py +0 -0
  32. local_deep_research/web_search_engines/engines/full_search.py +128 -0
  33. local_deep_research/web_search_engines/engines/meta_search_engine.py +274 -0
  34. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +367 -0
  35. local_deep_research/web_search_engines/engines/search_engine_brave.py +245 -0
  36. local_deep_research/web_search_engines/engines/search_engine_ddg.py +123 -0
  37. local_deep_research/web_search_engines/engines/search_engine_github.py +663 -0
  38. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +283 -0
  39. local_deep_research/web_search_engines/engines/search_engine_guardian.py +337 -0
  40. local_deep_research/web_search_engines/engines/search_engine_local.py +901 -0
  41. local_deep_research/web_search_engines/engines/search_engine_local_all.py +153 -0
  42. local_deep_research/web_search_engines/engines/search_engine_medrxiv.py +623 -0
  43. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +992 -0
  44. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +230 -0
  45. local_deep_research/web_search_engines/engines/search_engine_wayback.py +474 -0
  46. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +242 -0
  47. local_deep_research/web_search_engines/full_search.py +254 -0
  48. local_deep_research/web_search_engines/search_engine_base.py +197 -0
  49. local_deep_research/web_search_engines/search_engine_factory.py +233 -0
  50. local_deep_research/web_search_engines/search_engines_config.py +54 -0
  51. local_deep_research-0.1.0.dist-info/LICENSE +21 -0
  52. local_deep_research-0.1.0.dist-info/METADATA +328 -0
  53. local_deep_research-0.1.0.dist-info/RECORD +56 -0
  54. local_deep_research-0.1.0.dist-info/WHEEL +5 -0
  55. local_deep_research-0.1.0.dist-info/entry_points.txt +3 -0
  56. 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,9 @@
1
+ # config/enums.py
2
+ from enum import Enum, auto
3
+
4
+ class KnowledgeAccumulationApproach(Enum):
5
+ QUESTION = auto()
6
+ ITERATION = auto()
7
+ NO_KNOWLEDGE = auto()
8
+ MAX_NR_OF_CHARACTERS = auto()
9
+
@@ -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")